第一次写日志还不太会写qaq
第一部分 楔子 在本篇博客中会记录第一次做3D游戏的心路历程以及一定量的代码(有过2D游戏的经验,后续可能会把之前做出来2D的游戏文件上传),成果可能会很粗糙,此项目主要是供做笔记所用
本项目会持续更新
人物跟随鼠标移动 经验引出 首先看看之前写过的2D人物跟随键盘 移动的代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 void Movement (){ float horizontalMove; float facedirection; horizontalMove = Input.GetAxis("Horizontal" ); facedirection = Input.GetAxisRaw("Horizontal" ); rb.velocity = new Vector2(horizontalMove * speed * Time.fixedDeltaTime , rb.velocity.y); anim.SetFloat("running" ,Mathf.Abs(facedirection)); if (facedirection != 0 ) { transform.localScale = new Vector3(facedirection, 1 , 1 ); } Crouch(); }
在Unity初始设置中Horizontal 这个水平轴其实就是X轴,也就是键盘上的AD键或方向箭头 ,当静止时为0,当按下A键时这个数值减小,返回一个小于0的数值,同理,D键为大于0的数值;物体就在X轴方向水平移动。
本代码是通过动态获取horizontalMove的数据来控制人物速度的
本游戏代码实现(自动寻路系统) 前置工作 已经用AI中的Navigation完成bake
先把场景里所有物品全部调为Navigation Static
选择地面,将其Navigation Area改为Walkable,然后bake
选择场景里其他物品,将其Navigation Area改为Not Walkable,然后bake
给Player添加Nav Mesh Agent
如果不想把物品调为Navigation Static ,也可以给物体添加 Nav Mesh Obstacle,再选择Carve进行实时切割(此方法也可用来创建可移动的场景)
MouseManager类 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 using System.Collections;using System.Collections.Generic;using UnityEngine;using UnityEngine.Events;using System;public class MouseManager : MonoBehaviour { public static MouseManager Instance; public Texture2D point, doorway, attack, target, arrow; RaycastHit hitInfo; public event Action<Vector3> OnMouseClicked; private void Awake () { if (Instance != null ) { Destroy(gameObject); } Instance = this ; } void Update () { SetCursorTexture(); MouseControl(); } 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 ; } } } void MouseControl () { if (Input.GetMouseButtonDown(0 ) && hitInfo.collider != null ) { if (hitInfo.collider.gameObject.CompareTag("Ground" )) { OnMouseClicked?.Invoke(hitInfo.point); } } } }
PlayerController类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 using System.Collections;using System.Collections.Generic;using UnityEngine;using UnityEngine.AI;public class PlayerController : MonoBehaviour { private NavMeshAgent agent; private void Awake () { agent = GetComponent<NavMeshAgent>(); } private void Start () { MouseManager.Instance.OnMouseClicked += MoveToTarget; } public void MoveToTarget (Vector3 target ) { agent.destination = target; } }
Post Processing与Shader Graph PostProcessing
功能 相机后处理,画面质感提升
使用方法
层级栏右击可以找到Volume,添加Volume可以进行画质
在Volume脚本下新建配置文件,然后在Add Override中添加自己想要的效果(别忘了在Scene中打开PostProcessing)
用Post processing改变了下镜头质感但依然会有视角遮挡的情况
这就要引出
Shader Graph
功能 通过图标这种直观的方式创建shader
使用方法
右键在URP分类里找到ShaderGraph
创建后双击在Inspector中打开编辑器即可使用
ShaderGraph具体使用方法篇幅过长,之后有机会会补上
建立好自己想要的Shader后附在材质上
在PipelineSetting中UniversalRenderPipelineAsset_Renderer里Add RendererFeature,设置Depth Test为Greater,然后选择材质和图层
再一次Add RendererFeature,选择图层(此步是为了防止自己遮挡自己)
第二部分 敌人
提示:导入项目后正常在edit里面把项目都升级为URP敌人才可正常显示
前置工作
加NavMeshAgent并同Player一样设置遮挡材质
适当改一下MouseManger代码使鼠标移动到身上时变为攻击贴图(这里就不贴代码了,略过)
人物追随敌人 使用协程
在PlayController里面增加两个函数
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 private void EventAttack (GameObject target ){ if (target != null ) { attackTarget = target; StartCoroutine(MoveToAttackTarget()); } } IEnumerator MoveToAttackTarget () { agent.isStopped = false ; transform.LookAt(attackTarget.transform); while (Vector3.Distance(attackTarget.transform.position, transform.position) > 1 ) { agent.destination = attackTarget.transform.position; yield return null ; } agent.isStopped = true ; if (lastAttackTime < 0 ) { anim.SetTrigger("Attack" ); lastAttackTime = 0.5f ; } }
中途改一下相机设置,让玩家可以自己改变摄像机
选择Cinemachine中的freelook
添加Follow和LookAt的物体,改变X和Y的Axis
在Orbits中通过改变Rig改变视角大小,也可以切换Binding Mode来摄像机固定人物的一个方向
追击Player 先是一段EnemyController的代码,也算是基本骨架了
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 using System.Collections;using System.Collections.Generic;using UnityEngine;using UnityEngine.AI;public enum EnemyStates { GUARD , PATROL , CHASE , DEAD }[RequireComponent(typeof(NavMeshAgent)) ] public class EnemyController : MonoBehaviour { private EnemyStates enemyStates; private NavMeshAgent agent; private Animator anim; [Header("Basic Setting" ) ] public bool isGuard; public float sightRadius; private float speed; private GameObject attackTarget; bool isWalk; bool isChase; bool isFollow; private void Awake () { agent = GetComponent<NavMeshAgent>(); speed = agent.speed; anim = GetComponent<Animator>(); } private void Update () { SwitchStates(); SwitchAnimation(); } private void SwitchAnimation () { anim.SetBool("Walk" , isWalk); anim.SetBool("Chase" , isChase); anim.SetBool("Follow" , isFollow); } void SwitchStates () { if (FoundPlayer()) { enemyStates = EnemyStates.CHASE; } switch (enemyStates) { case EnemyStates.GUARD: break ; case EnemyStates.PATROL: break ; case EnemyStates.CHASE: isWalk = false ; isChase = true ; agent.speed = speed; if (!FoundPlayer()) { isFollow = false ; agent.destination = transform.position; } else { isFollow = true ; agent.destination = attackTarget.transform.position; } break ; case EnemyStates.DEAD: break ; } } bool FoundPlayer () { var colliders = Physics.OverlapSphere(transform.position, sightRadius); foreach (var target in colliders) { if (target.CompareTag("Player" )) { attackTarget = target.gameObject; return true ; } } attackTarget = null ; return false ; } }
如果想让监视的范围可视化也可以写下面这个函数
1 2 3 4 5 6 7 private void OnDrawGizmosSelected (){ Gizmos.color = Color.blue; Gizmos.DrawWireSphere(transform.position, sightRadius); }
Patrol Randomly随机巡逻 EnemyController的case Patrol中更新代码大致如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 case EnemyStates.PATROL: isChase = false ; agent.speed = speed * 0.5f ; if (Vector3.Distance(wayPoint, transform.position) <= agent.stoppingDistance) { isWalk = false ; if (remainLookAtTime > 0 ) remainLookAtTime -= Time.deltaTime; else GetNewWayPoint(); } else { isWalk = true ; agent.destination = wayPoint; } break ;
更新新函数
1 2 3 4 5 6 7 8 9 10 11 12 13 void GetNewWayPoint (){ remainLookAtTime = lookAtTime; float randomX = Random.Range(-patrolRange, patrolRange); float randomZ = Random.Range(-patrolRange, patrolRange); Vector3 randomPoint = new Vector3(guardPos.x + randomX,transform.position.y,guardPos.z + randomZ); NavMeshHit hit; wayPoint = NavMesh.SamplePosition(randomPoint, out hit, patrolRange, 1 ) ? hit.position : transform.position; }
数据 使用ScriptObjectable
人物基本属性
1 2 3 4 5 6 7 8 9 10 11 12 13 14 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 int maxHealth; public int currentHealth; public int baseDefence; public int curentDefence; }
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 using System.Collections;using System.Collections.Generic;using UnityEngine;public class CharacterStats : MonoBehaviour { public CharacterData_SO characterData; #region Read from Data_SO public int MaxHealth { get { if (characterData != null ) return characterData.maxHealth; else return 0 ; } set { characterData.maxHealth = value ; } } public int CurrentHealth { get { if (characterData != null ) return characterData.currentHealth; else return 0 ; } set { characterData.currentHealth = value ; } } public int BaseDefence { get { if (characterData != null ) return characterData.baseDefence; else return 0 ; } set { characterData.baseDefence = value ; } } public int CurrentDefence { get { if (characterData != null ) return characterData.curentDefence; else return 0 ; } set { characterData.curentDefence = value ; } } #endregion }
攻击属性 添加Attack_SO类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 using System.Collections;using System.Collections.Generic;using UnityEngine;[CreateAssetMenu (fileName= "New Data" , menuName = "Attack/Attack Data" ) ] public class AttackData_SO : ScriptableObject { public float attackRange; public float skillRange; public float coolDown; public int minDamage; public int maxDamage; public float criticalMutiplier; public float criticalChance; }
在EnemyController中增加三个函数Attack,TargetInAttackRange和TargetInSkillRange
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 void Attack () { transform.LookAt(attackTarget.transform); if (TargetInAttackRange()) { anim.SetTrigger("Attack" ); } else if (TargetInSkillRange()){ anim.SetTrigger("Skill" ); } } bool TargetInAttackRange () { if (attackTarget != null ) return Vector3.Distance(attackTarget.transform.position, transform.position) <= characterStats.attackData.attackRange; else return false ; } bool TargetInSkillRange () { if (attackTarget != null ) return Vector3.Distance(attackTarget.transform.position, transform.position) <= characterStats.attackData.skillRange; else return false ; }
在CharacterData里添加暴击判断,并更新EnemyController的case CHASE中代码如下
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 case EnemyStates.CHASE: isWalk = false ; isChase = true ; agent.speed = speed; if (!FoundPlayer()) { isFollow = false ; if (remainLookAtTime > 0 ) { agent.destination = transform.position; remainLookAtTime -= Time.deltaTime; } else if (isGuard) enemyStates = EnemyStates.GUARD; else enemyStates = EnemyStates.PATROL; } else { isFollow = true ; agent.isStopped = false ; agent.destination = attackTarget.transform.position; } if (TargetInAttackRange() || TargetInSkillRange()) { isFollow = false ; agent.isStopped = true ; if (lastAttackTime < 0 ) { lastAttackTime = characterStats.attackData.coolDown; characterStats.isCritical = Random.value < characterStats.attackData.criticalChance; Attack(); } } break ;
攻击数值计算 在CharacterStats中添加以下代码
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 #region Character Combat public void TakeDamage (CharacterStats attaker , CharacterStats defener ){ int damage = Mathf.Max(attaker.CurrentDamage() - defener.CurrentDefence,0 ); CurrentHealth = Mathf.Max(CurrentHealth - damage, 0 ); } private int CurrentDamage (){ float coreDamage = UnityEngine.Random.Range(attackData.minDamage, attackData.maxDamage); if (isCritical) { coreDamage *= attackData.criticalMutiplier; Debug.Log("暴击" + coreDamage); } return (int )coreDamage; } #endregion
在EnemyController和PlayController中添加hit方法
1 2 3 4 5 6 7 8 9 10 11 void Hit (){ if (attackTarget != null ) { var targetStats = attackTarget.GetComponent<CharacterStats>(); targetStats.TakeDamage(characterStats, targetStats); } }
在PlayController中的Hit方法不用加判断攻击对象是否为空
在动画里添加事件调用Hit函数
守卫状态和死亡状态 守卫状态 EnemyController的switch处加如下代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 case EnemyStates.GUARD: isChase = false ; if (transform.position != guardPos) { isWalk = true ; agent.isStopped = false ; agent.destination = guardPos; if (Vector3.SqrMagnitude(guardPos - transform.position) <= agent.stoppingDistance*agent.stoppingDistance) { isWalk = false ; transform.rotation = Quaternion.Lerp(transform.rotation,guardRotation,0.01f ); } } break ;
死亡状态 其实无论玩家还是怪物死亡就是加动画,这个代码就不贴了,下面贴一个case DEAD把switch那补全
1 2 3 4 5 6 case EnemyStates.DEAD: coll.enabled = false ; agent.enabled = false ; Destroy(gameObject, 2f ); break ;
注意由于2秒后才摧毁物体,所以在这期间需要关掉一些可能会出现bug的组件
动画图层权重的问题,其实就是优先级
在Anystates后连接,会出现一个“过渡到自己的选项”,相当于动画循环,一般死亡动画要关闭
触发器触发的动画回来时一般要勾选有退出时间并设定退出时间为1,这样又能完整播放又不会让动画循环