unity实现第一人称的打靶(射箭)游戏

目录

游戏要求

项目地址与视频展示

玩家动作表(游戏规则表)

图形设计

弩弓与弩箭设计

靶子设计

天空盒设计

地形设计

动画控制器设计

原理及代码设计

弩弓与弩箭的实现Bow

玩家移动控制器PlayerMove

设计区域控制ShootingArea

靶子各个区域不同得分的控制Target

控制靶子的移动TargetMove

提示和积分管理Tips

天空盒切换SkyboxSwitcher

主相机(主视觉)移动控制器CameraMove


游戏要求

本项目游戏是3D游戏设计的一个期中大作业,其中具体的实验要求如下所示:

  •  地形:使用地形组件,上面有草、树;
  •  天空盒:使用天空盒,天空可随玩家位置 或 时间变化 或 按特定按键切换天空盒;
  •  固定靶:有一个以上固定的靶标;
  •  运动靶:有一个以上运动靶标,运动轨迹,速度使用动画控制;
  •  射击位:地图上应标记若干射击位,仅在射击位附近可以拉弓射击,每个位置有 n 次机会;
  •  驽弓动画:支持蓄力半拉弓,然后 hold,择机 shoot;
  •  游走:玩家的驽弓可在地图上游走,不能碰上树和靶标等障碍;
  •  碰撞与计分:在射击位,射中靶标的相应分数,规则自定;

项目地址与视频展示

本游戏项目的代码仓库地址为:github的仓库地址

本游戏项目的演示视频地址为:本打靶(射箭)游戏演示视频

玩家动作表(游戏规则表)

动作 条件 结果
WASD

玩家(弩弓)在设计好的地形上

玩家(弩弓)前后左右移动
按下鼠标右键 天空盒发生变化

玩家(弩弓)

移动

玩家(弩弓)与树木、靶子发生碰撞 玩家(弩弓)停止移动
按下鼠标左键 玩家(弩弓)在射击位置上 弩弓开始蓄力
长按鼠标左键 弩弓的拉力不断增加直至所设定的上限
松开鼠标左键 弩弓根据长按的时间所计算的拉力来发射弩箭
鼠标移动 玩家(第一人称)的视觉范围发生变化
弩箭射中靶子 弩箭的碰撞检测体与靶子的碰撞检测体发生碰撞 总得分根据我们为每一个靶子所设定的分数进行增加

图形设计

弩弓与弩箭设计

在本游戏项目中,我们采用的弩弓与弩箭预制体为导入的资源包里面的预制体和model,采用的是资源商店里面下载的Classical Crossbow。如图所示:

靶子设计

通过不同大小、不同形状的基础物体来构成。最外面的绿色区域为Cube,白色区域为Cylinder,而最里面的靶心(红色)区域也是为Cylinder,通过修改其半径来显示不同的Cylinder,同时还需要为靶子不同的部分构建不同的碰撞检测体。最后将这一个构成的集合物体建成预制体,以供生成后面的靶子。

天空盒设计

在本次游戏项目中,我们还需要实现天空盒的切换,而我在这里采用的方法就是通过按动鼠标右键来切换天空盒。我们首先需要构建天空盒,我们可以直接从资源商店中导入相应的资源,我们导入的是资源是8K Skybox Pack Free,在这一个资源中,我们可以看到有很多个天空盒供我们自己选择,我们随便选择两种来实现天空盒的切换即可。我们采用的是天空盒3和天空盒10。

地形设计

我们首先通过鼠标左键添加3D对象中的Terrain,然后根据Inspector界面中的不同选择来对地形进行修改和描绘,可以增高或降低地形,可以为地形增加不同的纹理。我们还可以为地形种树,我们选择不同的树木,然后点击Terrain即可实现种树的功能。一样的,我们还可以选择种草,我们选择不一样的小草,然后点击Terrain即可实现种草的功能。我们可以通过不同的选项来实现对地形进行不同的修改功能。

动画控制器设计

在本游戏项目中,主要的动画控制为弩箭的蓄力、发射等,我们导入的资源中,它已经自带了一些动作包以及一个动画控制器,我们可以直接采用才动画控制器,然后在这个的基础上进行修改即可。

原理及代码设计

弩弓与弩箭的实现Bow

1. Start(): 游戏开始时的初始化操作。设置时间缩放为正常速度,获取弓的动画控制器,锁定鼠标光标。

2. Update(): 每帧执行的更新操作:

       检测是否按下了 Escape 键来切换光标锁定状态。
       检测是否按下了鼠标左键来切换光标锁定状态。 
       检查射击区域是否存在,如果不存在则隐藏箭的数量文本并返回。
       如果射击区域存在,显示箭的数量文本,并更新箭的数量显示。
       检查是否可以射击箭,并且当前还有剩余的箭。
       如果按下鼠标左键,重置拉动开始时间,触发弓的拉动动画,并调用 FindBullet() 函数清除场         景中的箭。
       如果按住鼠标左键,增加拉动箭头的时间,并将该时间值设置为弓的拉动动画的参数。
       如果松开鼠标左键,将拉动距离设为拉动开始时间,重置拉动开始时间,触发弓的射击动               画,并调用 ShootArrow() 函数发射箭,并在1.5秒后调用 FindShootingArea() 函数查找射击           区域。
3. ShootArrow(): 发射箭的函数。实例化箭游戏对象,获取箭的刚体组件,根据拉动距离设置箭的速度,减少射击区域的箭数量,并更新箭的数量显示。

4. FindBullet(): 清除场景中的箭的函数。通过标签找到所有的箭游戏对象,然后销毁它们。

5. FindShootingArea(): 查找射击区域的函数。通过标签找到所有的射击区域游戏对象,检查它们是否还有剩余的箭。如果没有任何射击区域剩余箭了,解锁光标,显示游戏结束的 UI,并将时间缩放设为0,即暂停游戏。

6. LockCursor(bool a): 锁定/解锁光标的函数。根据传入的布尔值来切换光标的锁定状态和可见性。如果传入 true,则锁定光标并隐藏;如果传入 false,则解锁光标并显示。

using UnityEngine;
using UnityEngine.UI;

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 Text arrowCountTxt;
    //箭的数量UI
    public GameObject arrowCount;

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

    void Start()
    {
        Time.timeScale = 1;
        anim = GetComponent<Animator>();
        LockCursor(true);

    }

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

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

        if (shootingArea == null) 
        {
            arrowCount.SetActive(false);
            return;
        }
        else
        {
            arrowCount.SetActive(true);
            arrowCountTxt.text = "箭数:" + shootingArea.arrowCount;
        }
        

        if (shootingArea.isArrow && shootingArea.arrowCount > 0)
        {
            if (Input.GetMouseButtonDown(0))
            {
                pullStartTime = 0;
                anim.SetTrigger("hold");
                //调用该函数清除场景中的箭
                FindBullet();
            }
            else if (Input.GetMouseButton(0))
            {
                //增加拉动箭头的时间
                pullStartTime += Time.deltaTime;
                //将拉动箭头的时间设置为前面计算得到的时间
                anim.SetFloat("holdTime", pullStartTime);
            }//玩家松开鼠标左键释放箭
            else if (Input.GetMouseButtonUp(0))
            {
                pullDistance = pullStartTime;
                pullStartTime = 0;
                anim.SetTrigger("shoot");
                ShootArrow();
                Invoke("FindShootingArea", 1.5f);
            }
        }
    }

    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 = "箭数:" + shootingArea.arrowCount;
    }

    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;
        }
    }
}

玩家移动控制器PlayerMove

1. Start(): 游戏开始时的初始化操作。获取角色的 CharacterController 组件。

2. Update(): 每帧执行的更新操作。调用 Move() 函数来处理角色的移动逻辑。

3. Move(): 处理角色移动的函数:

        通过输入获取水平方向(X轴)和垂直方向(Z轴)的按键输入值。
        根据输入值计算出移动方向,并将其乘以移动速度和时间增量,得到最终的移动向量。
        使用 CharacterController 的 Move() 方法来移动角色,传入计算得到的移动向量。
        根据重力值和时间增量,更新垂直方向上的速度(velocity.y)。
        再次使用 CharacterController 的 Move() 方法来应用垂直方向上的速度变化,使角色受到重            力影响下落或上升。

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

public class PlayerMove : MonoBehaviour
{
    //人物控制器
    private CharacterController controller;
    //人物移动速度
    public float speed = 2f;
    public float gravity = -15f;
    Vector3 velocity;

    private void Start()
    {
        controller = GetComponent<CharacterController>();
    }


    // Update is called once per frame
    void Update()
    {
       Move();
    }


    public void Move()
    {
        //键盘输入
        float x = Input.GetAxis("Horizontal");
        float z = Input.GetAxis("Vertical");

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

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

        velocity.y += gravity * Time.deltaTime;

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

射击区域控制ShootingArea

1. arrowCount: 用于记录射击区域内剩余的箭的数量,初始值为10。

2. isArrow: 用于表示射击区域内是否有可用的箭。

3. isPlayer: 用于记录玩家是否在射击区域内。

4. OnTriggerStay(Collider other): 当有物体停留在射击区域内时触发的函数:

       检查如果玩家已经在射击区域内,直接返回,不执行后续操作。
       检查与射击区域碰撞的物体是否具有 "Player" 标签。
       如果是玩家物体发生碰撞:
       更新相关的变量,将 isPlayer 和 isArrow 设置为 true。
       获取玩家物体上的 Bow 脚本,并将射击区域设置为当前的脚本。

5. OnTriggerExit(Collider other): 当有物体离开射击区域时触发的函数:

       检查离开触发器的物体是否具有 "Player" 标签。
       如果是玩家物体离开:
       更新相关变量,将 isPlayer 设置为 false。
       检查玩家物体上的 Bow 脚本的 shootingArea 是否为 null。
       如果不为 null,将射击区域内的箭矢数量赋值给玩家物体上的 Bow 脚本的射击区域的箭矢数           量。
       将 isArrow 设置为 false。
       将玩家物体上的 Bow 脚本的射击区域设置为 null。

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

public class ShootingArea : MonoBehaviour
{
    //将该区域的可用箭数初始化为10
    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;
        }
    }


    private void OnTriggerExit(Collider other)
    {
        //如果触发器与标签为player的玩家离开碰撞
        if (other.gameObject.tag == "Player")
        {
            //更新相关变量
            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;
        }
    }
}

靶子各个区域不同得分的控制Target

1. score: 靶子的得分,初始值为1。

2. isSportsTarget: 表示靶子是否为运动靶子。

3. point: 靶子的位置点的Transform组件。

4. indexTarget: 靶子的索引。

5. Start(): 在脚本启动时进行初始化操作。获取靶子的父对象作为位置点。

6. OnCollisionEnter(Collision collision): 当靶子弩箭弹发生碰撞时触发的函数:

       检查碰撞的物体是否具有 "Bullet" 标签。
           如果是弩箭发生碰撞:
               调用 CalculateScore() 函数计算得分。
               将碰撞的弩箭的刚体设为运动学(isKinematic),停止其物理模拟。
               将碰撞点的位置稍微偏移并将弩箭放置在靶子的位置点下。
                将弩箭的父对象设置为位置点。
7. CalculateScore(): 计算得分的函数:

       检查靶子的标签。
           如果靶子的标签为 "Bullseye",表示击中了靶心:
               检查靶子是否为运动靶子。
                   如果是运动靶子,将得分加10,并显示相应的提示信息。
                   如果不是运动靶子,将得分加8,并显示相应的提示信息。
           如果靶子的标签为 "Circle",表示击中了白色区域:
                检查靶子是否为运动靶子。
                    如果是运动靶子,将得分加5,并显示相应的提示信息。
                    如果不是运动靶子,将得分加3,并显示相应的提示信息。

using UnityEngine;

public class Target : MonoBehaviour
{
    //得分
    public int score = 1; 
    //是否为运动靶子
    public bool isSportsTarget;
    //靶子的位置点
    private Transform point;
    //靶子的索引
    public int indexTarget;

    private void Start()
    {
        //获取靶子的父对象作为位置点
        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()
    {
        //如果靶子的标签为Bullseye,即为靶心
        if (gameObject.tag == "Bullseye")
        {
            // 如果为运动靶子
            if (isSportsTarget)
            {
                // 将得分加3
                Tips.Instance.SetScore(10);
                Tips.Instance.SetText("在" + indexTarget + "号射击位上射中" + indexTarget + "号靶子,加10分");

            }
            else
            {
                //如果不是运动靶子,将得分加2
                Tips.Instance.SetScore(8);
                Tips.Instance.SetText("在" + indexTarget + "号射击位上射中" + indexTarget + "号靶子,加3分");
            }
        }
        // 如果靶子的标签为Circle,即为白色区域
        else if (gameObject.tag == "Circle")
        {
            //如果为运动靶子
            if (isSportsTarget)
            {
                // 将得分加2
                Tips.Instance.SetScore(5);
                Tips.Instance.SetText("在" + indexTarget + "号射击位上射中" + indexTarget + "号靶子,加5分");
            }
            else
            {
                // 如果不是运动靶子,就将得分加1
                Tips.Instance.SetScore(3);
                Tips.Instance.SetText("在" + indexTarget + "号射击位上射中" + indexTarget + "号靶子,加3分");
            }
        }
    }
}

控制靶子的移动TargetMove

1. speed: 靶子的移动速度。

2. distance: 靶子的移动距离。

3. startPosition: 靶子的起始位置。

4. direction: 靶子的移动方向,初始值为1。

5. Start(): 在脚本启动时进行初始化操作。记录靶子的起始位置。

6. Update(): 在每一帧更新时执行的函数:

       计算下一帧的位置,根据当前位置、移动速度、移动方向和时间间隔来计算。
       判断下一帧的位置与起始位置之间的距离是否超过设定的移动距离。
           如果超过移动距离,改变移动方向(乘以-1),以实现来回移动。
       更新靶子的位置为计算得到的下一帧位置。

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;
    }
}

提示和得分管理Tips

1. Instance: Tips类的静态实例,用于在其他脚本中访问Tips类的实例。

2. tips: 提示框的游戏对象。

3. tipsText: 提示文本的Text组件。

4. score: 当前的得分。

5. scoreText: 显示得分的Text组件。

6. Awake(): 在脚本被加载时执行的函数。将Tips类的实例设置为当前实例。

7. SetText(string str): 设置提示文本的函数:

       接收一个字符串参数,用于设置提示文本的内容。
       将提示文本设置为接收的字符串。
       激活提示框的游戏对象,使其可见。
8. SetScore(int score): 设置得分的函数:

       接收一个整数参数,表示要增加的得分。
       将接收的得分加到当前的得分上。
       将得分显示在得分文本中,格式为 "分数: 得分值"。

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

public class Tips : MonoBehaviour
{
    public static Tips Instance;
    public GameObject tips;
    public Text tipsText;

    private int score;
    public Text scoreText;

    private void Awake()
    {
        Instance = this;
    }

    public void SetText(string str)
    {
        tipsText.text = str;
        tips.SetActive(true);
    }

    public void SetScore(int score)
    {
        this.score += score;
        scoreText.text = "分数:" + this.score;
    }
}

天空盒切换SkyboxSwitcher

1. skybox1: 第一个天空盒的材质。

2. skybox2: 第二个天空盒的材质。

3. isSkybox1Active: 当前激活的天空盒是否为天空盒1的标志,初始值为true。

4. Update(): 在每一帧更新时执行的函数:

       检测是否按下鼠标右键(按钮编号为1)。
       如果按下右键,调用SwitchSkybox()函数切换天空盒。
5. SwitchSkybox(): 切换天空盒的函数:

       切换天空盒的状态,将isSkybox1Active的值取反。
       如果isSkybox1Active为true,将渲染设置中的天空盒材质设置为第一个天空盒材质。
       如果isSkybox1Active为false,将渲染设置中的天空盒材质设置为第二个天空盒材质。

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;
        }
    }
}

主相机(主视觉)移动控制器CameraMove

1. mouseXSensitivity: 鼠标在X轴上的灵敏度,用于控制视角旋转的速度。

2. player: 用于存储玩家对象的Transform组件。

3. xRotation: 控制视角绕X轴旋转的角度。

4. Start(): 在脚本启动时执行的函数:

       获取父级对象的Transform组件并赋值给player变量。
5. Update(): 在每一帧更新时执行的函数:

       获取鼠标在X轴和Y轴上的移动距离(输入值)并乘以鼠标灵敏度和时间间隔。
       将Y轴移动距离(mouseY)累加到xRotation上,用于控制视角绕X轴旋转。
       限制xRotation的值在-45到10之间,以限制视角的上下旋转幅度。
       将当前的xRotation应用于摄像机的本地旋转,实现视角的上下旋转。
       将X轴移动距离(mouseX)乘以玩家对象的向上方向(Vector3.up)应用于玩家对象的旋               转,实现视角的左右旋转。

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

//鼠标控制视角
public class CameraMove : MonoBehaviour
{
    //鼠标x轴灵敏度
    public float mouseXSensitivity = 25f;
    //人物
    private Transform player;
    //旋转角度
    float xRotation = 0f;

    private void Start()
    {
        player = transform.parent.transform;
    }


    // Update is called once per frame
    void Update()
    {
        float mouseX = Input.GetAxis("Mouse X") * mouseXSensitivity * Time.deltaTime;
        float mouseY = Input.GetAxis("Mouse Y") * mouseXSensitivity * Time.deltaTime;
        xRotation -= mouseY;
        //y轴最大旋转角度为正负90;
        xRotation = Mathf.Clamp(xRotation, -45f, 10f);
        transform.localRotation = Quaternion.Euler(xRotation, 0f, 0f);
        player.Rotate(Vector3.up * mouseX);
    }
}

本图文内容来源于网友网络收集整理提供,作为学习参考使用,版权属于原作者。
THE END
分享
二维码
< <上一篇
下一篇>>