【Unity】第三人称射击游戏开发过程之瞄准状态设计(TPS.S.P1)

2023-10-30


这篇文章开始,我将逐渐的整理并开发第三人称射击游戏。在项目到达稳定之前,这个系列的任何一篇文章的任何一个部分都有可能改变。这也就意味着本系列文章仅作为自己的笔记。文章内容可以提供制作游戏的思路,但尚且存在可能的优化。

设计整体的结构与模式

以下是我的设计思路:
角色有两件武器,同时会存在无武器状态。武器可以在进入游戏前进行更换。手雷暂时不做设计。
枪械可以存在一个基类,然后不同的武器类型进行继承。不过是否这样我仍然在思考,优先使用一种类抽象出不同的枪械类型。

瞄准状态的设计

这个可以看看代码猴使用第三人称开发的视频,我将使用类似的思想进行设计。
我们做另一个渐进的VCamera。用代码控制这个的激活。每当按下瞄准,会将其激活,会自动进入瞄准的VM。在瞄准状态下,我们限制移动速度只能为行走,不允许跑步,并且移动将成为始终朝向瞄准方向下的前后左右。

瞄准相机

我们将之前的相机复制出来,重新命名为瞄准相机。调整FOV以及distance和offset,确定一个合适的相机大小与角度。这是我设置的一个机位。
在这里插入图片描述
我们在代码中引入Cinemachine的命名空间,将这个相机加进来。然后做一个DoAim的方法,将这个组件的游戏对象与输入系统中的aim属性同步。
当然,在动画状态机中也会设置相应的部分,做一个bool值的更新。

public CinemachineVirtualCamera _aimCamera;

    private void DoAim()
    {
        _aimCamera.gameObject.SetActive(_isAim);
        _anim.SetBool("Aiming", _isAim);
    }

在测试前,请确保在本系列第一篇中的Input系统中有做好映射,我们的逻辑一直都是input监测输入,然后Controller检测输入值。做好对应关系,就可以测试了。

瞄准状态

瞄准时的鼠标移动速度限制

我们创建一个float值表示速度倍率,这个值将与普通速度相乘,平时为1f,瞄准状态下的值会决定当前鼠标速度是普通状态下的几倍。对应的,创建一些基础的值,这将会方便我们后续的修正。最后在项目做到相对完整时,写一个Setting的代码,将所有的基础值写进去,然后写一个设置面板就可以了。

    public float SpeedChangeRate = 10.0f;
    private float normalSensitivity=1f;
    [Tooltip("这是在基础鼠标移速下,瞄准速度的倍率")]
    public float aimSensitivity = 0.6f;
    [Tooltip("这是鼠标移动速度比例")]
    private float Sensitivity = 1f;

接下来,在CameraRotation的方法中,将赋值的部分乘上这个倍率。

_cinemachineTagertX += _inputsMassage.look.x * deltaTimeMultiplier * Sensitivity;
_cinemachineTagertY += _inputsMassage.look.y * deltaTimeMultiplier * Sensitivity;

在DoAim中加入对这个值修改

    private void DoAim()
    {
        _aimCamera.gameObject.SetActive(_isAim);
        _anim.SetBool("Aiming", _isAim);
        if(_isAim)
        {
            Sensitivity = aimSensitivity;
        }
        else
        {
            Sensitivity = normalSensitivity;
        }
    }

瞄准点的处理

我们在这里要处理瞄准点的问题,这也是之后武器类的处理方式。
我们写一个Ray,然后通过Hitinfo来获取命中点。
我们使用相机的ScreenPointToRay方法创建一个射线,这个方法需要提供一个二维坐标来确定这条射线会射向哪里(可以理解为两点确定一条线,由于是从相机发出的,所以一点已经确定,只需要另一点确定方向)。
很多时候我们会选择用鼠标位置来决定,因为很多时候我们的鼠标会指向目标点。而在射击类游戏中,我们会把鼠标锁定在屏幕正中心,然后用准星瞄准。我们可以认为屏幕的中心点就是我们所需的另一个点。

Vector2 screenCenterPoint = new Vector2(Screen.width / 2f, Screen.height / 2f);
Ray ray = Camera.main.ScreenPointToRay(screenCenterPoint);

接下来我们获取瞄准信息。使用Physics.Raycast方法

Physics.Raycast(ray,out RaycastHit raycastHit,9999f,aimColliderMask)

其中,ray是射线,out会返回一个值,这里是检测到的信息,第三个数值是检测距离,第四个是检测哪些层级。大家对于这些有什么设置就自己做一做。我这里就不做演示了。
这个方法会返回一个bool值,用这个值做判断即可。
我们将一个球摆到碰撞点,这样以后就可以直接制作射击功能,然后配合IK等组件实现细致的动画。
注意: 请删除目标点的碰撞器(如果是球之类的有碰撞器的)

    [Tooltip("这是目标点")]
    public Transform aimDestinationPoint;
	private void AimPointUpdate()
    {
        Vector2 screenCenterPoint = new Vector2(Screen.width / 2f, Screen.height / 2f);
        Ray ray = Camera.main.ScreenPointToRay(screenCenterPoint);
        if(Physics.Raycast(ray,out RaycastHit raycastHit,9999f,aimColliderMask))
        {
            aimDestinationPoint.position = raycastHit.point;
        }
    }

角色在瞄准状态下的旋转

这个我们仍在Move方法中的旋转部分去设置。之前是是直接转向,现在添加一个if来分开瞄准与否的旋转方式。
之前我们设置了瞄准点,它本身的X与Z坐标是我们应该朝向的方向。
我们新建一个变量,保存目前的瞄准点位置,然后将Y的影响剔除,然后就可以获得一个方向向量,我们此处仍然进行归一化获得方向向量。
最后,我们进行插值处理方向。此处也可以使用旋转。

Vector3 worldAimTarget = aimDestinationPoint.position;
worldAimTarget.y = transform.position.y;
Vector3 aimdirection = (worldAimTarget - transform.position).normalized;
transform.forward = Vector3.Lerp(transform.forward, aimdirection, Time.deltaTime * 20f);

当然,如果我们像下方这段代码写,是会有bug的,原因在于这个判断是基于位移不为零,当位移为零,我们的角色不会旋转。

			if (!_isAim)
            {
                #region 在位移过程中的转向
                float rotation = Mathf.SmoothDampAngle(transform.eulerAngles.y, _targetRotation, ref _rotationVelocity,
                    RotationSmoothTime);
                transform.rotation = Quaternion.Euler(0.0f, rotation, 0.0f);
                #endregion
            }
            else
            {
                #region 瞄准状态下的转向
                Vector3 worldAimTarget = aimDestinationPoint.position;
                worldAimTarget.y = transform.position.y;
                Vector3 aimdirection = (worldAimTarget - transform.position).normalized;
                transform.forward = Vector3.Lerp(transform.forward, aimdirection, Time.deltaTime * 20f);
                #endregion
            }

这里贴上修正过后的代码

		if (_inputsMassage.move!=Vector2.zero)
        {
            _targetRotation = Mathf.Atan2(currentInput.x, currentInput.z) * Mathf.Rad2Deg + _mainCamera.transform.eulerAngles.y;

            if (!_isAim)
            {
                #region 在位移过程中的转向
                float rotation = Mathf.SmoothDampAngle(transform.eulerAngles.y, _targetRotation, ref _rotationVelocity,
                    RotationSmoothTime);
                transform.rotation = Quaternion.Euler(0.0f, rotation, 0.0f);
                #endregion
            }
        }

        if(_isAim)
        {
            #region 瞄准状态下的转向
            Vector3 worldAimTarget = aimDestinationPoint.position;
            worldAimTarget.y = transform.position.y;
            Vector3 aimdirection = (worldAimTarget - transform.position).normalized;
            transform.forward = Vector3.Lerp(transform.forward, aimdirection, Time.deltaTime * 20f);
            #endregion
        }

瞄准时的移动速度限制

这里可以和上方的瞄准状态的旋转一起判断。
不过我们对整个Move函数重新审视时,会发现这里还是与上方的下蹲状态类似,所有一起进行判断。

if (_isAim) targetSpeed = _aimMoveSpeed;

总结

此处很多关于IK的内容我没有说明,因为IK插件本身是要付费的。代码中会夹杂着关于IK和动画状态机的代码,相信需要做动画状态机和IK的同学能够看懂,不需要的只需要看我提到的那些代码,然后选择性剔除便可。

代码

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.InputSystem;
using Cinemachine;
using RootMotion.FinalIK;

public class ThirdPlayerMoveController : MonoBehaviour
{
    CharacterController _characterController;
    PlayerInput _playerInput;
    PlayerInputsMassage _inputsMassage;
    GameObject _mainCamera;
    Animator _anim;

    public PlayerActionState playerActionState = PlayerActionState.Idle;

    [Header("相机设置")]
    public GameObject _cinemachineFollowTarget;
    public CinemachineVirtualCamera _aimCamera;
    float _cinemachineTagertX;
    float _cinemachineTagertY;
    [Tooltip("相机仰角")]
    public float TopClamp = 70.0f;
    [Tooltip("相机俯角")]
    public float BottomClamp = -30.0f;
    [Tooltip("额外的度数覆盖摄像头。有用的微调相机位置时,锁定")]
    public float CameraAngleOverride = 0.0f;
    [Tooltip("瞄准IK")]
    public AimIK _playerAimIK;

    [Header("玩家设置")]
    [Tooltip("这将决定普通行走时的速度")]
    public float walkSpeed = 1.5f;
    [Tooltip("这将决定跑步时的速度")]
    public float _runSpeed = 5.0f;
    private bool _isCrouch = false;
    [Tooltip("这将决定蹲下行走的速度")]
    public float _crouchSpeed = 1.0f;
    [Tooltip("这将决定瞄准状态下的速度")]
    public float _aimMoveSpeed = 1.0f;

    [Header("瞄准设置")]
    [Tooltip("这是基础的鼠标移动速度")]
    public float SpeedChangeRate = 10.0f;
    private float normalSensitivity=1f;
    [Tooltip("这是在基础鼠标移速下,瞄准速度的倍率")]
    public float aimSensitivity = 0.6f;
    [Tooltip("这是鼠标移动速度比例")]
    private float Sensitivity = 1f;
    private bool _isAim = false;
    [Tooltip("这是目标点")]
    public Transform aimDestinationPoint;
    [Tooltip("射线检测时所能检测的目标层级")]
    public LayerMask aimColliderMask;


    [Header("重力及下落")]
    [Tooltip("重力加速度")]
    public float Gravity = -15.0f;
    [Tooltip("是否着地")]
    public bool Grounded = true;
    [Tooltip("检测半径")]
    public float GroundedRadius = 0.28f;
    [Tooltip("检测的层级")]
    public LayerMask GroundLayers;
    [Tooltip("地面宽容度")]
    public float GroundedOffset = -0.14f;
    [Tooltip("进入坠落所需时间")]
    public float FallTimeout = 0.15f;
    private float _fallTimeOutDelta;
    private float _verticalVelocity;
    private float _terminalVelocity = 53.0f;


    private float _currentSpeed;
    private float _targetRotation = 0.0f;
    [Tooltip("角色光滑旋转时间")]
    private float RotationSmoothTime = 0.12f;
    [Tooltip("在角色光滑旋转过程中的速度")]
    private float _rotationVelocity;

    private float _threshold = 0.01f;

    private bool IsCurrentDeviceMouse
    {
        get { return _playerInput.currentControlScheme == "KeyboardMouse"; }
    }

    private void Awake()
    {
        // get a reference to our main camera
        if (_mainCamera == null)
        {
            _mainCamera = GameObject.FindGameObjectWithTag("MainCamera");
        }
    }

    // Start is called before the first frame update
    void Start()
    {
        _characterController = GetComponent<CharacterController>();
        _inputsMassage = GetComponent<PlayerInputsMassage>();
        _playerInput = GetComponent<PlayerInput>();
        _anim = GetComponentInChildren<Animator>();
        _playerAimIK = GetComponentInChildren<AimIK>();
    }

    private void Update()
    {
        PlayerStateJudge();
        DoGravity();
        GroundedCheack();
        Move();
        AimPointUpdate();
        DoAim();
    }

    private void LateUpdate()
    {
        CameraRotation();
    }

    /// <summary>
    /// 相机追踪点的控制
    /// </summary>
    private void CameraRotation()
    {
        if(_inputsMassage.look.sqrMagnitude>_threshold)//look值大于误差代表有输入
        {
            float deltaTimeMultiplier = IsCurrentDeviceMouse ? 1f : Time.deltaTime;

            _cinemachineTagertX += _inputsMassage.look.x * deltaTimeMultiplier * Sensitivity;
            _cinemachineTagertY += _inputsMassage.look.y * deltaTimeMultiplier * Sensitivity;
        }
        _cinemachineTagertX = ClampAngle(_cinemachineTagertX, float.MinValue, float.MaxValue);
        _cinemachineTagertY = ClampAngle(_cinemachineTagertY, BottomClamp, TopClamp);

        _cinemachineFollowTarget.transform.rotation = Quaternion.Euler((-_cinemachineTagertY - CameraAngleOverride) * Settings.mouseYmoveTimes,
                _cinemachineTagertX * Settings.mouseXmoveTimes, 0.0f);
    }

    private void Move()
    {
        _isCrouch = _inputsMassage.crouch;
        //在这里进行状态的判断
        //PlayerStateJudge(); 

        //首先将移动速度赋予临时变量,考虑到有可能在其他地方使用,我们将其存储起来
        //_currentSpeed = walkSpeed;(转换为更加完善的速度控制)
        float targetSpeed = playerActionState switch
        {
            PlayerActionState.Idle => 0f,
            PlayerActionState.Walk => walkSpeed,
            PlayerActionState.Run => _runSpeed,
            _ => 0f
        };
        if (_isCrouch) targetSpeed = _crouchSpeed;
        if (_isAim) targetSpeed = _aimMoveSpeed;

        //玩家当前水平速度的参考
        float currentHorizontalSpeed = new Vector3(_characterController.velocity.x, 0.0f, _characterController.velocity.z).magnitude;
        //偏离度,保证目标速度与目前速度相差大才可以插值,避免小幅度的抽搐
        float speedOffset = 0.1f;
        //判断偏离度
        if (currentHorizontalSpeed < targetSpeed - speedOffset ||
                currentHorizontalSpeed > targetSpeed + speedOffset)
        {
            _currentSpeed = Mathf.Lerp(currentHorizontalSpeed, targetSpeed ,Time.deltaTime * SpeedChangeRate);
            //四舍五入到小数点后3位
            _currentSpeed = Mathf.Round(_currentSpeed * 1000f) / 1000f;
        }
        else
        {
            _currentSpeed = targetSpeed;
        }

        //判断是否进行移动输入
        if (_inputsMassage.move == Vector2.zero) _currentSpeed = 0;

        var currentInput = new Vector3(_inputsMassage.move.x, 0, _inputsMassage.move.y).normalized;

        //单位向量的方向,或者说位移方向
        if (_inputsMassage.move!=Vector2.zero)
        {
            _targetRotation = Mathf.Atan2(currentInput.x, currentInput.z) * Mathf.Rad2Deg + _mainCamera.transform.eulerAngles.y;

            if (!_isAim)
            {
                #region 在位移过程中的转向
                float rotation = Mathf.SmoothDampAngle(transform.eulerAngles.y, _targetRotation, ref _rotationVelocity,
                    RotationSmoothTime);
                transform.rotation = Quaternion.Euler(0.0f, rotation, 0.0f);
                #endregion
            }
        }

        if(_isAim)
        {
            #region 瞄准状态下的转向
            Vector3 worldAimTarget = aimDestinationPoint.position;
            worldAimTarget.y = transform.position.y;
            Vector3 aimdirection = (worldAimTarget - transform.position).normalized;
            transform.forward = Vector3.Lerp(transform.forward, aimdirection, Time.deltaTime * 20f);
            #endregion
        }

        Vector3 targetDir = Quaternion.Euler(0.0f, _targetRotation, 0.0f) * Vector3.forward;
        _characterController.Move(targetDir.normalized * (_currentSpeed * Time.deltaTime)
            + new Vector3(0.0f, _verticalVelocity, 0.0f) * Time.deltaTime);
        //TODO:这里的Move可以执行垂直方向的速度,直接加上垂直的Vector就可以

        _anim.SetFloat("Speed", _currentSpeed);
        _anim.SetBool("Crouch", _isCrouch);
    }

    /// <summary>
    /// 重力
    /// </summary>
    private void DoGravity()
    {
        if(Grounded)
        {
            //重置坠落计时器
            _fallTimeOutDelta = FallTimeout;
            //落地后我们停止垂直速度累加
            if(_verticalVelocity<0.0f)
            {
                _verticalVelocity = -2f;
            }
        }
        else
        {
            //此if用于决定是否是下落动画
            if(_fallTimeOutDelta>=0.0f)
            {
                _fallTimeOutDelta -= Time.deltaTime;
            }
            else
            {
                //下落动画
            }
        }

        if(_verticalVelocity<_terminalVelocity)
        {
            _verticalVelocity += Gravity * Time.deltaTime;
        }
    }

    private void DoAim()
    {
        _aimCamera.gameObject.SetActive(_isAim);
        _anim.SetBool("Aiming", _isAim);
        if(_isAim)
        {
            Sensitivity = aimSensitivity;
        }
        else
        {
            Sensitivity = normalSensitivity;
        }
    }

    /// <summary>
    /// 限制角度
    /// </summary>
    /// <param name="lfAngle"></param>
    /// <param name="lfMin"></param>
    /// <param name="lfMax"></param>
    /// <returns></returns>
    private static float ClampAngle(float lfAngle, float lfMin, float lfMax)
    {
        if (lfAngle < -360f) lfAngle += 360f;
        if (lfAngle > 360f) lfAngle -= 360f;
        return Mathf.Clamp(lfAngle, lfMin, lfMax);
    }

    /// <summary>
    /// 对玩家状态进行判断
    /// </summary>
    private void PlayerStateJudge()
    {
        playerActionState = PlayerActionState.Idle;
        if (_inputsMassage.move!=Vector2.zero)
        {
            playerActionState = PlayerActionState.Walk;
            if (_inputsMassage.run)
                playerActionState = PlayerActionState.Run;
        }
        _isAim = _inputsMassage.aim;
        _playerAimIK.enabled = _isAim;
    }
    /// <summary>
    /// 地面检测
    /// </summary>
    private void GroundedCheack()
    {
        Vector3 spherePosition = new Vector3(transform.position.x, transform.position.y - GroundedOffset, transform.position.z);
        Grounded = Physics.CheckSphere(spherePosition, GroundedRadius, GroundLayers, QueryTriggerInteraction.Ignore);
    }

    /// <summary>
    ///校准十字中心与目标点
    /// </summary>
    private void AimPointUpdate()
    {
        Vector2 screenCenterPoint = new Vector2(Screen.width / 2f, Screen.height / 2f);
        Ray ray = Camera.main.ScreenPointToRay(screenCenterPoint);
        if(Physics.Raycast(ray,out RaycastHit raycastHit,9999f,aimColliderMask))
        {
            aimDestinationPoint.position = raycastHit.point;
        }
    }

}

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

【Unity】第三人称射击游戏开发过程之瞄准状态设计(TPS.S.P1) 的相关文章

  • Unity 分块延迟渲染01 (TBDR)

    现代移动端图形体系结构的概述 现代SoC通常会同时集成CPU和GPU CPU被用于处理需要低内存延迟的序列 大量分支的数据集 其晶体管用于流控制和数据缓存 GPU为处理大型 未分支的数据集 如3D渲染 晶体管专用于寄存器和算术逻辑单元 而不
  • Unity-AR 简介

    Unity AR 简介 现有Unity AR Sdk ARKit 苹果推出的AR开发平台 ARCore Google 推出的增强现实 SDK ARFoundation ARFoundation是ARKit XR插件和ARCore XR插件
  • Unity之获取游戏物体对象或组件的几个方法

    文章目录 前言 通过物体名称获取对象 GameObject Find Transform Find 通过物体标签获取对象 GameObject FindWithTag GameObject FindGameObjectWithTag Gam
  • Unity学习笔记(一)—— 基础知识

    一 基础知识 1 开发团队组成 2 unity特点 图形界面 所见即所得 入门简单 支持C 比OC C 更友好 js 国内外资源丰富 因为使用的人多 跨平台性好 PC端 移动端等 对VR AR的支持最完善 3 成功案例 游戏 炉石传说 神庙
  • unity后台加密时间锁

    前言 在做一些项目的时候 有些不良甲方在给完项目后会有不给尾款的情况 之前都是加一些水印啥的 感觉不是很方便 第一不美观 第二如果甲方给完尾款后还得重新打包去水印 然后又做过一个本地的时间锁 等到时间 程序直接退出 但是感觉还是不方便 有时
  • 【IMGUI】 各种辅助类 EditorGUIUtility、EditorUtility、GUIUtility、GUILayoutUtility

    EditorGUIUtility class in Editor 继承自 GUIUtility EditorGUI 的各种辅助程序 EditorGUIUtility currentViewWidth 我尝试打印了下这个值和position
  • unity3d大型互动照片墙

    1 本次应客户需求 制作一个大型照片墙互动 输出分辨率为9600 4320 注 unity3d官方推荐最大分辨率为8192 3686 4 经过现场长达24小时暴力测试中途未发生问题 姑且判定可以达到正常标准 废话不多说 先上效果 unity
  • unity: C#的Action Event Delegate的异同

    目录 一 Action 二 Event 三 Action和Event区别 四 Delegate 总结 Action Event Delegate的异同 前言 Action Event和Delegate都是C 语言中的重要概念 分别用于管理函
  • unity 性能查看工具Profiler

    文章目录 前言 profiler工具介绍 菜单栏 帧视图 模块视图 模块详细信息 通过profiler分析优化游戏性能 最后 前言 每次进行游戏优化的时候都用这个工具查看内存泄漏啊 代码优化啊之类的东西 真的好用 但是之前也就是自己摸索一下
  • unity实现鼠标右键控制视角

    主要实现的功能是相机跟随主角 鼠标右击移动后 相机的视角会旋转 思路 在主角里创建空的子物体 把相机绑在空物体上 通过旋转空物体来实现视角的旋转 要把相机调整到适当位置 代码如下 public float rotateSpeed 100 设
  • unity dots jobSystem 记录

    Looking for a way to get started writing safe multithreaded code Learn the principles behind our Job System and how it w
  • mixamo根动画导入UE5问题:滑铲

    最近想做一个跑酷游戏 从mixamo下载滑铲动作后 出了很多动画的问题 花了两周时间 终于是把所有的问题基本上都解决了 常见问题 1 动画序列 人物不移动 2 动画序列 人物移动朝向错误 3 蒙太奇 人物移动后会被拉回 4 蒙太奇 动画移动
  • unity小球跟随音乐节奏放大缩小和改变颜色

    放在小球身上 设置对应组件即可 using System Collections using System Collections Generic using Unity VisualScripting using UnityEngine
  • 【Unity】运行时创建曲线(贝塞尔的运用)

    Unity 运行时创建线 贝塞尔的运用 1 实现的目标 在运行状态下创建一条可以使用贝塞尔方法实时编辑的网格曲线 2 原理介绍 2 1 曲线的创建 unity建立网格曲线可以参考 Unity程序化网格体 的实现方法 主要分为顶点 三角面 U
  • 【Unity】运行时创建曲线(贝塞尔的运用)

    Unity 运行时创建线 贝塞尔的运用 1 实现的目标 在运行状态下创建一条可以使用贝塞尔方法实时编辑的网格曲线 2 原理介绍 2 1 曲线的创建 unity建立网格曲线可以参考 Unity程序化网格体 的实现方法 主要分为顶点 三角面 U
  • Unity学习笔记

    一 旋转欧拉角 四元数 Vector3 rotate new Vector3 0 30 0 Quaternion quaternion Quaternion identity quaternion Quaternion Euler rota
  • Unity中URP下的指数雾

    文章目录 前言 一 指数雾 雾效因子 1 FOG EXP 2 FOG EXP2 二 MixFog 1 ComputeFogIntensity 雾效强度计算 2 lerp fogColor fragColor fogIntensity 雾效颜
  • U3D游戏开发中摇杆的制作(NGUI版)

    在PC端模拟摇杆 实现控制摇杆让玩家或者物体移动 以下是完整代码 using System Collections using System Collections Generic using UnityEngine public clas
  • 游戏开发常见操作梳理之NPC任务系统

    多数游戏存在任务系统 接下来介绍通过NPC触发任务的游戏制作代码 using System Collections using System Collections Generic using UnityEngine
  • 游戏开发常见操作梳理之NPC药品商店系统(NGUI版)

    后续会出UGUI Json的版本 敬请期待 游戏开发中经常会出现药品商店 实际操作与武器商店类似 甚至根据实际情况可以简化设置 废话不多说 直接上代码 药品商店的源码 using System Collections using Syste

随机推荐