Unity3D —— 第一人称射箭游戏

游戏简介

游戏要求

游戏画面

游戏演示视频如下:

射箭游戏演示视频

玩家动作表

WASD:玩家前后左右移动

按下左键:拉弓开始蓄力

松开左键:射出弓箭

按下右键:切换天空盒

鼠标移动:视角跟随移动

P:背景音乐暂停/播放

箭射中靶子:根据规则计分

游戏资源

地形设计和弓弩以及天空盒的资源直接从Unity官方的资源商店中下载免费资源导入到项目当中

做一个靶子的预制体,白色区域和靶心是得分区域,其中靶心得分更高

将弓弩的预制体拉到主摄像头下作为它的子对象,这样可以跟随第一人称视角移动

地图的中间是射击区域,只有进入栅栏围起来的区域才可以射击,周围有三个靶子,其中两边的靶子是移动靶子,射中的得分更多

代码设计

玩家移动控制器

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PlayerMovement : MonoBehaviour
{
    public CharacterController controller;

    public float speed = 5;
    public float gravity = -9.18f;
    public float jumpHeight = 3f;

    public Transform groundCheck;
    public float groundDistance = 0.4f;
    public LayerMask groundMask;

    Vector3 velocity;
    bool isGrounded;
    void Update()
    {
        isGrounded = Physics.CheckSphere(groundCheck.position, groundDistance, groundMask);

        if (isGrounded && velocity.y < 0)
        {
            velocity.y = -2f;
        }

        if (Input.GetKey("left shift") && isGrounded)
        {
            speed = 10;
        }
        else
        {
            speed = 5;
        }

        float x = Input.GetAxis("Horizontal");
        float z = Input.GetAxis("Vertical");

        Vector3 move = transform.right * x + transform.forward * z;

        controller.Move(move * speed * Time.deltaTime);

        if (Input.GetButtonDown("Jump") && isGrounded)
        {
            velocity.y = Mathf.Sqrt(jumpHeight * -2f * gravity);
        }

        velocity.y += gravity * Time.deltaTime;

        controller.Move(velocity * Time.deltaTime);
    }
}

天空盒切换

按下右键在两个天空盒材质之间切换,挂载在主摄像机上即可

using UnityEngine;

public class SkyboxSwitcher : MonoBehaviour
{
    //第一个天空盒
    public Material skybox1; 
    //第二个天空盒
    public Material skybox2; 

    //当前激活的天空盒是否为天空盒1
    private bool isSkybox1Active = true; 

    private void Update()
    {
        //如果按下C键,切换天空盒
        if (Input.GetMouseButtonDown(1))
        {
            SwitchSkybox();
        }
    }

    private void SwitchSkybox()
    {
        //切换天空盒的状态
        isSkybox1Active = !isSkybox1Active;

        if (isSkybox1Active)
        {
            //设置渲染设置中的天空盒材质为第一个天空盒材质
            RenderSettings.skybox = skybox1;
        }
        else
        {
            //设置渲染设置中的天空盒材质为第二个天空盒材质
            RenderSettings.skybox = skybox2;
        }
    }
}

射箭实现

控制弓弩射箭的脚本,挂载在弓弩对象上,同时要添加射击区域,射箭点,箭数文本等属性

若进入射击区域,则显示可用箭的数量,表示可以进行射击

按下左键重置蓄力条,播放拉弓动画,持续蓄力更新进度条,松开鼠标射出弓箭,在弓弩前添加一个子对象作为射击点,射箭瞬间在射击点实例化一支弓箭设置速度射出

using UnityEngine;
using UnityEngine.UI;
using TMPro;

public class Bow : MonoBehaviour
{
    // 导入箭的预制体
    public GameObject arrowPrefab;
    // 箭的Transform组件
    public Transform arrowSpawnPoint;
    // 弓的最大拉动距离
    public float maxPullDistance = 3f;
    // 弓的最大拉动力度
    public float maxPullForce = 100f;
    // 弓的最小拉动时间
    public float minPullTime = 1f;
    // 弓的最大拉动时间
    public float maxPullTime = 5f;
    // 箭的飞行速度
    public float arrowFlightSpeed = 10f;

    // 开始的拉动时间
    private float pullStartTime;
    // 拉动的距离
    private float pullDistance;
    // 弓的动画控制器
    private Animator anim;

    // 射击区域
    public ShootingArea shootingArea;
    // 箭的数量text
    public TMP_Text arrowCountTxt;
    // 箭的数量UI
    public GameObject arrowCount;

    // 游戏介绍的UI
    public GameObject over;

    // 蓄力条
    public Slider chargeSlider;

    void Start()
    {
        Time.timeScale = 1;
        anim = GetComponent<Animator>();
        LockCursor(true);
        chargeSlider.value = 0f;  // 初始化蓄力条
    }

    private void Update()
    {
        if (Input.GetKeyDown(KeyCode.Escape) && Cursor.visible) { LockCursor(false); }

        if (Input.GetMouseButtonDown(0) && Cursor.visible == false) { LockCursor(true); }

        if (shootingArea == null)
        {
            Debug.Log("shootingArea is not assigned or cannot be found!");
            arrowCount.SetActive(false);
            return;
        }
        else
        {
            arrowCount.SetActive(true);
            arrowCountTxt.text = "Arrow:" + shootingArea.arrowCount;
        }

        if (shootingArea.isArrow && shootingArea.arrowCount > 0)
        {
            if (Input.GetMouseButtonDown(0))  // 按下左键开始拉弓
            {
                pullStartTime = 0;
                anim.SetTrigger("hold");
                chargeSlider.value = 0f;  // 重置蓄力条
                // 清除场景中的箭
                FindBullet();
            }

            else if (Input.GetMouseButton(0))  // 持续蓄力
            {
                // 增加拉动箭头的时间
                pullStartTime += Time.deltaTime;
                // 将拉动箭头的时间设置为前面计算得到的时间
                anim.SetFloat("holdTime", pullStartTime);
                // 更新蓄力条进度
                chargeSlider.value = Mathf.Clamp(pullStartTime / maxPullTime, 0f, 1f);
            }

            else if (Input.GetMouseButtonUp(0))  // 松开左键释放箭
            {
                pullDistance = pullStartTime;
                pullStartTime = 0;
                anim.SetTrigger("shoot");  // 播放射箭动画
                ShootArrow();
                Invoke("FindShootingArea", 1.5f);  // 1.5秒后查找射击区域
            }
        }
    }

    private void ShootArrow()
    {
        // 实例化箭
        GameObject arrow = Instantiate(arrowPrefab, arrowSpawnPoint.position, arrowSpawnPoint.rotation);
        Rigidbody arrowRigidbody = arrow.GetComponent<Rigidbody>();
        // 根据拉动距离给箭设定速度
        arrowRigidbody.velocity = transform.forward * pullDistance * 30f;
        shootingArea.arrowCount -= 1;
        arrowCountTxt.text = "Arrow:" + shootingArea.arrowCount;

        // 重置蓄力条回到起始位置
        chargeSlider.value = 0f;
    }


    public void FindBullet()
    {
        var bullets = GameObject.FindGameObjectsWithTag("Bullet");
        for (int i = 0; i < bullets.Length; i++)
        {
            Destroy(bullets[i]);
        }
    }

    public void FindShootingArea()
    {
        var ShootingAreas = GameObject.FindGameObjectsWithTag("ShootingArea");
        var temp = 0;
        for (int i = 0; i < ShootingAreas.Length; i++)
        {
            if (ShootingAreas[i].transform.GetComponent<ShootingArea>().arrowCount > 0)
            {
                temp++;
            }
        }
        if (temp <= 0)
        {
            LockCursor(false);
            over.SetActive(true);
            Time.timeScale = 0;
        }
    }

    public void LockCursor(bool a)
    {
        if (a)
        {
            Cursor.lockState = CursorLockMode.Locked;
            Cursor.visible = false;
        }
        else
        {
            Cursor.lockState = CursorLockMode.None;
            Cursor.visible = true;
        }
    }
}

射击区域

挂载到一个有碰撞器组件的空对象中,调整大小作为射击区域,当玩家进入和离开区域会触发碰撞,执行OnTriggerStay和OnTriggerExit函数,设置射箭的标志位

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class ShootingArea : MonoBehaviour
{
    //将该区域的可用箭数初始化为5
    public int arrowCount = 10;
    //设置一个变量记录该区域是否有箭
    public bool isArrow;
    //记录玩家是否在区域内
    private bool isPlayer;

    private void OnTriggerStay(Collider other)
    {
        //如果玩家已经在区域内
        if (isPlayer) return;
        //如果该区域的触发器与标签为player的玩家发生碰撞
        if (other.gameObject.tag == "Player")
        {
            //更新相关的变量
            isPlayer = true;
            isArrow = true;
            //获取玩家物体上的脚本,并且将射击区域设置为当前的脚本
            other.gameObject.transform.GetComponent<Bow>().shootingArea = this;
            Debug.Log("Player entered shooting area.");
        }
    }


    private void OnTriggerExit(Collider other)
    {
        //如果触发器与标签为player的玩家离开碰撞
        if (other.gameObject.tag == "Player")
        {
            //更新相关变量
            Debug.Log("asfasfsafasfasfsaf");
            isPlayer = false;
            if (other.gameObject.transform.GetComponent<Bow>().shootingArea != null)
            {
                //将射击区域内的箭矢数量赋值给玩家物体上的Bow脚本的射击区域的箭矢数量
                arrowCount = other.gameObject.transform.GetComponent<Bow>().shootingArea.arrowCount;
            }
            isArrow = false;
            //将玩家物体上的Bow脚本的射击区域设置为null
            other.gameObject.transform.GetComponent<Bow>().shootingArea = null;
        }
    }
}

靶子控制脚本

当箭和靶子的得分区域发生碰撞执行函数计算得分,挂载在靶子对象上

using UnityEngine;
using UnityEngine.UI;
using TMPro;

public class Target : MonoBehaviour
{
    // 添加一个静态变量,表示玩家的总分数
    public static int playerScore = 0;

    // UI 文本用于显示分数
    public TMP_Text scoreText;
    
    //是否为运动靶子
    public bool isSportsTarget;

    //靶子的位置点
    private Transform point;

    //靶子的索引
    public int indexTarget;

    private void Start()
    {
        // 初始化分数为 0
        playerScore = 0;
        UpdateScoreText();
        //获取靶子的父对象作为位置点
        point = transform.parent;
    }

    private void OnCollisionEnter(Collision collision)
    {
        if (collision.gameObject.CompareTag("Bullet"))
        {
            // 调用函数计算得分
            CalculateScore();
            collision.transform.GetComponent<Rigidbody>().isKinematic = true;
            collision.transform.position = new Vector3(collision.contacts[0].point.x, collision.contacts[0].point.y, collision.contacts[0].point.z - Random.Range(-0.3f, -0.5f));
            collision.gameObject.transform.parent = point;
        }
    }

    private void CalculateScore()
    {
        int scoreToAdd = 0;

        if (gameObject.tag == "Bullseye")
        {
            scoreToAdd = isSportsTarget ? 10 : 8;
        }
        else if (gameObject.tag == "Circle")
        {
            scoreToAdd = isSportsTarget ? 5 : 3;
        }

        // 更新总分
        playerScore += scoreToAdd;
        UpdateScoreText();

        // 显示提示信息
        Tips.Instance.SetText($"在{indexTarget}号射击位上射中{indexTarget}号靶子,加{scoreToAdd}分");
    }

    private void UpdateScoreText()
    {
        if (scoreText != null)
        {
            scoreText.text = $"Score: {playerScore}";
        }
    }
}

靶子移动

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class TargetMove : MonoBehaviour
{
    //靶子的移动速度
    public float speed = 5f; 
    //靶子的移动距离
    public float distance = 10f; 

    //靶子的起始位置
    private Vector3 startPosition;
    //靶子的移动方向
    private float direction = 1f;

    void Start()
    {
        //记录起始位置
        startPosition = transform.position;
    }

    void Update()
    {
        //计算下一帧的位置
        Vector3 nextPosition = transform.position + new Vector3(speed * direction * Time.deltaTime, 0f, 0f);

        // 判断是否超出移动范围,超出则改变移动方向
        if (Vector3.Distance(startPosition, nextPosition) > distance)
        {
            direction *= -1f;
        }

        // 更新位置
        transform.position = nextPosition;
    }
}

碰撞检测

当检测到标签为bullet的箭和靶子发送碰撞时,禁用箭的 Rigidbody,使其静止,并获取碰撞点和法线向量,调整箭的旋转,使箭能够垂直于靶子定在上面模拟中靶的效果

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class ArrowStick : MonoBehaviour
{
    private void OnCollisionEnter(Collision collision)
    {
        // 检查碰撞的物体是否是箭(标签为 Bullet)
        if (collision.gameObject.CompareTag("Bullet"))
        {
            // 禁用箭的 Rigidbody,使其静止
            Rigidbody arrowRigidbody = collision.gameObject.GetComponent<Rigidbody>();
            if (arrowRigidbody != null)
            {
                arrowRigidbody.isKinematic = true;
                arrowRigidbody.velocity = Vector3.zero; // 停止箭的运动
                arrowRigidbody.angularVelocity = Vector3.zero; // 停止旋转
            }

            // 获取碰撞点和法线向量
            ContactPoint contact = collision.contacts[0];
            Vector3 hitPoint = contact.point;       // 碰撞点
            Vector3 hitNormal = contact.normal;     // 碰撞法线(靶子的表面方向)

            // 设置箭的位置为碰撞点,并稍微向后调整(避免穿透)
            collision.transform.position = hitPoint - hitNormal * 0.01f; // 根据法线向后偏移少许

            // 设置箭的旋转,使其方向垂直于靶子表面
            collision.transform.rotation = Quaternion.LookRotation(-hitNormal);

            // 将箭的父对象设置为靶子,使其固定在靶子上
            collision.transform.SetParent(this.transform);
        }
    }
}

背景音乐

using UnityEngine;

public class MusicToggleByKey : MonoBehaviour
{
    public AudioSource backgroundMusic; // 拖入背景音乐的 AudioSource

    void Update()
    {
        // 检测 P 键是否被按下
        if (Input.GetKeyDown(KeyCode.P))
        {
            ToggleMusic();
        }
    }

    void ToggleMusic()
    {
        if (backgroundMusic.isPlaying)
        {
            backgroundMusic.Pause(); // 如果正在播放,则暂停
        }
        else
        {
            backgroundMusic.Play(); // 如果暂停,则播放
        }
    }
}

完整代码

由于篇幅关系只能展示部分代码,完整代码仓库可以在下面链接下载

完整代码仓库链接

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值