Unity_第一人称射箭游戏

 一、游戏要求

  • 游戏场景(14分)
    •  地形(2分):使用地形组件,上面有山、路、草、树;(可使用第三方资源改造)
    •  天空盒(2分):使用天空盒,天空可随 玩家位置 或 时间变化 或 按特定按键切换天空盒
    •  固定靶(2分):使用静态物体,有一个以上固定的靶标;(注:射中后状态不会变化)
    •  运动靶(2分):使用动画运动,有一个以上运动靶标,运动轨迹,速度使用动画控制;(注:射中后需要有效果或自然落下)
    •  射击位(2分):地图上应标记若干射击位,仅在射击位附近或区域可以拉弓射击,每个位置有 n 次机会;
    •  摄像机(2分):使用多摄像机,制作 鸟瞰图 或 瞄准镜图 使得游戏更加易于操控;
    •  声音(2分):使用声音组件,播放背景音 与 箭射出的声效;
  • 运动与物理与动画(8分)
    •  游走(2分):使用第一人称组件,玩家的驽弓可在地图上游走,不能碰上树和靶标等障碍;(注:建议使用 unity 官方案例
    •  射击效果(2分):使用 物理引擎 或 动画 或 粒子,运动靶被射中后产生适当效果。
    •  碰撞与计分(2分):使用 计分类 管理规则,在射击位射中靶标得相应分数,规则自定;(注:应具有现场修改游戏规则能力)
    •  驽弓动画(2分):使用 动画机 与 动画融合, 实现十字驽蓄力半拉弓,然后 hold,择机 shoot

        接下来会以这些要求来说明游戏的构成。

        游戏游玩视频:Unity_第一人称射击游戏演示_哔哩哔哩_bilibili

        游戏源代码:ShootingGame: 使用unity实现第一人称射箭游戏

        ps:这里的源代码只包含了脚本内容,其他的资源等由于空间太大而无法上传到Gitee,如果有需要完整内容的,可以私聊我。

二、具体说明:

        1.        地形:使用地形盒简单构造一个地形即可,在资源商店有很多免费好用的资源,这里使用的资源是"Fantasy Forest Environment Free Sample" "Polytope Studio"

        2.        天空盒:这里实现了按下数字1~5可以切换天空盒,天空盒使用的资源是"SkySeries Freebie"。具体代码如下,创建了public 的Material[]后,在unity界面把需要的天空盒拖入,然后将整个脚本挂载到一个空对象即可。

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

public class ChangeSkybox : MonoBehaviour
{
    public Material[] skyboxes; // 存储所有天空盒材质
    private int currentSkyboxIndex = 0; // 当前天空盒的索引
    void Start()
    {

    }

    // Update is called once per frame
    void Update()
    {
        // 检测按下数字键1
        if (Input.GetKeyDown(KeyCode.Alpha1))
        {
            currentSkyboxIndex = 0;
            RenderSettings.skybox = skyboxes[currentSkyboxIndex];
        }
        // 检测按下数字键2
        else if (Input.GetKeyDown(KeyCode.Alpha2))
        {
            currentSkyboxIndex = 1;
            RenderSettings.skybox = skyboxes[currentSkyboxIndex];
        }
        // 检测按下数字键3
        else if (Input.GetKeyDown(KeyCode.Alpha3))
        {
            currentSkyboxIndex = 2;
            RenderSettings.skybox = skyboxes[currentSkyboxIndex];
        }
        // 检测按下数字键4
        else if (Input.GetKeyDown(KeyCode.Alpha4))
        {
            currentSkyboxIndex = 3;
            RenderSettings.skybox = skyboxes[currentSkyboxIndex];
        }
        else if (Input.GetKeyDown(KeyCode.Alpha5))
        {
            currentSkyboxIndex = 4;
            RenderSettings.skybox = skyboxes[currentSkyboxIndex];
        }
    }
}

        ​​​

        3.        固定靶:这个比较简单,只需要在资源商店找到一个标靶的资源(这里使用的是"Military target"),然后检查其有没有“... Colider”组件(用于检测碰撞),然后调整大小放到心仪的位置即可即可。

        4.        运动靶:这个只需要在固定靶的基础上,加上动画(Animator组件)即可。 

                加上后在动画控制器里添加向左移动和向右移动两个动画循环播放即可实现靶子左右移动。

        5.        射击位:通过检测玩家方圆一定范围内是否存在靶子来判断是否能够射击

                   射击的实现:当其位于“Shoot”的动画状态时,将弓弩上的箭设为失效,然后再这个箭的位置生成一个Aroow预制体向前射出,然后射出的方向跟随摄像头的方式即可。当需要装填箭矢时再把箭重新激活即可。

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

public class Contorller : MonoBehaviour
{
    public GameObject arrowPrefab;
    public Transform arrow_shoot_rotate;
    public float speed = 500f;  // 箭速度
    // Start is called before the first frame update
    void Start()
    {
        
    }

    // Update is called once per frame
    void Update()
    {
        // 获取父对象的Animator组件
        Animator parentAnimator = GetComponentInParent<Animator>();
        AnimatorStateInfo stateInfo = parentAnimator.GetCurrentAnimatorStateInfo(0);
         // 获取shoot的状态
        bool shootState = parentAnimator.GetBool("Shoot");
        if (stateInfo.IsName("Shoot")){
            gameObject.SetActive(false);
            // 生成箭
            GameObject arrow = Instantiate(arrowPrefab, transform.position,arrow_shoot_rotate.rotation);  //生成预制体
            // 获取Rigidbody组件
            Rigidbody rb = arrow.GetComponent<Rigidbody>();
            
            // 设置速度
            rb.velocity = arrow.transform.forward * speed;
        }
        // 获取Fill的状态
        bool fillState = parentAnimator.GetBool("Fill");
        if (fillState){
            Debug.Log("Fill");
            gameObject.SetActive(true);
        }
    }
}

        6.        摄像机:这里实现了两种摄像机位置,一个是普通的第一人称视角,一个是按下右键后会有的瞄准视角。

                普通视角:

                右键射击视角:

                具体实现:提前准备好两个视角,然后再脚本中检测右键按下后,一个视角失效,一个视角激活即可。这里视角是如何准备的会放到后面的“游走”部分说明。同时两个视角的准心会不一样,只需要准备两个准心(UI的图片)即可。这里准心使用的资源是:“OccaSoftware”

// 检测鼠标右键是否被按下
        if (Input.GetMouseButtonDown(1) && isAim == false)
        {
            // 如果按下,则隐藏准心
            Aim.SetActive(false);
            MainCamera.SetActive(false);
            Aim2.SetActive(true);
            AimCamera.SetActive(true);
            isAim = true;
        }
        else if (Input.GetMouseButtonDown(1) && isAim == true)
        {
            // 如果按下,则隐藏准心
            Aim.SetActive(true);
            MainCamera.SetActive(true);
            Aim2.SetActive(false);
            AimCamera.SetActive(false);
            isAim = false;
        }

        7.        声音:这里添加了背景音乐和射箭时的音效。背景音乐比较简单,直接在一个场景内找一个不会失效的对象或者新建一个空对象然后添加一个“Audio Source”属性,然后把心仪的音乐添加即可,我这里是新建了一个空对象“Music”去实现的。对于射箭的音效,实现原理是在射箭时把音效打开即可。我这里的音效是挂载到Arrow预制体中的。

                

        8.        游走:这里使用了第一人称组件"StarterAssets",这个资源里面有个"FirstPersonController"文件夹,里面的"Prefabs"里面有四个预制体,我们只需要把"MainCamera","PlayerCapsule"和"PlayerFollowCamera"三个预制体拖出来使用,然后要把弓弩的预制体拖到MainCamera里,作为其子对象,然后稍微调整一下弓弩的位置即可。这里的弓弩使用的资源是“RyuGiken”

        这个第一人称组件已经帮你实现了上下左右移动、跳跃和加速,强烈建议使用这个第一人称组件!!(主要是自己写还挺困难的,我就是刚开始自己写,写出来的效果很糟糕)

       

        这里还要注意:弓弩和"PlayerCapsule"不要放在同一个图层,不然到后面射击的时候弓箭就会和这个组件发生碰撞。

        我这里一共有8个图层,其中3,6,7,8是我新建的。

                3:        用于存放靶子,来检测箭射中的是靶子而不是其他东西

                6:        用于存储射出的并发生碰撞的箭,避免射出的箭还能与人物发生碰撞

                7:        用于存放"PlayerCapsule"组件

                8:        用于存放弓弩

                

        这里还注意到图片底下还有一个"AimCamera",这个就是复制"MainCamera"改个名字然后调整一下弓弩的位置从而实现两个视角的准备的。

        9.        射击效果:这里我运气很好,碰巧选到一个已经有拖尾效果的弓弩资源,所以这里只需要实现碰撞后产生的粒子效果即可。

                   由于射击的逻辑是:产生一个新的Arrow游戏对象然后向前射出,所以这里的粒子效果要加到Arrow预制体中。双击Arrow预制体即可看到预制体中包含的内容:

                

                这里前三个是他原本就有的,我们不用管他,只需要在里面右键,新建一个粒子系统即可,这里"HitEffect"就是我的粒子系统,这里我的粒子系统做的比较简单,只是一个很基础的粒子实现而已。

                

        10.        碰撞与记分:这里的碰撞检测是挂载到射出的Arrow预制体中,这个OnCollisionEnter()函数会在物体发生碰撞的时候自动调用,所以不需要放到Update()或Start()函数里。

        这里实现的效果是:发生碰撞后:

                1.        启动粒子效果

                2.        箭的位置固定在发生碰撞的位置

                3.        分数加一

                4.        将发生碰撞的箭放到一个专门存储无用的箭的图层

                5.        将发生碰撞的箭与发生碰撞的物体形成父子关系,为了让箭能跟随运动靶的移动而移动

                6.        在发生碰撞后的15s后将对象销毁,避免产生的对象过多而导致占用资源

void OnCollisionEnter(Collision collision)
    {
        // 激活"HitEffect"粒子效果
        hitEffect.SetActive(true);
        rb = GetComponent<Rigidbody>();
        rb.velocity = Vector3.zero; // 停止移动
        rb.angularVelocity = Vector3.zero; // 停止旋转
        rb.position = rb.position; // 保持位置不变
        rb.useGravity = false; // 禁用重力
        gameObject.layer = 6; 
        if (collision.gameObject.layer == 3){
            //Debug.Log("Arrow hit the target");
            // 分数++
            StartUI.AddScore();
        }

        // 获取发生碰撞的对象
        GameObject collidedObject = collision.gameObject;
        // 打印碰撞对象的名称
        //Debug.Log("碰撞的对象是:" + collidedObject.name);
        transform.parent = collidedObject.transform;

        // 在15秒后销毁当前对象
        Invoke("DestroyObject", 15f);
    }

        11.        弓弩动画:这里我运气很好,选到的弓弩资源里刚好有射击的动画预制体,我只需要将这几个动画的运作逻辑稍作修改即可。所以强烈建议使用“RyuGiken”的弓弩资源!!!                                弓弩的动画控制器:   

                      我把它原本的运作逻辑全部删除了,然后自己构建了一个逻辑。

                       左边参数部分有两个“Shoot”和“Fill”,这两个都是Trigger,代码如下:

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

public class CrossbowController : MonoBehaviour
{
    public GameObject arrow;

    // Start is called before the first frame update
    void Start()
    {
        
    }

    // Update is called once per frame
    void Update()
    {
        // 获取Animator组件
        Animator animator = GetComponentInParent<Animator>();
        // 获取当前动画状态信息
        AnimatorStateInfo stateInfo = animator.GetCurrentAnimatorStateInfo(0);
        
        if(stateInfo.IsName("Hold")){
            if(Input.GetKeyDown(KeyCode.F)){
                //发射箭矢
                //Debug.Log("发射箭矢");
                animator.SetTrigger("Shoot");
            } 
        }
        if(stateInfo.IsName("Empty")){
            if(Input.GetKeyDown(KeyCode.R)){
                //Debug.Log("装载箭矢");
                arrow.SetActive(true);
                animator.SetTrigger("Fill");
            }
        }
    }
}

                        这里实现的是:当此时的状态为“Hold”时,按下F才可射击;当此时状态为“Empty”时,按下R才可以装填箭矢。不然就会出现“预制菜”的效果:即当你没处于Hold时按下F,当其动画播放到Hold时就会自动射击。这个脚本时挂载到Crossbow里的Armature里的,其实直接放到Corssbow里也可以,只需要把获取动画组件里的GetComponentInParent<Animator>();改成:GetComponent<Animator>();即可。

三、不足之处

        在实现两个视角的时候,实现的方法是建了两个相同的弓弩对象,只是这两个对象的摄像机位置不同,但是这就会导致两个不同视角的动作是相互独立的。也就会导致一个视角射出箭后,切换另一个视角后也不需要重新装载即可再射出一支箭,相当于拿了两把弓弩。

        这里的玩法可以更加多样,我这里只是实现了一个很基础的射箭游戏,鼓励各位进行二创。