Package Manager
搜索 Universal RP
进行安装Rendering--->URP Assets(with Universal Renderer)
Project Settings--->Graphics
,选中刚创建的渲染管线Quality
中同样上述操作Shadows-->Max Distance
,以减小渲染时带给显卡的压力更新渲染:
Windows--->Rendering-->Render Pipeline Converter--->Built-in to URP--->全部勾选--->Initialize Converters--->Convert Assets
Input Manager
–>复制粘贴 Horizontal和Vertical 并修改其为第四,第五坐标轴,命名为 Camera Rate X & Camera Rate Y相机脚本:
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);
}
}
人物移动脚本(通用):
[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);
}
玩家移动脚本:
[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
}
切换鼠标指针:
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;
}
}
}
create-->Shader Graph--> URP--->Unit Shader Graph
,并起名为 Occlusion Shader
。Occlusion
放回meterials文件夹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;
}
}
}
MonoBehavior
和ScriptableObject
文件夹。CharacterData_SO
。数据模板写法:
[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;
}
数据操作写法:
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
}
CharacterStats
挂载到Player和Enemy上,并拖入对应数据文件。[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;
}
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;
}
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
}
由于这里攻击函数是玩家和怪物通用的,所以后续只需要传入攻击者和受击者即可正常完成伤害计算并扣血。其次,怪物的进攻逻辑是AI操控,只需要在追踪玩家时,吧玩家对象传入怪物的攻击目标变量即可。但玩家的攻击目标选择却成了问题,这里采用类似怪物发现玩家的方式,采用一个圆形探测范围(该范围阈值即为玩家的攻击范围),当玩家进行攻击操作时,只需要检测怪物是否在范围检测距离内,如果在距离内,则正常调用TakeDamage(),如果不在,则视为空刀。缺点是实际攻击范围(以玩家为圆心,以攻击距离为半径的圆形内)与动画攻击范围(人物的前方扇形区域)不符合。后续有待改进…此外,暴击功能较为繁琐,目前未实现。以下为判断暴击的常用方法之一,仅供参考:
//设置全局变量,判断是否暴击
characterStats.isCritical = UnityEngine.Random.value < characterStats.attackData.criticalChance;
//引入数值组件
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);
}
}
//引入数值组件
//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
//引入变量
bool isDead;
void Update()
{
if (characterStats.CurrentHealth != 0)
{
UpdateMovementInput();
Jump();
AttackAnim();
}
else
{
isDead = true;
rb.constraints = RigidbodyConstraints.FreezePosition;
rb.constraints = RigidbodyConstraints.FreezeRotation;
}
SwitchAnim();
}
怪物死亡:
void Update()
{
if (characterStats.CurrentHealth != 0)
{
SwitchStates();
}
else
{
isDead = true;
enemyStates = EnemyStates.DEAD;
}
SwitchAnimation();
}
//管理类的基类
//泛型单例模式
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;
}
}
}
public interface IEndGameObserver
{
void EndNotify();
}
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();
}
}
}
void Start()
{
//利用单例模式调用注册玩家信息
GameManager.Instance.RigisterPlayer(characterStats);
}
//首先让其实现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;
}
报错总结:此处在GameManager的实例化阶段一直报 空引用异常
,后来发现自己没有创建 Game Manager 的结点,并挂载GameManager的脚本。此外,还需注意 Onable函数是场景初始化时被创建,此处不涉及场景切换,故,需将其暂时写在Start方法中,后续再更改。
public class CharacterStats : MonoBehaviour
{
public CharacterData_SO templateData;
public CharacterData_SO characterData;
void Awake()
{
if (templateData != null)
{
characterData = Instantiate(templateData);
}
}
}
由于之前只要怪物发动攻击动画,玩家必掉血。这是极度不合理的,我们希望玩家有一定的闪避空间或容错,故我们采用 拓展方法 Extension Method
来对其攻击范围做一个限制。
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;
}
}
//怪物攻击的出伤害事件
void Hit()
{
if (AttackTarget != null && transform.IsFacingTarget(AttackTarget.transform))
{
//拿到受害者的属性信息
var targetStats = AttackTarget.GetComponent<CharacterStats>();
//造成伤害
targetStats.TakeDamage(characterStats, targetStats);
}
}
但暂时有点问题,无法正常击退玩家。后续再改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.拖入石头的素材,添加必要组件: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);
}
}
– 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;
}
}
}
– 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;
}
}
}
– 5.函数重载TakeDamage()方法,让石头也能造成伤害:
public void TakeDamage(int damage,CharacterStats defencer)
{
int finalDamage = Mathf.Max(damage - defencer.CurrentDefence, 1);
CurrentHealth = Mathf.Max(CurrentHealth - finalDamage, 0);
}
– 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.创建一个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;
}
}
}
总结:该套程序采用逐帧检测血条变化,相对来说效率较差,优化可以让角色攻击时计算一次血条变化。后续会优化!!!
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);
}
}
暂时为了学习升级逻辑的构思以及数值的模范书写,后续功能有待优化!
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);
}
}
做出如下效果的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;
}
}
场景内传送:
DestinationPoint
作为被传送点。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 ;
}
}
}
TransitionDestination.cs
:public class TransitionDestination : MonoBehaviour
{
public enum DestinationTags
{
ENTRE,A,B,C
}
public DestinationTags destinationTag;
}
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;
}
}
以上代码遇到的问题:
protected override void Awake()
{
base.Awake();
//意为该Manage脚本不会被销毁
DontDestroyOnLoad(this);
}
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>();
}
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);
}
}
}
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;
}
}
public class MainManu : MonoBehaviour
{
Button quitBtn;
void Awake()
{
quitBtn = transform.GetChild(3).GetComponent<Button>();
quitBtn.onClick.AddListener(QuitGame);
}
void QuitGame()
{
Application.Quit();
Debug.Log("退出游戏");
}
}
– 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;
}
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;
}
}
– 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);
}
}
}
2).在场景控制里,调用协程:
SceneController.cs
public void TransitionToLoadGame()
{
StartCoroutine(LoadLevel(SaveManager.Instance.SceneName));
}
3).在玩家生成的生成的时候,就读取一遍玩家数据
PlayerController.cs
void OnEnable()
{
//利用单例模式调用注册玩家信息
GameManager.Instance.RigisterPlayer(characterStats);
}
void Start()
{
SaveManager.Instance.LoadPlayerData();
//初始化攻击冷却计时器(初始状态可攻击)
AttackCD = -1;
}
4).给予玩家回到Main的方式:
SceneController.cs:
public void TransitionToMain()
{
StartCoroutine(LoadMain());
}
IEnumerator LoadMain()
{
yield return SceneManager.LoadSceneAsync("Main");
yield break;
}
5).添加补全继续游戏的调用函数
MainMenu.cs:
void ContinueGame()
{
//读取进度
SceneController.Instance.TransitionToMain();
}
Windows-->Sequencing-->Timeline