unity小游戏 弓弩游戏

背景介绍

这是unity课程的一次作业,目的是加深对动画、碰撞功能的理解。

要求

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

本游戏完成了以上所有要求,并且在命中时有反馈,在发射时有镜头抖动效果,提升游玩体验。以下内容对完成的逻辑进行讲解。

视频展示

Unity 小游戏 弓箭手大作战

具体介绍

天空盒

本游戏天空盒采用Fantasy Skybox FREE的包,每10秒自动进行切换。

using System.Collections;
using UnityEngine;

namespace Assets.script
{
    public class Skybox : MonoBehaviour
    {
        public Material[] skyboxMaterials;
        void Start()
        {
            StartCoroutine(ChangeSkybox());
        }

        IEnumerator ChangeSkybox()
        {
            while (true)
            {
                // 随机选择一个天空盒子材质
                int index = Random.Range(0, skyboxMaterials.Length);
                RenderSettings.skybox = skyboxMaterials[index];

                // 等待一段时间后再切换天空盒子
                yield return new WaitForSeconds(10);
            }
        }
    }
}

将上述代码挂载在main camera中即可完成。

靶子

靶子采用Training_dummy包中的预设,如下图

预设自带受击动画,方便后续箭射中时进行反馈。

每个靶子挂载一个脚本,用于受击动画。

using System.Collections;
using UnityEngine;

namespace Assets.script
{
    public class targetControl : MonoBehaviour
    {
        private Animator ani;


        // Use this for initialization
        void Start()
        {
            ani = GetComponent<Animator>();
        }

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

        }

        private void OnCollisionEnter(Collision collision)
        {
            if (collision.gameObject.CompareTag("arrow"))
            {
                ani.Play("pushed");
            }
        }
    }
}

射击位

在地图上设置射击位,通过Mathf.Sqrt()函数计算当前位置与射击位中心的距离,小于半径即可进行设计。

相关代码在后面的crossBowControl中

驽弓动画

创建动画控制器及相关控制参数

当前状态下一状态触发变量
Emptypullstart
pullhold_nhold
hold_nshootshoot

通过代码控制相关逻辑

using System.Collections;
using UnityEngine;

namespace Assets.script
{
    public class crossbowControl : MonoBehaviour
    {
        private float lastInputTime; // 记录上一次接收鼠标输入的时间
        public float inputInterval = 1.5f; // 两次鼠标输入之间的最小时间间隔
        public GameObject arrowPrefab; // 弓箭预设
        public CameraShake cameraShake;
        public Transform firePoint;
        public float arrowSpeed = 10.0f;
        private Animator ani;
        public float power = 0.0f;
        private AnimatorStateInfo aniState;
        public Camera Camera;
        private UserUI ui;
        private bool isInplane;
        private bool first; // 重新进入射击点
        //plane1
        private float p1x = 710f;
        private float p1z = 336f;
        //plane2
        private float p2x = 748f;
        private float p2z = 305f;
        // 半径大小
        private float range = 12.5f;

        // Use this for initialization
        void Start()
        {
            ani = GetComponent<Animator>();
            ui = Camera.GetComponent<UserUI>();
        }

        // Update is called once per frame
        void Update()
        {
            // 判断是否处于射击点
            Vector3 curPos = Camera.transform.position;
            float horizontalDistance1 = Mathf.Sqrt(Mathf.Pow(curPos.x - p1x, 2) + Mathf.Pow(curPos.z - p1z, 2));
            float horizontalDistance2 = Mathf.Sqrt(Mathf.Pow(curPos.x - p2x, 2) + Mathf.Pow(curPos.z - p2z, 2));
            if(horizontalDistance1 <= range)
            {
                ui.plane1 = true;
                isInplane = true;
                newInPlane();
            }
            else if (horizontalDistance2 <= range)
            {
                ui.plane2 = true;
                isInplane = true;
                newInPlane();
            }
            else
            {
                ui.plane1 = ui.plane2 = false;
                isInplane = false;
                newInPlane();
            }
            if (Input.GetKey(KeyCode.T))
            {
                ui.tips = true;
            }
            if (Input.GetKey(KeyCode.Q))
            {
                ui.tips = false;
            }
            // 处在射击点
            if (isInplane && ui.arrow_num > 0)
            {
                if (Time.time - lastInputTime > inputInterval)
                {
                    if (Input.GetMouseButtonDown(2))
                    {
                        aniState = ani.GetCurrentAnimatorStateInfo(0);
                        if (aniState.IsName("hold_n"))
                        { //重新蓄力,直接跳转回Empty状态
                            ani.Play("Empty");
                        }
                        StartCoroutine("CheckForLongPress");

                    }
                    if (Input.GetMouseButtonUp(2))
                    {
                        StopCoroutine("CheckForLongPress");
                        ani.SetFloat("hold_power", power);
                        ani.SetTrigger("hold");
                    }
                    if (Input.GetMouseButtonDown(0))
                    {
                        if (power != 0.0f) // 有力量,当前处于hold状态
                        {
                            StartCoroutine(cameraShake.Shake(.02f, .1f));
                            ui.arrow_num--;
                            shootArrow(power);
                            ani.SetTrigger("shoot");
                            power = 0.0f;
                            ani.SetFloat("hold_power", 0.0f);
                            ani.SetFloat("power", 0.0f);
                        }
                    }
                }
            
            }

        }

        IEnumerator CheckForLongPress() //长按蓄力
        {
            ani.SetTrigger("start");
            power = 0;
            float longPressDuration = 2.0f; // 长按的时间阈值,以秒为单位

            float timer = 0.0f;

            while (timer < longPressDuration)
            {
                timer += Time.deltaTime;

                power += 0.01f;
                if(power >= 1)
                {
                    power = 1;
                }
                ani.SetFloat("power", power);

                yield return null;
            }

            // 长按时的操作
            Debug.Log("Long press detected!");
        }

        void shootArrow(float power)
        {

            if (arrowPrefab != null)
            {
                GameObject arrow = Instantiate<GameObject>(Resources.Load<GameObject>("prefabs/Arrow"));
                arrow.AddComponent<ArrowControl>();
                arrow.gameObject.tag = "arrow";
                ArrowControl arrowControl = arrow.GetComponent<ArrowControl>();
                arrowControl.cam = Camera;
                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 * 100 * power);
                }
            }
        }

        void newInPlane()
        {
            // 重新进入
            if (first)
            {
                first = false;
                ui.arrow_num = 6;
                ui.score = 0;
            }
            // 不在射击点
            if (!first && !isInplane)
            {
                first= true;
            }
        }
    }
}

游走

代码实现

using UnityEngine;
public class TourCamera : MonoBehaviour
{
    // 在场景中游览的相机(不要给相机加碰撞器!)
    public Transform tourCamera;
    #region 相机移动参数
    public float moveSpeed = 10.0f;
    public float rotateSpeed = 300.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;
        // 防止相机边缘穿透
        //if (tourCamera.GetComponent<Camera>().nearClipPlane > minDistance / 3)
        //{
        //    tourCamera.GetComponent<Camera>().nearClipPlane /= 3;
        //}
    }
    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;// 返回目标点
    }
}

碰撞与计分

提示,分数显示等在UserUI中进行

using System.Collections;
using UnityEngine;

namespace Assets.script
{
    public class UserUI : MonoBehaviour
    {
        public int arrow_num;
        public int score;
        public bool plane1, plane2;
        public bool tips = false;

        // Use this for initialization
        void Start()
        {

        }

        // Update is called once per frame
        void OnGUI()
        {
            GUIStyle style = new GUIStyle();
            style.normal.textColor = Color.red;
            style.fontSize = 20;
            GUI.Label(new Rect(10, 10, 200, 200), "Score:"+score,style);
            if(plane1)
            {
                GUI.Label(new Rect(300, 10, 200, 200), "当前处于[ 第一射击点 ]\n剩余箭矢数  " + arrow_num + " / 6", style);
            }
            else if(plane2)
            {
                GUI.Label(new Rect(300, 10, 200, 200), "当前处于[ 第二射击点 ]\n剩余箭矢数  " + arrow_num + " / 6", style);
            }
            else
            {
                GUI.Label(new Rect(300, 10, 200, 200), "当前未处于射击点,无法射击", style);
            }
            if(tips)
            {
                GUI.Label(new Rect(600, 10, 200, 200), "按住鼠标右键进行移动\n鼠标中键蓄力\n鼠标左键发射\nq键关闭帮助", style);
            }
            else
            {
                GUI.Label(new Rect(600, 10, 200, 200), "按 t 键查看帮助", style);
            }
            
        }
    }
}

在arrowControl中加入碰撞检测,并计算分数

using System.Collections;
using UnityEngine;

namespace Assets.script
{
    public class ArrowControl : MonoBehaviour
    {
        private Rigidbody rb;
        public Camera cam;
        private UserUI ui;

        // Use this for initialization
        void Start()
        {
            rb = GetComponent<Rigidbody>();
        }

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

        }

        private void OnCollisionEnter(Collision collision)
        {
            ui = cam.GetComponent<UserUI>();
            rb.velocity = Vector3.zero;
            rb.angularVelocity = Vector3.zero;
            if (collision.gameObject.name.Equals("target_7") || collision.gameObject.name.Equals("target_72"))
            {
                ui.score += 10;
            }
            else if (collision.gameObject.name.Equals("target_5"))
            {
                ui.score += 20;
            }
            else if (collision.gameObject.name.Equals("target_10"))
            {
                ui.score += 5;
            }
            else if (collision.gameObject.name.Equals("moveTarget_7"))
            {
                ui.score += 20;
            }
            else if (collision.gameObject.name.Equals("moveTarget_8"))
            {
                ui.score += 30;
            }
            Destroy(gameObject);
        }
    }
}

摄像头抖动效果

 为了更有代入感,在射击时加入镜头抖动效果,将脚本挂载在main camera下,发射时调用shake函数即可

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

public class CameraShake : MonoBehaviour
{
    public IEnumerator Shake(float duration, float magnitude)//摇晃时间、幅度
    {
        Vector3 originalPos = transform.localPosition;//相机原始位置

        float elapsed = 0.0f;//摇晃进行时间
        while (elapsed < duration)
        {
            float x = Random.Range(-2f, 2f) * magnitude;//x轴随机抖动幅度
            float y = Random.Range(-2f, 2f) * magnitude;//y轴随机抖动幅度

            transform.localPosition = new Vector3(x, y, originalPos.z);

            elapsed += Time.deltaTime;

            yield return null;
        }
        transform.localPosition = originalPos;//再次复原
    }
}

参考链接

感谢以下相关博客,在游戏编写过程中给予了莫大帮助。

https://blog.csdn.net/qq_17367039/article/details/104645469
https://blog.csdn.net/h_phoe/article/details/100987949
https://tieba.baidu.com/p/5311609737
https://zhidao.baidu.com/question/652893391159855445.html
https://zhuanlan.zhihu.com/p/142708182
https://blog.csdn.net/qq_62799812/article/details/134513431
https://blog.csdn.net/m0_55373754/article/details/130977672
https://blog.csdn.net/qq_51701007/article/details/126502171
https://zhuanlan.zhihu.com/p/641149968
https://blog.csdn.net/weixin_64218808/article/details

  • 28
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值