• 基于Unity引擎的RPG3D项目开发笔录


    RPG游戏开发笔录

    1.将普通3D项目升级为RPG渲染管线

    • 1.Package Manager 搜索 Universal RP进行安装
    • 2.创建通用渲染管线 Rendering--->URP Assets(with Universal Renderer)
    • 3.进入Project Settings--->Graphics,选中刚创建的渲染管线
      在这里插入图片描述
    • 4.进入Quality中同样上述操作
    • 5.进入渲染管线,修改Shadows-->Max Distance,以减小渲染时带给显卡的压力
    • 6.修改默认渲染方式为GPU,并可修改GPU类型和烘焙渲染:
      在这里插入图片描述

    2.导入素材(人物,场景,天空盒)

    更新渲染:
    Windows--->Rendering-->Render Pipeline Converter--->Built-in to URP--->全部勾选--->Initialize Converters--->Convert Assets

    3.第三人称自由视角与移动

    • 1.Input Manager–>复制粘贴 Horizontal和Vertical 并修改其为第四,第五坐标轴,命名为 Camera Rate X & Camera Rate Y
    • 2.给摄像机设置父节点 Photographer,使其能绕着父物体移动

    相机脚本

    public class Photographer : MonoBehaviour
    {
        //相机抬升(绕X轴)
        public float Pitch { get; private set; }
        //相机水平角度(绕Y轴)
        public float Yaw { get; private set; }
    
        //鼠标灵敏度
        public float mouseSensitivity = 5;
        //摄像机旋转速度
        public float cameraRotatingSpeed = 80;
        public float cameraYSpeed = 5;
    
        //相机跟随目标
        private Transform followTarget;
    
    
        public void InitCamera(Transform target)
        {
            followTarget = target;
            transform.position = target.position;
        }
    
        private void Update()
        {
            UpdateRotation();
            UpdatePosition();
        }
    
        private void UpdateRotation()
        {
            Yaw += Input.GetAxis("Mouse X") * mouseSensitivity;
            Yaw += Input.GetAxis("CameraRateX") * cameraRotatingSpeed * Time.deltaTime ;
            //Debug.Log(Yaw);
            Pitch += Input.GetAxis("Mouse Y") * mouseSensitivity;
            Pitch += Input.GetAxis("CameraRateY") * cameraRotatingSpeed * Time.deltaTime;
            //限制抬升角度
            Pitch = Mathf.Clamp(Pitch, -90, 90);
            //Debug.Log(Pitch);
    
            transform.rotation = Quaternion.Euler(Pitch, Yaw, 0);
        }
    
        private void UpdatePosition()
        {
            Vector3 position = followTarget.position;
            float newY = Mathf.Lerp(transform.position.y, position.y, Time.deltaTime * cameraYSpeed);
            transform.position = new Vector3(position.x,newY,position.z);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50

    人物移动脚本(通用)

    [RequireComponent(typeof(Rigidbody))]
    public class CharacterMove : MonoBehaviour
    {
        //刚体组件
        [Header("组件")]
        private Rigidbody rb;
        
        public Vector3 CurrentInput { get; private set; }
    
        public float MaxWalkSpeed = 5;
    
    
        void Awake()
        {
            rb = GetComponent<Rigidbody>();
        }
    
    
        private void FixedUpdate()
        {
            rb.MovePosition(rb.position + CurrentInput * MaxWalkSpeed * Time.fixedDeltaTime);
            //rb.MoveRotation(Quaternion.LookRotation(CurrentInput * MaxWalkSpeed * Time.fixedDeltaTime));
            //rb.rotation = Quaternion.LookRotation(CurrentInput * MaxWalkSpeed * Time.fixedDeltaTime);
            //目标旋转角度
            Quaternion quaternion = Quaternion.LookRotation(CurrentInput * MaxWalkSpeed * Time.fixedDeltaTime);
            //平滑过渡,deltaTime为每帧渲染的时间
            transform.localRotation = Quaternion.Lerp(transform.localRotation, quaternion, Time.deltaTime * 10f);
    
        }
    
        public void SetMovementInput(Vector3 input)
        {
            
            CurrentInput = Vector3.ClampMagnitude(input, 1);
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35

    玩家移动脚本

    [RequireComponent(typeof(Rigidbody))]
    public class PlayerLogic : MonoBehaviour
    {
        [Header("组件")]
        //刚体组件
        private Rigidbody rb;
        //动画组件
        private Animator anim;
        //移动组件
        private CharacterMove characterMove;
        //摄像机组件
        public Photographer photographer;
    
        //摄像机的根位置
        public Transform followTarget;
    
        //附加项
        public float jumpForce = 10f;
    
    
        [Header("人物信息")]
        //人物移动方向
        private bool isForward, isBack, isLeft, isRight,isFall;
    
    
        void Awake()
        {
            rb = GetComponent<Rigidbody>();
            anim = GetComponent<Animator>();
            characterMove = GetComponent<CharacterMove>();
            photographer.InitCamera(followTarget);
        }
        
        void Update()
        {
            UpdateMovementInput();
            //Jump();
            AttackAnim();
            SwitchAnim();
        }
    
        //动画变量同步
        private void SwitchAnim()
        {
            anim.SetBool("Forward", isForward);
            anim.SetBool("Back", isBack);
            anim.SetBool("Left", isLeft);
            anim.SetBool("Right", isRight);
            anim.SetBool("Fall", isFall);
        }
    
        #region 玩家移动
        //人物移动函数
        private void UpdateMovementInput()
        {
            /*
             * TODO:添加人物死亡条件限制
             */
            float ad = Input.GetAxis("Horizontal");
            //Debug.Log("ad值为:" + ad);
            float ws = Input.GetAxis("Vertical");
            //Debug.Log("ws值为:" + ws);
    
            Quaternion rot = Quaternion.Euler(0, photographer.Yaw, 0);
            characterMove.SetMovementInput(rot * Vector3.forward * ws + 
                                           rot * Vector3.right * ad);
    
            if (ad != 0 || ws != 0)
            {
                /*//目标旋转角度
                Quaternion quaternion = Quaternion.LookRotation(new Vector3(ad, 0, ws));
                //平滑过渡,deltaTime为每帧渲染的时间
                transform.localRotation = Quaternion.Lerp(transform.localRotation, quaternion, Time.deltaTime * 10f);*/
    
    
                //动画状态切换
                if (ad > 0.1)
                {
                    isRight = true;
    
                }else if (ad < -0.1)
                {
                    isLeft = true;
                }else
                {
                    isRight = false;
                    isLeft = false;
                }
    
                if (ws > 0.1)
                {
                    isForward = true;
                }
                else if (ws < -0.1)
                {
                    isBack = true;
                }
                else
                {
                    isForward = false;
                    isBack = false;
                }
            }
    
            //添加速度
            //rb.velocity = new Vector3(ad * moveSpeed, 0, ws * moveSpeed);
        }
        #endregion 
    
        #region 玩家跳跃
        //跳跃函数
        private void Jump()
        {
            if (Input.GetKeyDown(KeyCode.Space) && isFall)
            {
                anim.SetTrigger("Jump");
                rb.velocity = new Vector3(rb.velocity.x, jumpForce,rb.velocity.z);
                isFall = false;
            }     
        }
    
        private void OnTriggerEnter(Collider other)
        {
            if (other.gameObject.CompareTag("Ground"))
            {
                Debug.Log("触发到地面了");
                isFall = true;
            }
        }
    
        #endregion
    
        #region 玩家攻击
        private void AttackAnim()
        {
            this.transform.LookAt(this.transform);
            if (Input.GetMouseButtonDown(0))
            {
                anim.SetTrigger("Attack");
            }else if (Input.GetMouseButtonDown(1))
            {
                anim.SetTrigger("Ability");
            }
        }
    
        #endregion
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139
    • 140
    • 141
    • 142
    • 143
    • 144
    • 145
    • 146
    • 147

    4.切换鼠标指针

    切换鼠标指针

    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    using UnityEngine.Events;
    
    [RequireComponent(typeof(Rigidbody))]
    public class CharacterMove : MonoBehaviour
    {    
        [Header("人物攻击模块")]
        //鼠标图标
        public Texture2D target, attack;
        RaycastHit hitInfo;
    
    
        void Awake()
        {
            rb = GetComponent<Rigidbody>();
        }
    
        void Update()
        {
            SetCursorTexture();  
        }
        
        //设置鼠标指针
        void SetCursorTexture()
        {
            Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
    
            if (Physics.Raycast(ray,out hitInfo))
            {
                switch (hitInfo.collider.gameObject.tag)
                {
                    case "Ground":
                        Cursor.SetCursor(target, new Vector2(16, 16), CursorMode.Auto);
                        break;
                    case "Enemy":
                        Cursor.SetCursor(attack, new Vector2(16, 16), CursorMode.Auto);
                        break;
                }
            }
        }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43

    5.遮挡剔除实现

    • 1.create-->Shader Graph--> URP--->Unit Shader Graph,并起名为 Occlusion Shader
    • 2.基于它创建材质Occlusion放回meterials文件夹
    • 编辑Occlusion Shader为如下参考:
      在这里插入图片描述
      材质参数参考:
      在这里插入图片描述
      URP参数参考:
      在这里插入图片描述
      遮挡剔除实现完成!

    6.敌人的创建,站岗,追逐

    • 1.导入素材,设置基本状态信息(Idle,Chase,Guard,Dead)
    • 2.敌人的状态切换,追逐玩家以及切换动画:
    using UnityEngine;
    using UnityEngine.AI;
    
    public enum EnemyStates { GUARD,PATROL,CHASE,DEAD }
    
    [RequireComponent(typeof(NavMeshAgent))]
    public class EnemyController : MonoBehaviour
    {
        [Header("组件")]
        private NavMeshAgent agent;
        private Animator anim;
    
        [Header("状态变量")]
        private EnemyStates enemyStates;
    
        [Header("敌人基础设置")]
        //敌人可视范围
        public float sightRadius;
        //敌人的类型
        public bool isGuard;
        //敌人攻击目标
        private GameObject AttackTarget;
        //记录敌人初始站岗位置
        private Vector3 GuardPos;
    
        //敌人状态
        bool isWalk,isDead;
    
    	//脱战观望时间
    	public float remainLookatTime = 3f;
        //脱战停留计时器
        private float remainTimer;
    
        private void Awake()
        {
            agent = GetComponent<NavMeshAgent>();
            anim = GetComponent<Animator>();
        }
    
    void Start()
        {
            //给敌人初始位置赋值
            GuardPos = transform.position;
        }
        
        void Update()
        {
            SwitchStates();
            SwitchAnimation();
        }
    
        //切换动画
        void SwitchAnimation()
        {
            anim.SetBool("Chase", isChase);
            anim.SetBool("Walk", isWalk);
            anim.SetBool("Dead", isDead);
        }
    
        void SwitchStates()
        {
            //如果发现玩家,切换到Chase
            if (FoundPlayer())
            {
                enemyStates = EnemyStates.CHASE;
                Debug.Log("发现玩家");
            }
    
            switch (enemyStates)
            {
                case EnemyStates.GUARD:
                	Guard();
                    break;
                case EnemyStates.PATROL:
                    break;
                case EnemyStates.CHASE:
                    ChasePlayer();
                    break;
                case EnemyStates.DEAD:
                    break;
            }
        }
    
        //是否发现玩家
        bool FoundPlayer()
        {
            //拿到检测到对应范围内的所以碰撞体
            var hitColliders = Physics.OverlapSphere(this.transform.position, sightRadius);
    
            //检测其中是否存在Player
            foreach(var target in hitColliders)
            {
                if (target.gameObject.CompareTag("Player"))
                {
                    AttackTarget = target.gameObject;
                    return true;
                }
            }
            //脱离目标
            AttackTarget = null;
            return false;
        }
    
        //追逐玩家函数
        void ChasePlayer()
        {
            //脱战逻辑
            if (!FoundPlayer())
            {
                if (remainTimer > 0)
                {
                    //观望战立状态
                    agent.destination = transform.position;
                    remainTimer -= Time.deltaTime;
                    isWalk = false;
                }
                else
                {
                    
                    //回到上一个状态(巡逻或者站立)
                    enemyStates = isGuard ? EnemyStates.GUARD : EnemyStates.PATROL;
                }
    
            }
            else
            {
                agent.isStopped = false;
                isWalk = true;
                //刷新观望时间计时器
                remainTimer = remainLookatTime;
                //当距离小于攻击距离时,开始攻击
                if (Vector3.Distance(transform.position, AttackTarget.transform.position) < characterStats.AttackData.attackRange)
                {
                    isWalk = false;
                    agent.isStopped = true;
                    anim.SetTrigger("Attack");
                }
                else
                {
                    
                    //追击Player
                    agent.destination = AttackTarget.transform.position;
                }
    
            }
            
    
        }
        //怪物返回出生点以及站岗逻辑
        void Guard()
        {
            //判断怪物是否返回出生点
            if (Vector3.Distance(transform.position,GuardPos) <= 3)
            {
                isWalk = false;
            }
            else
            {
                isWalk = true;
                agent.destination = GuardPos;
            }
            
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139
    • 140
    • 141
    • 142
    • 143
    • 144
    • 145
    • 146
    • 147
    • 148
    • 149
    • 150
    • 151
    • 152
    • 153
    • 154
    • 155
    • 156
    • 157
    • 158
    • 159
    • 160
    • 161
    • 162
    • 163
    • 164

    7.人物基本数值实现

    • 1.给文件夹分好类,分别创建 MonoBehaviorScriptableObject文件夹。
    • 2.创建第一个ScriptableObject脚本文件,命名为 CharacterData_SO
    • 3.编写人物应有的通用属性,并在专门的数据文件夹下创建出数据文件。

    数据模板写法

    [CreateAssetMenu(fileName ="New Data",menuName = "Character Stats/Data")]
    public class CharacterData_SO : ScriptableObject
    {
        [Header("Stats Info")]
        //最大生命值
        public int maxHealth;
        //当前生命值
        public int currentHealth;
        //基础防御力
        public int baseDefence;
        //当前防御力
        public int currentDefence;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    数据操作写法

    public class CharacterStats : MonoBehaviour
    {
        public CharacterData_SO characterData;
    
        #region Read from Data_SO
        public int MaxHealth 
        {
            get
            {
                return characterData != null ? characterData.maxHealth : 0;
            }
            set
            {
                characterData.maxHealth = value;
            }
        }
    
        public int CurrentHealth
        {
            get
            {
                return characterData != null ? characterData.currentHealth : 0;
            }
            set
            {
                characterData.currentHealth = value;
            }
        }
    
        public int BaseDefence
        {
            get
            {
                return characterData != null ? characterData.baseDefence : 0;
            }
            set
            {
                characterData.baseDefence = value;
            }
        }
    
        public int CurrentDefence
        {
            get
            {
                return characterData != null ? characterData.currentDefence : 0;
            }
            set
            {
                characterData.currentDefence = value;
            }
        }
        #endregion
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 4.将数据操作脚本 CharacterStats挂载到Player和Enemy上,并拖入对应数据文件。
    • 5.在玩家控制脚本中获取到数据文件,并获取到操作该数据的方法。
    • 6.同样方式实现攻击数值的基本书写:
    [CreateAssetMenu(fileName = "New Attack",menuName = "Attack/Data")]
    public class AttackData_SO : ScriptableObject
    {
        //攻击距离
        public float attackRange;
        //技能距离
        public float skillRange;
        //冷却时间
        public float coolDown;
        //基础伤害范围
        public int minDamage;
        public int maxDamage;
    
        //暴击倍率(伤害)
        public float criticalMultiplier;
        //暴击率
        public float criticalChance;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    8.攻击功能的实现(重难)

    • 1.创建攻击数据脚本:包括玩家和怪物
      给出参考:Player Attack Data_SO
    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    
    [CreateAssetMenu(fileName = "New Attack",menuName = "Attack/Data")]
    public class AttackData_SO : ScriptableObject
    {
        //攻击距离
        public float attackRange;
        //技能距离
        public float skillRange;
        //冷却时间
        public float coolDown;
        //基础伤害范围
        public int minDamage;
        public int maxDamage;
    
        //暴击倍率(伤害)
        public float criticalMultiplier;
        //暴击率
        public float criticalChance;
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    玩家攻击脚本面板参考:
    在这里插入图片描述

    • 2.在角色数据操作脚本中引入攻击脚本:并写出伤害计算逻辑
    public class CharacterStats : MonoBehaviour
    {
        public CharacterData_SO characterData;
    
        public AttackData_SO AttackData;
    
        #region 攻击模块
        public void TakeDamage(CharacterStats attacker,CharacterStats defencer)
        {
            //计算一次攻击的伤害
            float coreDamage = UnityEngine.Random.Range(attacker.AttackData.minDamage, attacker.AttackData.maxDamage);
    
            //控制最低伤害为1
            int damage = Mathf.Max((int)coreDamage - defencer.CurrentDefence,1);
            
            //控制血量最低为0
            CurrentHealth = Mathf.Max(CurrentHealth - damage, 0);
            //更新 UI
            
        }
        #endregion
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    由于这里攻击函数是玩家和怪物通用的,所以后续只需要传入攻击者和受击者即可正常完成伤害计算并扣血。其次,怪物的进攻逻辑是AI操控,只需要在追踪玩家时,吧玩家对象传入怪物的攻击目标变量即可。但玩家的攻击目标选择却成了问题,这里采用类似怪物发现玩家的方式,采用一个圆形探测范围(该范围阈值即为玩家的攻击范围),当玩家进行攻击操作时,只需要检测怪物是否在范围检测距离内,如果在距离内,则正常调用TakeDamage(),如果不在,则视为空刀。缺点是实际攻击范围(以玩家为圆心,以攻击距离为半径的圆形内)与动画攻击范围(人物的前方扇形区域)不符合。后续有待改进…此外,暴击功能较为繁琐,目前未实现。以下为判断暴击的常用方法之一,仅供参考:

    //设置全局变量,判断是否暴击
    characterStats.isCritical = UnityEngine.Random.value < characterStats.attackData.criticalChance;
    
    • 1
    • 2
    • 3.怪物的攻击逻辑:(将其挂载到怪物攻击动画的某一帧上)
    	//引入数值组件
    	private CharacterStats characterStats;
    
    	//给组件赋值
    	private void Awake()
    	{
    		characterStats = GetComponent<CharacterStats>();
    	}
    	
    	//每次游戏开始给怪物重置生命值
    	void Start()
    	{
    		//刷新初始生命值
            characterStats.CurrentHealth = characterStats.MaxHealth;
    	}
    
    //怪物攻击的出伤害事件
        void Hit()
        {
            if (AttackTarget != null)
            {
                //拿到受害者的属性信息
                var targetStats = AttackTarget.GetComponent<CharacterStats>();
                //造成伤害
                targetStats.TakeDamage(characterStats, targetStats);
            }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 4.玩家的攻击逻辑
    //引入数值组件
    //Awake初始化数值组件
    //初始化生命值
    
    #region 玩家攻击
        public void AttackAnim()
        {
            if (Input.GetMouseButtonDown(0))
            {
                anim.SetTrigger("Attack");
            }else if (Input.GetMouseButtonDown(1))
            {
                anim.SetTrigger("Ability");
            }
        }
    
        //范围检测
        private bool AttackRangeTest()
        {
            Debug.Log(characterStats.AttackData.attackRange);
            //拿到检测到对应范围内的所有碰撞体
            Collider[] hitColliders = Physics.OverlapSphere(transform.position, characterStats.AttackData.attackRange);
            foreach (Collider collider in hitColliders)
            {
                if (collider.gameObject.CompareTag("Enemy"))
                {
                    AttackTarget = collider.gameObject;
                    return true;
                }
            }
            return false;
        }
    
        //玩家的出伤害逻辑
        void Hit()
        {
            if (AttackRangeTest())
            {
                CharacterStats targetStats = AttackTarget.GetComponent<CharacterStats>();
                targetStats.TakeDamage(characterStats, targetStats);
            }
            else
            {
                Debug.Log("空刀!");
            }
        }
    #endregion
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 5.死亡的判断
      思路:死亡判断直接在Update()函数中逐帧判断当前生命值是否为0即可:
      玩家死亡:
    //引入变量
    	bool isDead;
    	void Update()
        {
            if (characterStats.CurrentHealth != 0)
            {
                UpdateMovementInput();
                Jump();
                AttackAnim();
                
            }
            else
            {
                isDead = true;
                rb.constraints = RigidbodyConstraints.FreezePosition;
                rb.constraints = RigidbodyConstraints.FreezeRotation;
            }
            SwitchAnim();
    
        }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    怪物死亡:

    	void Update()
        {
            if (characterStats.CurrentHealth != 0)
            {
                SwitchStates();
            }
            else
            {
                isDead = true;
                enemyStates = EnemyStates.DEAD;
            }
            
            SwitchAnimation();
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    9.泛型单例模式以及怪物获胜通知

    • 1.写一个管理单例模式的基类( Tools/Singleton ):
    //管理类的基类
    //泛型单例模式
    
    public class Singleton<T> : MonoBehaviour where T:Singleton<T>
    {
        private static T instance;
    
        //外部可访问的函数
        public static T Instance
        {
            get { return instance; }
        }
    
        //可继承可重写的Awake方法
        protected virtual void Awake()
        {
            if (instance != null)
            {
                Destroy(gameObject);
            }
            else
            {
                instance = (T)this;
            }
        }
    
        public static bool IsInitialized
        {
            get { return instance != null; }
        }
    
        protected virtual void OnDestroy() 
        { 
            if (instance == this)
            {
                instance = null;
            }    
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 2.写一个发放广播的接口 IEndGameObserver
    public interface IEndGameObserver 
    {
        void EndNotify();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 3.写一个游戏管理类继承管理基类 ( Manages/GameManager )
    public class GameManager : Singleton<GameManager>
    {
        public CharacterStats playerStats;
    
    
        //收集所有需要接收广播的对象
        List<IEndGameObserver> endGameObservers = new List<IEndGameObserver>();
    
    	//在游戏开始时将玩家信息注册到管理类
        public void RigisterPlayer(CharacterStats player)
        {
            playerStats = player;
        }
    
    	//在游戏开始时,将怪物信息(被通知者)录入通知对象
        public void AddObserver(IEndGameObserver observer)
        {
            endGameObservers.Add(observer);
        }
    
    	//怪物被消灭时,清理通知对象
        public void RemoveObserver(IEndGameObserver observer)
        {
            endGameObservers.Remove(observer);
        }
    
    	//通知逻辑
        public void NotifyObservers()
        {
            foreach(var observer in endGameObservers)
            {
                observer.EndNotify();
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 4.在玩家逻辑中添加注册信息到管理类
      PlayerLogic:
    void Start()
        {
            //利用单例模式调用注册玩家信息
            GameManager.Instance.RigisterPlayer(characterStats);
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 5.在怪物逻辑中添加 注册和销毁通知,以及通知内容的具体实现
      EnemyController:
    //首先让其实现IEndGameObserver接口
    public class EnemyController : MonoBehaviour,IEndGameObserver
    {
    	private void OnEnable()
        {
            GameManager.Instance.AddObserver(this);
        }
    
        private void OnDisable()
        {
        	if (!GameManager.IsInitialized) return;
            GameManager.Instance.RemoveObserver(this);
        }
    }
    
    	public void EndNotify()
        {
            //获胜动画
            //停止所有移动
            //停止Agent
            isWalk = false;
            AttackTarget = null;
            isVictory = true;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    报错总结:此处在GameManager的实例化阶段一直报 空引用异常,后来发现自己没有创建 Game Manager 的结点,并挂载GameManager的脚本。此外,还需注意 Onable函数是场景初始化时被创建,此处不涉及场景切换,故,需将其暂时写在Start方法中,后续再更改。

    10.模板生成更多Enemy

    • 1.由于之前写法会使多个复制出来的怪物属性共享,这显然不是我们需要的功能,故我们需要创建一个属性模板,让其新生成的怪物以该模板生成对应的数值。
      Character Stats中:
    public class CharacterStats : MonoBehaviour
    {
    	public CharacterData_SO templateData;
    
        public CharacterData_SO characterData;
    
    	void Awake()
        {
            if (templateData != null)
            {
                characterData = Instantiate(templateData);
            }    
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 2.引入新怪物的美术模型资源,添加必要组件EnemyController,NavMesh Agent以及基本碰撞体,别忘了将其标签改为Enemy,并创建对应的数据文件。
    • 3.在重置新类型敌人的动画信息时,可直接创建一个 Override Animators,可十分便利的重置怪物动画。

    11.拓展方法实现怪物的攻击范围限制

    由于之前只要怪物发动攻击动画,玩家必掉血。这是极度不合理的,我们希望玩家有一定的闪避空间或容错,故我们采用 拓展方法 Extension Method 来对其攻击范围做一个限制。

    • 1.写一个 Extension Method脚本:
    public static class ExtensionMethod 
    {
    
        //确保怪物攻击在正前方[-60°,60°]之间范围攻击有效
        private const float dotThreshold = 0.5f;
    
        public static bool IsFacingTarget(this Transform transform, Transform target)
        {
            //计算出目标物对于攻击者正前方的相对位置并取单位向量
            var vectorToTarget = target.position - transform.position;
            vectorToTarget.Normalize();
    
            float dot = Vector3.Dot(transform.forward, vectorToTarget);
    
            return dot >= dotThreshold;
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 2.并在怪物的攻击动画事件中加上限制条件:
      EnemyController:
    //怪物攻击的出伤害事件
        void Hit()
        {
            if (AttackTarget != null && transform.IsFacingTarget(AttackTarget.transform))
            {
                //拿到受害者的属性信息
                var targetStats = AttackTarget.GetComponent<CharacterStats>();
                //造成伤害
                targetStats.TakeDamage(characterStats, targetStats);
            }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 3.给Boss的基础攻击增加击退效果

    但暂时有点问题,无法正常击退玩家。后续再改BUG。

    public class Boss_Rock : EnemyController
    {
        //击飞玩家的力
        public float kickForce = 25f;
    
        public void KickOff()
        {
            if (AttackTarget != null && transform.IsFacingTarget(AttackTarget.transform))
            {
                Debug.Log("被踢开了");
                //拿到受害者的属性信息
                var targetStats = AttackTarget.GetComponent<CharacterStats>();
    
                //计算击飞方向(问题)
                //FIXME:有待修改
                Vector3 direction = (AttackTarget.transform.position - transform.position).normalized;
                AttackTarget.GetComponent<Rigidbody>().velocity = direction * kickForce;
                
                //造成伤害
                targetStats.TakeDamage(characterStats, targetStats);
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 4.设置石头人可投掷物

    – 1.拖入石头的素材,添加必要组件:RigidBody,Mesh Collider(勾选第一项),脚本Rock.cs

    public class Rock : MonoBehaviour
    {
    	private Rigidbody rb;
    	
    	[Header("Basic Settings")]
    	public float force;
    
    	public GameObject target;
    	private Vector3 direction;
    
    	void Start()
    	{
    		rb = GetComponent<Rigidbody>();
    		FlyToTarget();	
    	}
    
    	public void FlyToTarget()
    	{
    		//预防石头生成瞬间玩家脱离范围
    		if (target == null)
    		{
    			target = FindObjectOfType<PlayerController>().gameObject;
    		}
    		direction = (target.transform.position - transform.position + Vector3.up).normalized;
    		rb.AddForce(direction * force, ForceMode.Impulse);
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    – 2.修改石头人生成石头代码:

    public class Boss_Rock : EnemyController
    {
    	//扔出的石头的预制体
    	public GameObject rockPrefab;
    	//出手点的坐标
    	public Transform handPos;
    
    	//投掷石头的逻辑
    	public void ThrowRock()
    	{
    		if (AttackTarget != null)
    		{
    			var rock = Instantiate(rockPrefab,handPos.position,Quaternion.identify);
    			rock.GetComponent<Rock>().target = AttackTarget;
    		}
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    – 3.将ThrowRock方法添加到动画对应帧数上。

    – 4.设置石头的状态:在被投掷出的时候能对敌人以及玩家造成伤害,但落地以后无法对玩家或敌人造成伤害。

    public class Rock : MonoBehaviour
    {
    	public enum RockStates { HitPlayer,HitEnemy,HitNothing };
    	//石头的伤害值
        public int damage = 8;
        //石头的状态
        public RockStates rockStates;
    
    	void Start()
        {
            rb = GetComponent<Rigidbody>();
            //为了防止石头刚一出来就被判断为hitNothing
            rb.velocity = Vector3.one;
            //初始化石头的状态
            rockStates = RockStates.HitPlayer;
            //石头被生成的时候就自动飞向目标
            FlyToTarget();
            //石头扔出三秒后延迟销毁
            Destroy(this.gameObject, 3);
        }
    	//逐帧判断,当石头几乎静止时变得不再有威胁
    	void FixedUpdate()
        {
            Debug.Log(rb.velocity.sqrMagnitude);
            if (rb.velocity.sqrMagnitude < 1)
            {
                rockStates = RockStates.HitNothing;
            }    
        }
    
    	void OnCollisionEnter(Collision collision)
        {
            
            switch (rockStates)
            {
                case RockStates.HitPlayer:
                    if (collision.gameObject.CompareTag("Player"))
                    {
                        CharacterStats characterStats = collision.gameObject.GetComponent<CharacterStats>();
                        //碰到玩家了,造成伤害,并对玩家播放受击动画(TakeDamage的函数重载)
                        characterStats.TakeDamage(damage,characterStats);
                        collision.gameObject.GetComponent<Animator>().SetTrigger("Hit");
    
                        rockStates = RockStates.HitNothing;
                    }
                    break;
    
                case RockStates.HitEnemy:
                    if (collision.gameObject.CompareTag("Enemy"))
                    {
                        var EnemyStats = collision.gameObject.GetComponent<CharacterStats>();
                        EnemyStats.TakeDamage(damage, EnemyStats);
                        //攻击到敌人以后,也将其设为无危胁状态
                        
                    }
                    break;
            }    
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59

    – 5.函数重载TakeDamage()方法,让石头也能造成伤害:

    	public void TakeDamage(int damage,CharacterStats defencer)
        {
            int finalDamage = Mathf.Max(damage - defencer.CurrentDefence, 1);
            CurrentHealth = Mathf.Max(CurrentHealth - finalDamage, 0);
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    – 6.修改玩家的攻击逻辑,使其攻击石头也具有一定逻辑:

    //范围检测
        private bool AttackRangeTest()
        {
            //Debug.Log(characterStats.AttackData.attackRange);
            //拿到检测到对应范围内的所有碰撞体
            Collider[] hitColliders = Physics.OverlapSphere(transform.position, characterStats.AttackData.attackRange);
            foreach (Collider collider in hitColliders)
            {
            	//有限判断敌人,如果不存在敌人,则判断是否有可攻击物
                if (collider.gameObject.CompareTag("Enemy") 
                 || collider.gameObject.CompareTag("Attackable"))
                {
                    AttackTarget = collider.gameObject;
                    return true;
                }
            }
            return false;
        }
    
        //玩家的出伤害逻辑
        void Hit()
        {
            
            if (AttackRangeTest())
            {
                if (AttackTarget.CompareTag("Attackable"))
                {
                    //进一步判断是石头
                    if (AttackTarget.GetComponent<Rock>())
                    {
                        AttackTarget.GetComponent<Rock>().rockStates = Rock.RockStates.HitEnemy;
                        AttackTarget.GetComponent<Rigidbody>().velocity = Vector3.one;
                        AttackTarget.GetComponent<Rigidbody>().AddForce(transform.forward * 20, ForceMode.Impulse);
                    }
                }else if (AttackTarget.CompareTag("Enemy"))
                {
                    CharacterStats targetStats = AttackTarget.GetComponent<CharacterStats>();
                    targetStats.TakeDamage(characterStats, targetStats);
                }
                
            }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42

    12.血条UI的设计

    • 1.创建一个Canvas命名为 HealthBarCanvas,修改Canvas的 UI Scale Mode改为 World Space,并设置相机Camera。创建一个子物体UI image,命名 Bar Holder。

    • 2.对UI界面的位置信息进行调整,修改长3和高0.25(参考值)。

    • 3.在Package Manager中引入 3D sprite,创建一个2D Object–>Square,找到其基础的文件,复制一份图片另存起来。将其拖入到Bar Holder的Source Image中,并可修改其颜色(血条底色)。

    • 在这里插入图片描述

    • 4.继续创建Bar Health的子节点Image,尺寸参数与父节点保持一致,拖入Source Image,修改颜色(血条上层色),并将其改为滑动条的形式进行显示。

    • 5.写UI脚本操控血条的变化。

    public class HealthBarUI : MonoBehaviour
    {
        //血条预制体
        public GameObject healthBarPrefab;
    
        //血条位置
        public Transform barPoint;
    
        //是否让血条持续显示
        public bool alwaysVisible;
        //血条被唤醒后显示的时间
        public float visibleTime;
    
        //血量滑动条
        Image healthSlider;
        Transform UIBar;
    
        //摄像机的位置(保证始终正对摄像机)
        Transform camera;
    
        //拿到当前目标的血量信息
        private CharacterStats currentStats;
    
        void Start()
        {
            currentStats = GetComponent<CharacterStats>();
        }
    
        void OnEnable()
        {
            camera = Camera.main.transform;
            
            foreach (Canvas canvas in FindObjectsOfType<Canvas>())
            {
                if (canvas.renderMode == RenderMode.WorldSpace)
                {
                    UIBar = Instantiate(healthBarPrefab, canvas.transform).transform;
                    healthSlider = UIBar.GetChild(0).GetComponent<Image>();
                    UIBar.gameObject.SetActive(alwaysVisible);
                }
            }
        }
    
        //血条跟随敌人
        void LateUpdate()
        {
            if (UIBar != null)
            {
                UIBar.position = barPoint.position;
    
                UIBar.forward = -camera.forward;
                
            }
        }
    
        void Update()
        {
            
            UpdateHealthBar(currentStats.CurrentHealth, currentStats.MaxHealth);
        }
    
        private void UpdateHealthBar(int currentHealth, int maxHealth) 
        {
            if (currentHealth <= 0)
            {
                if (UIBar != null)
                {
                    Destroy(UIBar.gameObject);
                }
    
            }
            else
            {
                UIBar.gameObject.SetActive(true);
    
                float sliderPercent = (float)currentHealth / maxHealth;
                //Debug.Log("当前血量仅剩:" + sliderPercent);
                healthSlider.fillAmount = sliderPercent;
            }
            
        }
    
        
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84

    总结:该套程序采用逐帧检测血条变化,相对来说效率较差,优化可以让角色攻击时计算一次血条变化。后续会优化!!!

    13.玩家升级系统

    • 1.先添加一下玩家的属性
      Character_SO:
    using System;
    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    
    [CreateAssetMenu(fileName ="New Data",menuName = "Character Stats/Data")]
    public class CharacterData_SO : ScriptableObject
    {
        [Header("Stats Info")]
        //基础生命值
        public float baseHealth;
        //最大生命值
        public float maxHealth;
        //当前生命值
        public float currentHealth;
        //基础防御力
        public int baseDefence;
        //当前防御力
        public int currentDefence;
    
        [Header("击杀敌人获得的经验值")]
        public int killExp;
    
        [Header("玩家升级系统")]
        //当前等级
        public int currentLevel;
        //最大等级
        public int maxLevel;
        //升级所需基础经验值
        public int baseExp;
        //当前经验值
        public int currentExp;
        //升级经验提高值
        public int ExpIncrement;
        //升级属性加权
        public float levelBuff;
    
        //更新经验值函数
        public void UpdateExp(int Exp)
        {
            currentExp += Exp;
    
            if (currentExp >= baseExp)
            {
                LevelUp();
            }
        }
    
        private void LevelUp()
        {
            //提高等级(将当前等级限制在[0,maxLevel之间])
            currentLevel = Mathf.Clamp(currentLevel + 1, 0, maxLevel);
            //升级所需的经验值也随之提高
            baseExp += ExpIncrement;
            //血量提高5%
            maxHealth = baseHealth * (levelBuff * currentLevel + 1);
            currentHealth = maxHealth;
            Debug.Log("LevelUP,当前血量为" + currentHealth);
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 暂时为了学习升级逻辑的构思以及数值的模范书写,后续功能有待优化!

    • 2.在造成伤害界面 添加击杀增加经验的判断:
      CharacterStats:

     public void TakeDamage(CharacterStats attacker,CharacterStats defencer)
        {
            //计算一次攻击的伤害
            float coreDamage = UnityEngine.Random.Range(attacker.AttackData.minDamage, attacker.AttackData.maxDamage);
    
            //控制最低伤害为1
            int damage = Mathf.Max((int)coreDamage - defencer.CurrentDefence,1);
            
            //控制血量最低为0
            CurrentHealth = Mathf.Max(CurrentHealth - damage, 0);
            //更新 UI
            //经验值update
            if (CurrentHealth == 0)
            {
                attacker.characterData.UpdateExp(defencer.characterData.killExp);
            }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    14.玩家的血条UI

    • 1.同样优先创建一个Canvas,设置参考如下:
    • 在这里插入图片描述

    做出如下效果的UI界面:
    在这里插入图片描述
    并在其父节点下嵌入以下脚本以控制血条的变化:
    PlayerHealthUI:

    using UnityEngine.UI;
    
    public class PlayerHealthUI : MonoBehaviour
    {
        Text levelText;
    
        Image healthSlider;
    
        Image expSlider;
    
        void Awake()
        {
            //拿到文本子节点
            levelText = transform.GetChild(2).GetComponent<Text>();
            healthSlider = transform.GetChild(0).GetChild(0).GetComponent<Image>();
            expSlider = transform.GetChild(1).GetChild(0).GetComponent<Image>();
        }
    
        void Update()
        {
            levelText.text = "level " + GameManager.Instance.playerStats.characterData.currentLevel.ToString("00");
            UpdateHealth();
            UpdateExp();
        }
    
        void UpdateHealth()
        {
            float sliderPercent = (float)GameManager.Instance.playerStats.CurrentHealth / GameManager.Instance.playerStats.MaxHealth;
            healthSlider.fillAmount = sliderPercent;
        }
    
        void UpdateExp()
        {
            float sliderPercent = (float)GameManager.Instance.playerStats.characterData.currentExp / GameManager.Instance.playerStats.characterData.baseExp;
            expSlider.fillAmount = sliderPercent;
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38

    15.传送门(切换关卡)

    场景内传送

    • 1.在之前Shader Graph目录下继续创建Shader,参数参考如下:
      在这里插入图片描述
    • 2.利用创建的Shader的基础上创建一个Meterial,并调参数。
    • 3.在层级窗口中创建一个Quad,并添加以上材质,在其下创建一个子节点 DestinationPoint 作为被传送点。
    • 4.创建 TransitionPoint.cs脚本控制传送点:(挂载在传送门父类上)
    public class TransitionPoint : MonoBehaviour
    {
        //传送类型(同场景,不同场景)
        public enum TransitionType
        {
            SameScene,DifferentScene
        }
    
        [Header("info")]
        //场景名字(如果同场景则可以不填)
        public string sceneName;
        //传送类型
        public TransitionType type;
    
        //传送到的目的地点
        public TransitionDestination.DestinationTags destinationTag;
    
        //只有该属性触发了才会传送
        private bool canTrans;
    
        void Update()
        {
            if (Input.GetKeyDown(KeyCode.F) && canTrans)
            {
                //场景传送
                SceneController.Instance.TransitionToDestination(this);
            }    
        }
    
    
        void OnTriggerStay(Collider other)
        {
            if (other.CompareTag("Player"))
            {
                canTrans = true;
            }
        }
    
        void OnTriggerExit(Collider other)
        {
            if (other.CompareTag("Player"))
            {
                canTrans = false ;
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 5.创建传送目标点脚本 TransitionDestination.cs:
    public class TransitionDestination : MonoBehaviour
    {
        public enum DestinationTags
        {
            ENTRE,A,B,C
        }
    
        public DestinationTags destinationTag;
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 6.写场景控制脚本实现同场景传送逻辑:SceneController.cs
    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    using UnityEngine.SceneManagement;
    
    public class SceneController : Singleton<SceneController>
    {
        Transform player;
        //玩家的预制体(加载新场景时引入)
        public GameObject playerPrefab;
    
        protected override void Awake()
        {
            base.Awake();
            DontDestroyOnLoad(this);
        }
    
        public void TransitionToDestination(TransitionPoint transitionPoint)
        {
            switch (transitionPoint.type) 
            {
                case TransitionPoint.TransitionType.SameScene:
                    StartCoroutine(Transition(SceneManager.GetActiveScene().name, transitionPoint.destinationTag));
                    break;
                case TransitionPoint.TransitionType.DifferentScene:
                    StartCoroutine(Transition(transitionPoint.sceneName, transitionPoint.destinationTag));
                    break;
            
            }
    
        }
    
        //使用携程异步加载场景
        IEnumerator Transition(string sceneName , TransitionDestination.DestinationTags destinationTag)
        {
            //TODO:保存数据
    
            //判断是相同场景还是不同场景
            if (SceneManager.GetActiveScene().name != sceneName)
            {
                //等待return值完成后再执行其他代码(异步加载)
                yield return SceneManager.LoadSceneAsync(sceneName);
                yield return Instantiate(playerPrefab, GetDestination(destinationTag).transform.position,GetDestination(destinationTag).transform.rotation);
                //中断携程
                yield break;
            }
            else
            {
                player = GameManager.Instance.playerStats.transform;
                player.SetPositionAndRotation(GetDestination(destinationTag).transform.position, GetDestination(destinationTag).transform.rotation);
                yield return null;
            }     
        }
    
    
        //在全部传送门中查找传送标签一样的传送门
        private TransitionDestination GetDestination(TransitionDestination.DestinationTags destinationTag)
        {
            var entrances = Resources.FindObjectsOfTypeAll<TransitionDestination>();
            for (int i = 0; i < entrances.Length; i++)
            {
                if (entrances[i].destinationTag == destinationTag)
                {
                    return entrances[i];
                }
            }
    
            return null;
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71

    以上代码遇到的问题:

    • 1.切换到新场景后,管理类代码全部消失,导致游戏无法正常运行,解决方法:在Manage相关代码前都加上重写的Awake()方法即可:
    protected override void Awake()
    {
        base.Awake();
        //意为该Manage脚本不会被销毁
        DontDestroyOnLoad(this);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 2.注意在切换至新场景之前,一定要在新场景安置 自己写的 PhotoGrapher 结点,并且在PlayerLogic初始化的时候找到摄像机别赋值完整:
    	void Awake()
        {
            photographer = FindObjectOfType<Photographer>();
            rb = GetComponent<Rigidbody>();
            anim = GetComponent<Animator>();
            characterMove = GetComponent<CharacterMove>();
            photographer.InitCamera(followTarget);
            characterStats = GetComponent<CharacterStats>();
            audio = photographer.GetComponent<AudioSource>();
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 3.这样改完以后从第二场景重新返回第一场景时,会出现两个人物,并且人物在半空中。 (埋个伏笔)

    16.保存数据

    • 1.使用JSON保存游戏数据,在切换场景时调用保存函数与读取函数:
    • 2.新建一个结点 SaveManager并创建脚本 SaveManager.cs挂载到结点上:
    public class SaveManager : Singleton<SaveManager>
    {
        protected override void Awake()
        {
            base.Awake();
            DontDestroyOnLoad(this);
        }
    
        //封装保存和读取玩家信息的函数
        public void SavePlayerData()
        {
            Save(GameManager.Instance.playerStats.characterData, GameManager.Instance.playerStats.characterData.name);
        }
    
        public void LoadPlayerData()
        {
            Load(GameManager.Instance.playerStats.characterData, GameManager.Instance.playerStats.characterData.name);
        }
    
    
        //存储数据
        public void Save(Object data,string key)
        {
            //将要存储的数值转化为JSON
            var jsonData = JsonUtility.ToJson(data,true);
            //将数据以键值对的形式保存
            PlayerPrefs.SetString(key, jsonData);
            PlayerPrefs.Save();
        }
    
        //加载数据
        public void Load(Object data, string key)
        {
            if (PlayerPrefs.HasKey(key))
            {
                JsonUtility.FromJsonOverwrite(PlayerPrefs.GetString(key), data);
            }
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 3.在切换场景时调用这个两个函数即可完成对玩家数据的保存:
      SceneController.cs
    //使用携程异步加载场景
        IEnumerator Transition(string sceneName , TransitionDestination.DestinationTags destinationTag)
        {
            //TODO:保存数据
            SaveManager.Instance.SavePlayerData();
    
            //判断是相同场景还是不同场景
            if (SceneManager.GetActiveScene().name != sceneName)
            {
                //等待return值完成后再执行其他代码(异步加载)
                yield return SceneManager.LoadSceneAsync(sceneName);
                yield return Instantiate(playerPrefab, GetDestination(destinationTag).transform.position,GetDestination(destinationTag).transform.rotation);
                //读取数据
                SaveManager.Instance.LoadPlayerData();
    
                //中断携程
                yield break;
            }
            else
            {
                player = GameManager.Instance.playerStats.transform;
                player.SetPositionAndRotation(GetDestination(destinationTag).transform.position, GetDestination(destinationTag).transform.rotation);
                yield return null;
            }     
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 4.切记勿忘在新场景创建角色UI。

    17.主菜单的制作

    • 1.制作UI,按钮,标题,背景等等
    • 2.分别实现退出游戏,新的游戏和继续游戏的脚本功能:
      – 1.退出游戏
    public class MainManu : MonoBehaviour
    {
        Button quitBtn;
    
        void Awake()
        {
            quitBtn = transform.GetChild(3).GetComponent<Button>();
    
            quitBtn.onClick.AddListener(QuitGame);
        }
    
        void QuitGame()
        {
            Application.Quit();
            Debug.Log("退出游戏");
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    – 2.新的游戏:
    1).清除之前的数据
    2).在游戏控制中添加寻找全图起点的方法:
    GameManager.cs:

     //寻找入口的函数
        public Transform GetEntrance()
        {
            foreach (var item in FindObjectsOfType<TransitionDestination>())
            {
                if (item.destinationTag == TransitionDestination.DestinationTags.ENTRE)
                {
                    return item.transform;
                }
            }
            return null;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    2).切换到第一场景的某点(在场景控制中使用携程切换场景)

    public void TransitionToFirstLevel()
        {
            StartCoroutine(LoadLevel("City"));
        }
    
        IEnumerator LoadLevel(string scene)
        {
            if (scene != "")
            {
                yield return SceneManager.LoadSceneAsync(scene);
                yield return player = Instantiate(playerPrefab, GameManager.Instance.GetEntrance().position, GameManager.Instance.GetEntrance().rotation).transform;
    
                //保存数据
                SaveManager.Instance.SavePlayerData();
                yield break;
            }
            
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    – 3.继续游戏的实现:
    1).在保存函数里加入保存当前地图逻辑

    public class SaveManager : Singleton<SaveManager>
    {
        //当前所在的场景
        private string sceneName = "";
    
        public string SneceName { get { return PlayerPrefs.GetString(sceneName); } }
    
        protected override void Awake()
        {
            base.Awake();
            DontDestroyOnLoad(this);
        }
    
        //封装保存和读取玩家信息的函数
        public void SavePlayerData()
        {
            Save(GameManager.Instance.playerStats.characterData, GameManager.Instance.playerStats.characterData.name);
        }
    
        public void LoadPlayerData()
        {
            Load(GameManager.Instance.playerStats.characterData, GameManager.Instance.playerStats.characterData.name);
        }
    
    
        //存储数据
        public void Save(Object data,string key)
        {
            //将要存储的数值转化为JSON
            var jsonData = JsonUtility.ToJson(data,true);
            //将数据以键值对的形式保存
            PlayerPrefs.SetString(key, jsonData);
            PlayerPrefs.SetString(sceneName, SceneManager.GetActiveScene().name);
            PlayerPrefs.Save();
        }
    
        //加载数据
        public void Load(Object data, string key)
        {
            if (PlayerPrefs.HasKey(key))
            {
                JsonUtility.FromJsonOverwrite(PlayerPrefs.GetString(key), data);
            }
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46

    2).在场景控制里,调用协程:
    SceneController.cs

    public void TransitionToLoadGame()
        {
            StartCoroutine(LoadLevel(SaveManager.Instance.SceneName));
        }
    
    • 1
    • 2
    • 3
    • 4

    3).在玩家生成的生成的时候,就读取一遍玩家数据
    PlayerController.cs

    	void OnEnable()
        {
            //利用单例模式调用注册玩家信息
            GameManager.Instance.RigisterPlayer(characterStats);
        }
    
        void Start()
        {
            SaveManager.Instance.LoadPlayerData();
            //初始化攻击冷却计时器(初始状态可攻击)
            AttackCD = -1;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    4).给予玩家回到Main的方式:
    SceneController.cs:

    	public void TransitionToMain()
        {
            StartCoroutine(LoadMain());
        }
    
    	IEnumerator LoadMain()
        {
            yield return SceneManager.LoadSceneAsync("Main");
            yield break;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    5).添加补全继续游戏的调用函数
    MainMenu.cs:

    	void ContinueGame()
        {
            //读取进度
            SceneController.Instance.TransitionToMain();
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    18.场景转场

    • 1.引入TimeLine窗口:Windows-->Sequencing-->Timeline
  • 相关阅读:
    测开小知识: Git目录下都放了什么
    【Nginx】Nginx实现图片防盗链
    前端页面渲染方式CSR、SSR、SSG
    jmeter做接口压力测试_jmeter接口性能测试
    mongodb学习完整版
    SEMI-SUPERVISED CLASSIFICATION WITH GRAPH CONVOLUTIONAL NETWORKS 论文/GCN学习笔记
    C语言 100道经典编程题适用于专升本,专接本【详细分析版】
    【Python深度学习】Python全栈体系(二十七)
    SQLite 3.37.0 发布,支持严格的字段数据类型
    实验23:lcd1602实验
  • 原文地址:https://blog.csdn.net/qq_55071342/article/details/126477393