目录

一、游戏介绍

(1)游戏内容设计需求

(2)游戏规则制定(玩家动作表)

二、游戏制作过程

(1)导入需要的游戏资源包

(2)地形

(3)天空盒

(4)固定靶和运动靶

(5)射击位(区域)

(6)玩家(弩弓)

1. 实现玩家的游走和视角变化控制

2. 实现弩弓蓄力动画

3. 弩弓的动画控制与发射

(7)碰撞与计分

(8)GUI

三、 游戏展示

一、游戏介绍

(1)游戏内容设计需求

        基于Unity平台制作一款第一人称射箭游戏,游戏中玩家需要移动到固定的射击区域,将弓箭射击到靶子即可获得分数。

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

(2)游戏规则制定(玩家动作表)

动作

条件

结果

按下WASD键

玩家(弩弓)进入设计好的地形

玩家(弩弓)进行前后左右的移动


时间每变化5秒

天空盒进行切换

玩家(弩弓)发生移动

玩家(弩弓)与树木、靶子发生碰撞

玩家(弩弓)停止移动

长按空格键

玩家(弩弓)在射击区域

弩弓开始蓄力,根据长按时间不断增加弩弓蓄力的强度直至最大

按下鼠标左键

玩家(弩弓)在射击区域且在该区域中的射击次数大于0

弩弓根据长按的时间所计算的强度来发射弓箭,玩家(弩弓)在该射击区域的射击次数减1

按下鼠标右键并进行方向拖拽移动


玩家(第一人称)/弓箭的视角/发射角度发生变化

弓箭射中靶子 

弓箭的碰撞检测体与靶子的碰撞检测体发生碰撞

提示射中靶子,若射中固定靶则分数加10,移动靶则分数加20

弓箭射中地面

弓箭的碰撞检测体与地形的碰撞检测体发生碰撞

提示射中地面,不加分

二、游戏制作过程

(1)导入需要的游戏资源包

Unity Asset Store - The Best Assets for Game Making中添加以上四个资源包,然后在Package Manager中下载资源包并将其import到项目资源中。

(2)地形

使用Terrain组件绘制地形:

1. 使用资源包Fantasy Skybox FREE中的地表与草地纹理绘制地形;

2. 使用资源包Dream Forest Tree中的树预制件和草预制件绘制树和草。

unity第一人称射击游戏完整教学_游戏


3. 为了让玩家不能穿过树木,需要为树预制件添加BoxCollider碰撞体组件:

unity第一人称射击游戏完整教学_System_02

绘制好的地形如下:

unity第一人称射击游戏完整教学_System_03


(3)天空盒

1. 创建几个Material文件,将Shader属性设置为Skybox-cubemap,并导入资源包Fantasy Skybox FREE中的天空盒图片。

unity第一人称射击游戏完整教学_System_04

2.编写脚本文件Skyboxswitcher,实现每5秒自动更换天空盒。 

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

public class Skyboxswitcher : MonoBehaviour
{
    // 存储轮换的天空盒
    public Material[] skys;
    // 当前天空盒数组索引
    private int index = 0;
    // 轮换时间
    private int changeTime = 5;

    void Start()
    {
        InvokeRepeating("changeBox", 0, changeTime);
    }

    void changeBox()
    {
        RenderSettings.skybox = skys[index];
        index++;
        index %= skys.Length;
    }
}

3. 将脚本文件挂载到新建的空game object "Skyboxcontroller"中,并将构建的几个天空Material文件添加到Skyboxcontroller的inspector中的skys数组中,即可实现天空盒的切换。

unity第一人称射击游戏完整教学_游戏_05

(4)固定靶和运动靶

1. 添加资源包Military target中的靶子预制体到场景中,游戏设置3个固定靶和3个移动方向不同的运动靶。

unity第一人称射击游戏完整教学_System_06

unity第一人称射击游戏完整教学_System_07

2. 设置两种靶子的tag以便后面区分:

unity第一人称射击游戏完整教学_游戏_08

unity第一人称射击游戏完整教学_游戏_09

3. 添加碰撞体组件

unity第一人称射击游戏完整教学_碰撞检测_10

unity第一人称射击游戏完整教学_碰撞检测_11

unity第一人称射击游戏完整教学_游戏_12

4. 实现运动靶的不同方向移动

游戏中添加了两个脚本实现了靶子前后,左右,上下三种方向的运动。以左右运动脚本为例,将Moved.cs挂载到靶子对象上就可以实现其左右方向的运动:

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

public class Move2 : MonoBehaviour
{
    public float moveSpeed = 5f; // 靶子的平移速度
    public float moveDistance = 5f; // 靶子平移的距离

    private Vector3 initialPosition; // 初始位置
    private bool movingRight = true; // 靶子是否向右移动

    void Start()
    {
        initialPosition = transform.position;
    }

    void Update()
    {
        MoveTarget();
    }

    void MoveTarget()
    {
        // 计算下一帧的位置
        Vector3 nextPosition = transform.position + Vector3.right * (movingRight ? 1 : -1) * moveSpeed * Time.deltaTime;

        // 判断是否达到平移的距离
        float distanceToInitial = Vector3.Distance(nextPosition, initialPosition);
        if (distanceToInitial >= moveDistance)
        {
            // 如果达到距离,改变方向
            movingRight = !movingRight;
        }

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

(5)射击位(区域)

游戏中设置了两个射击区域area1和area2,在下面的archercontroller.cs中实现了:

1. 对玩家射箭区域的限制,只有到这两个区域里面才可进行射击;

2. 对每个区域射击次数的限制,每个区域分别只有十次机会。

unity第一人称射击游戏完整教学_碰撞检测_13

(6)玩家(弩弓)

1. 实现玩家的游走和视角变化控制

创建一个Capsule的3D对象,将Main Camera移动到Player中作为子对象,再将Classical Crossbow资源包中的弩弓预制体Crossbow拖入Main Camera作为子对象。

unity第一人称射击游戏完整教学_unity第一人称射击游戏完整教学_14

unity第一人称射击游戏完整教学_unity第一人称射击游戏完整教学_15

编写脚本cameracontrolller挂载到Player对象上,即可实现玩家的游走和视角变化控制:

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

public class cameracontroller : MonoBehaviour
{
    // 在场景中游览的相机(不要给相机加碰撞器!)
    public Transform tourCamera;
    #region 相机移动参数
    public float moveSpeed = 10.0f;
    public float rotateSpeed = 150.0f;
    public float shiftRate = 2.0f;// 按住Shift加速
    public float minDistance = 0.5f;// 相机离不可穿过的表面的最小距离(小于等于0时可穿透任何表面)
    #endregion
    #region 运动速度和其每个方向的速度分量
    private Vector3 direction = Vector3.zero;
    private Vector3 speedForward;
    private Vector3 speedBack;
    private Vector3 speedLeft;
    private Vector3 speedRight;
    private Vector3 speedUp;
    private Vector3 speedDown;
    #endregion
    void Start()
    {
        if (tourCamera == null) tourCamera = gameObject.transform;
    }
    void Update()
    {
        GetDirection();
        // 检测是否离不可穿透表面过近
        RaycastHit hit;
        while (Physics.Raycast(tourCamera.position, direction, out hit, minDistance))
        {
            // 消去垂直于不可穿透表面的运动速度分量
            float angel = Vector3.Angle(direction, hit.normal);
            float magnitude = Vector3.Magnitude(direction) * Mathf.Cos(Mathf.Deg2Rad * (180 - angel));
            direction += hit.normal * magnitude;
        }
        if (tourCamera.localPosition.y > 3.3f)
        {
            tourCamera.localPosition = new Vector3(tourCamera.localPosition.x, 3.3f, tourCamera.localPosition.z);
        }
        if (tourCamera.localPosition.y < 2.8f)
        {
            tourCamera.localPosition = new Vector3(tourCamera.localPosition.x, 2.8f, tourCamera.localPosition.z);
        }
        tourCamera.Translate(direction * moveSpeed * Time.deltaTime, Space.World);
    }
    private void GetDirection()
    {
        #region 加速移动
        if (Input.GetKeyDown(KeyCode.LeftShift)) moveSpeed *= shiftRate;
        if (Input.GetKeyUp(KeyCode.LeftShift)) moveSpeed /= shiftRate;
        #endregion
        #region 键盘移动
        // 复位
        speedForward = Vector3.zero;
        speedBack = Vector3.zero;
        speedLeft = Vector3.zero;
        speedRight = Vector3.zero;
        speedUp = Vector3.zero;
        speedDown = Vector3.zero;
        // 获取按键输入
        if (Input.GetKey(KeyCode.W)) speedForward = tourCamera.forward;
        if (Input.GetKey(KeyCode.S)) speedBack = -tourCamera.forward;
        if (Input.GetKey(KeyCode.A)) speedLeft = -tourCamera.right;
        if (Input.GetKey(KeyCode.D)) speedRight = tourCamera.right;
        if (Input.GetKey(KeyCode.E)) speedUp = Vector3.up;
        if (Input.GetKey(KeyCode.Q)) speedDown = Vector3.down;
        direction = speedForward + speedBack + speedLeft + speedRight + speedUp + speedDown;
        #endregion
        #region 鼠标旋转
        if (Input.GetMouseButton(1))
        {
            // 转相机朝向
            tourCamera.RotateAround(tourCamera.position, Vector3.up, Input.GetAxis("Mouse X") * rotateSpeed * Time.deltaTime);
            tourCamera.RotateAround(tourCamera.position, tourCamera.right, -Input.GetAxis("Mouse Y") * rotateSpeed * Time.deltaTime);
            // 转运动速度方向
            direction = V3RotateAround(direction, Vector3.up, Input.GetAxis("Mouse X") * rotateSpeed * Time.deltaTime);
            direction = V3RotateAround(direction, tourCamera.right, -Input.GetAxis("Mouse Y") * rotateSpeed * Time.deltaTime);
        }
        #endregion
    }
    /// <summary>
    /// 计算一个Vector3绕旋转中心旋转指定角度后所得到的向量。
    /// </summary>
    /// <param name="source">旋转前的源Vector3</param>
    /// <param name="axis">旋转轴</param>
    /// <param name="angle">旋转角度</param>
    /// <returns>旋转后得到的新Vector3</returns>
    public Vector3 V3RotateAround(Vector3 source, Vector3 axis, float angle)
    {
        Quaternion q = Quaternion.AngleAxis(angle, axis);// 旋转系数
        return q * source;// 返回目标点
    }
}

2. 实现弩弓蓄力动画

Animator动画控制器的转移顺序如下,在Empty->half的转移过程添加bool条件“isPulling”,在half->shoot的转移过程添加bool条件“Fire” 。

unity第一人称射击游戏完整教学_System_16

其中half为混合树,Empty是没蓄力状态的动作,Fill是蓄力拉满的动作,

unity第一人称射击游戏完整教学_unity第一人称射击游戏完整教学_17

3. 弩弓的动画控制与发射

archercontroller.cs代码如下:

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

public class archercontroller : MonoBehaviour
{
    Animator animator;
    bool isPulling = false;
    float pullDuration = 0f;
    public float maxPullDuration = 2f; // 最长拉弓时间
    public float arrowSpeed = 1f; // 箭矢速度
    float pullStrength;
    public Transform firePoint;
    public Camera maincam;
    public int area1Shots = 10;
    public int area2Shots = 10;

    void Start()
    {
        // 获取弓上的Animator组件
        animator = GetComponent<Animator>();
    }

    void Update()
    {
        if (Isinarea())
        {
            // 当按下空格键时触发状态切换
            if (Input.GetKeyDown(KeyCode.Space))
            {
                pullDuration = 0;
                animator.SetBool("Fire", false);
                animator.SetFloat("power", 0);
                isPulling = true;
                animator.SetBool("isPulling", true);
                //animator.SetBool("Holding", false);
            }

            // 持续按下空格键时记录按下时间,决定拉弓的强度
            if (isPulling)
            {
                pullDuration += Time.deltaTime;
                // 将按下的时间映射到0到1的范围,作为拉弓强度的参数
                pullStrength = Mathf.Clamp01(pullDuration / maxPullDuration);
                animator.SetFloat("power", pullStrength);
            }


            if (Input.GetKeyUp(KeyCode.Space))
            {

                isPulling = false;
                //animator.SetBool("Holding", true);
                animator.SetBool("isPulling", false);

            }
            // 当点击鼠标左键时触发射击
            if (Input.GetMouseButtonDown(0))
            {
                Vector3 currentPosition = transform.position;
                Vector3 area1 = new Vector3(269.1f, 0f, 279f);
                Vector3 area2 = new Vector3(219.3f, 0f, 274.7f);
                float distance1 = Vector3.Distance(currentPosition, area1); // 计算目标点和玩家位置之间的距离
                float distance2 = Vector3.Distance(currentPosition, area2);
                float radius = 10f; // 圆的半径
                if (distance1 <= radius&&area1Shots>0)
                {
                    area1Shots--;
                    animator.SetBool("Fire", true);
                    animator.SetFloat("power", 0);
                    // animator.SetBool("Holding", false);
                    Fire(pullStrength);
                } 
                else if (distance2 <= radius && area2Shots > 0)
                {
                    area2Shots--;
                    animator.SetBool("Fire", true);
                    animator.SetFloat("power", 0);
                    // animator.SetBool("Holding", false);
                    Fire(pullStrength);
                }

            }
        }
    }
    public void Fire(float holdForce)
    {
        GameObject arrow = Instantiate<GameObject>(Resources.Load<GameObject>("Prefabs/Arrow"));
        arrow.AddComponent<ArrowController>();
        ArrowController arrowController = arrow.GetComponent<ArrowController>();
        arrowController.cam = maincam;
        arrow.transform.position = firePoint.transform.position;
        arrow.transform.rotation = Quaternion.LookRotation(this.transform.forward);
        Rigidbody rd = arrow.GetComponent<Rigidbody>();
        if (rd != null)
        {
            rd.AddForce(this.transform.forward * 60 * holdForce);
        }
    }
    bool Isinarea()
    {
        Vector3 currentPosition = transform.position;
        Vector3 area1 = new Vector3(269.1f, 0f, 279f);
        Vector3 area2 = new Vector3(219.3f, 0f, 274.7f);
        float distance1 = Vector3.Distance(currentPosition, area1); // 计算目标点和玩家位置之间的距离
        float distance2 = Vector3.Distance(currentPosition, area2);
        float radius = 10f; // 圆的半径

        if (distance1 <= radius || distance2 <= radius)
        {
            return true;
        }
        else
        {
            return false;
        }
    }

}

将该脚本挂载在Crossbow上,将Main Camera添加到脚本,并添加一个弓箭发射点的位置对象pos到Fire Point上:

unity第一人称射击游戏完整教学_System_18

(7)碰撞与计分

1. 给弓箭添加Capsule collider碰撞体和刚体属性:

unity第一人称射击游戏完整教学_碰撞检测_19

2. 碰撞处理与计分

检测碰撞,如果碰到静态靶,分数+10,如果碰到动态靶,分数+20,同时生成UI提示射中,销毁箭。

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

public class ArrowController : MonoBehaviour
{
    // Start is called before the first frame update
    private Rigidbody rb;
    public float Score = 0;

    public Camera cam;
    private UserGui ui;
    public float hitDisplayTime = 2f; // 提示显示时间
    public archercontroller archerControllerScript;
    void Start()
    {
        rb = GetComponent<Rigidbody>();
        ui = cam.GetComponent<UserGui>();
        archerControllerScript = GameObject.FindObjectOfType<archercontroller>();
    }

    // Update is called once per frame
    void Update()
    {

    }
    private void OnCollisionEnter(Collision collision)
    {
        if (collision.gameObject.CompareTag("staticTarget"))
        {
            ShowHitMessage("Hit Static Target! 加十分!");
            rb.velocity = Vector3.zero;
            rb.angularVelocity = Vector3.zero;
            ui.Score += 10;
            Destroy(gameObject);
        }
        else if (collision.gameObject.CompareTag("MovingTarget"))
        {
            ShowHitMessage("Hit Moving Target! 加二十分!");
            rb.velocity = Vector3.zero;
            rb.angularVelocity = Vector3.zero;
            ui.Score += 20;
            Destroy(gameObject);
        }
        else if (collision.gameObject.CompareTag("Ground"))
        {
            ShowHitMessage("Hit the Ground!");
            rb.velocity = Vector3.zero;
            rb.angularVelocity = Vector3.zero;
            Destroy(gameObject);
        }
    }
    void ShowHitMessage(string message)
    {
        StartCoroutine(DisplayHitMessage(message));
    }

    IEnumerator DisplayHitMessage(string message)
    {
        ui.SetHitMessage(message); // 假设 UserGui 类有 SetHitMessage 方法用于设置提示信息
        yield return new WaitForSeconds(hitDisplayTime);
        ui.ClearHitMessage(); // 假设 UserGui 类有 ClearHitMessage 方法用于清除提示信息
    }
}

(8)GUI

UserGUI:显示各个提示信息(射击次数,是否射中和分数),该代码挂载到摄像机上。

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

public class UserGui : MonoBehaviour
{
    // Start is called before the first frame update
    public float Score = 0;
    public archercontroller archerControllerScript;
    private string hitMessage = "";
    void Start()
    {
        archerControllerScript = GameObject.FindObjectOfType<archercontroller>();
    }

    // Update is called once per frame
    void Update()
    {

    }
    private void OnGUI()
    {
        GUIStyle style = new GUIStyle();
        style.fontSize = 14;
        style.normal.textColor = Color.black;

        // 定义游戏介绍文本内容
        string introText = "移动位置请按WASD\n蓄力拉弓请按空格\n发射弓箭请按左键\n调整发射角度/视角请按右键";

        // 在屏幕左上角绘制游戏介绍文本
        GUI.Label(new Rect(10, 10, 300, 100), introText, style);
        style.fontSize = 24;
        style.normal.textColor = Color.red;
        // 在屏幕右上角显示分数
        GUI.Label(new Rect(Screen.width - 150, 20, 150, 30), "Score: " + Score, style);
        // 显示射击次数
        style.fontSize = 18;
        GUI.Label(new Rect(Screen.width - 250, Screen.height - 20, 250, 30), "每个射击区域有十次射击机会 ",style);
        GUI.Label(new Rect(Screen.width - 150, Screen.height - 50, 150, 30), "Area 1 Shots: " + archerControllerScript.area1Shots,style);
        GUI.Label(new Rect(Screen.width - 150, Screen.height - 80, 150, 30), "Area 2 Shots: " + archerControllerScript.area2Shots,style);

        GUI.Label(new Rect( 50, Screen.height - 50, 150, 30), hitMessage, style);
    }
    public void SetHitMessage(string message)
    {
        hitMessage = message;
    }

    public void ClearHitMessage()
    {
        hitMessage = "";
    }
}

三、 游戏展示

视频地址:Unity3D 第一人称射箭游戏_哔哩哔哩_bilibili