Alex教程每一P的教程原代码加上我自己的理解初步理解写的注释,可供学习Alex教程的人参考
此代码仅为较上一P有所改变的代码
【Unity教程】从0编程制作类银河恶魔城游戏_哔哩哔哩_bilibili
CharacterStats.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class CharacterStats : MonoBehaviour
{
private EntityFX fx;
[Header("Major stats")]
public Stat strength; // 力量 增伤1点 爆伤增加 1% 物抗
public Stat agility;// 敏捷 闪避 1% 闪避几率增加 1%
public Stat intelligence;// 1 点 魔法伤害 1点魔抗
public Stat vitality;//加血的
[Header("Offensive stats")]
public Stat damage;
public Stat critChance; // 暴击率
public Stat critPower; //150% 爆伤
[Header("Defensive stats")]
public Stat maxHealth;
public Stat armor;
public Stat evasion;//闪避值
public Stat magicResistance;
[Header("Magic stats")]
public Stat fireDamage;
public Stat iceDamage;
public Stat lightingDamage;
public bool isIgnited; // 持续烧伤
public bool isChilded; // 削弱护甲 20%
public bool isShocked; // 降低敌人命中率
[SerializeField] private float ailmentsDuration = 4;
private float ignitedTimer;
private float chilledTimer;
private float shockedTimer;
private float igniteDamageCooldown = .3f;
private float ignitedDamageTimer;
private int igniteDamage;
public System.Action onHealthChanged;//使角色在Stat里调用UI层的函数
[SerializeField] public int currentHealth;
protected virtual void Start()
{
critPower.SetDefaultValue(150);//设置默认爆伤
currentHealth = GetMaxHealthValue();
fx = GetComponent<EntityFX>();
}
protected virtual void Update()
{
//所有的状态都设置上默认持续时间,持续过了就结束状态
ignitedTimer -= Time.deltaTime;
chilledTimer -= Time.deltaTime;
shockedTimer -= Time.deltaTime;
ignitedDamageTimer -= Time.deltaTime;
if (ignitedTimer < 0)
isIgnited = false;
if (chilledTimer < 0)
isChilded = false;
if (shockedTimer < 0)
isShocked = false;
//被点燃后,出现多段伤害后点燃停止
if (ignitedDamageTimer < 0 && isIgnited)
{
Debug.Log("Take Burning Damage" + igniteDamage);
DecreaseHealthBy(igniteDamage);
if (currentHealth < 0)
Die();
ignitedDamageTimer = igniteDamageCooldown;
}
}
public virtual void DoDamage(CharacterStats _targetStats)//计算后造成伤害函数
{
if (TargetCanAvoidAttack(_targetStats))设置闪避
{
return;
}
int totleDamage = damage.GetValue() + strength.GetValue();
//爆伤设置
if (CanCrit())
{
totleDamage = CalculateCriticalDamage(totleDamage);
}
totleDamage = CheckTargetArmor(_targetStats, totleDamage);//设置防御
//_targetStats.TakeDamage(totleDamage);
DoMagicaDamage(_targetStats);
}
public virtual void DoMagicaDamage(CharacterStats _targetStats)//法伤计算
{
int _fireDamage = fireDamage.GetValue();
int _iceDamage = iceDamage.GetValue();
int _lightingDamage = lightingDamage.GetValue();
int totleMagicalDamage = _fireDamage + _iceDamage + _lightingDamage + intelligence.GetValue();
totleMagicalDamage = CheckTargetResistance(_targetStats, totleMagicalDamage);
_targetStats.TakeDamage(totleMagicalDamage);
//让元素效果取决与伤害
bool canApplyIgnite = _fireDamage > _iceDamage && _fireDamage > _lightingDamage;
bool canApplyChill = _iceDamage > _lightingDamage && _iceDamage > _fireDamage;
bool canApplyShock = _lightingDamage > _fireDamage && _lightingDamage > _iceDamage;
//防止循环在所有元素伤害为0时出现死循环
if (Mathf.Max(_fireDamage, _iceDamage, _lightingDamage) <= 0)
return;
//为了防止出现元素伤害一致而导致无法触发元素效果
//循环判断触发某个元素效果
while (!canApplyIgnite && !canApplyChill && !canApplyShock)
{
if (Random.value < .25f)
{
canApplyIgnite = true;
Debug.Log("Ignited");
_targetStats.ApplyAilments(canApplyIgnite, canApplyChill, canApplyShock);
return;
}
if (Random.value < .35f)
{
canApplyChill = true;
Debug.Log("Chilled");
_targetStats.ApplyAilments(canApplyIgnite, canApplyChill, canApplyShock);
return;
}
if (Random.value < .55f)
{
canApplyShock = true;
Debug.Log("Shocked");
_targetStats.ApplyAilments(canApplyIgnite, canApplyChill, canApplyShock);
return;
}
}
//给点燃伤害赋值
if (canApplyIgnite)
{
_targetStats.SetupIgniteDamage(Mathf.RoundToInt(_fireDamage * .2f));
}
_targetStats.ApplyAilments(canApplyIgnite, canApplyChill, canApplyShock);
}
private static int CheckTargetResistance(CharacterStats _targetStats, int totleMagicalDamage)//法抗计算
{
totleMagicalDamage -= _targetStats.magicResistance.GetValue() + (_targetStats.intelligence.GetValue() * 3);
totleMagicalDamage = Mathf.Clamp(totleMagicalDamage, 0, int.MaxValue);
return totleMagicalDamage;
}
public void ApplyAilments(bool _ignite, bool _chill, bool _shock)//判断异常状态
{
if (isIgnited || isChilded || isShocked)
{
return;
}
if (_ignite)
{
isIgnited = _ignite;
ignitedTimer = ailmentsDuration;
fx.IgniteFxFor(ailmentsDuration);
}
if (_chill)
{
isChilded = _chill;
chilledTimer = ailmentsDuration;
float slowPercentage = .2f;
GetComponent<Entity>().SlowEntityBy(slowPercentage, ailmentsDuration);
fx.ChillFxFor(ailmentsDuration);
}
if (_shock)
{
isShocked = _shock;
shockedTimer = ailmentsDuration;
fx.ShockFxFor(ailmentsDuration);
}
}
public void SetupIgniteDamage(int _damage) => igniteDamage = _damage;//给点燃伤害赋值
protected virtual void TakeDamage(int _damage)//造成伤害是出特效
{
DecreaseHealthBy(_damage);
if (currentHealth < 0)
Die();
}
protected virtual void DecreaseHealthBy(int _damage)//此函数用来改变当前声明值,不调用特效
{
currentHealth -= _damage;
if(onHealthChanged != null)
{
onHealthChanged();
}
}
protected virtual void Die()
{
}
private static int CheckTargetArmor(CharacterStats _targetStats, int totleDamage)//设置防御
{
//被冰冻后,角色护甲减少
if (_targetStats.isChilded)
totleDamage -= Mathf.RoundToInt(_targetStats.armor.GetValue() * .8f);
else
totleDamage -= _targetStats.armor.GetValue();
totleDamage = Mathf.Clamp(totleDamage, 0, int.MaxValue);
return totleDamage;
}
private bool TargetCanAvoidAttack(CharacterStats _targetStats)//设置闪避
{
int totleEvation = _targetStats.evasion.GetValue() + _targetStats.agility.GetValue();
//我被麻痹后
//敌人的闪避率提升
if (isShocked)
totleEvation += 20;
if (Random.Range(0, 100) < totleEvation)
{
return true;
}
return false;
}
private bool CanCrit()//判断是否暴击
{
int totleCriticalChance = critChance.GetValue() + agility.GetValue();
if (Random.Range(0, 100) <= totleCriticalChance)
{
return true;
}
return false;
}
private int CalculateCriticalDamage(int _damage)//计算暴击后伤害
{
float totleCirticalPower = (critPower.GetValue() + strength.GetValue()) * .01f;
float critDamage = _damage * totleCirticalPower;
return Mathf.RoundToInt(critDamage);//返回舍入为最近整数的
}
public int GetMaxHealthValue()
{
return maxHealth.GetValue() + vitality.GetValue() * 10;
}//统计生命值函数
}
Entity.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Entity : MonoBehaviour
{
[Header("Knockback info")]
[SerializeField] protected Vector2 knockbackDirection;//被击打后的速度信息
[SerializeField] protected float knockbackDuration;//被击打的时间
protected bool isKnocked;//此值通过卡住SetVelocity函数的方式用来阻止当一个角色被攻击时,会乱动的情况
[Header("Collision Info")]
public Transform attackCheck;//transform类,代表的时物体的位置,用来控制攻击检测的位置
public float attackCheckRadius;//检测半径
[SerializeField] protected Transform groundCheck;//transform类,代表的时物体的位置,后面会来定位子组件的位置
[SerializeField] protected float groundCheckDistance;
[SerializeField] protected Transform wallCheck;//transform类,代表的时物体的位置,后面会来定位子组件的位置
[SerializeField] protected float wallCheckDistance;
[SerializeField] protected LayerMask whatIsGround;//LayerMask类,与Raycast配合,https://docs.unity3d.com/cn/current/ScriptReference/Physics.Raycast.html
#region 定义Unity组件
public SpriteRenderer sr { get; private set; }
public Animator anim { get; private set; }//这样才能配合着拿到自己身上的animator的控制权
public Rigidbody2D rb { get; private set; }//配合拿到身上的Rigidbody2D组件控制权
public EntityFX fx { get; private set; }//拿到EntityFX
public CharacterStats stats { get; private set; }
public CapsuleCollider2D cd { get; private set; }
#endregion
public int facingDir { get; private set; } = 1;
protected bool facingRight = true;//判断是否朝右
public System.Action onFlipped;//一个自身不用写函数,只是接受其他函数并调用他们的函数
//https://blog.csdn.net/weixin_44299531/article/details/131343583
protected virtual void Awake()
{
}
protected virtual void Start()
{
anim = GetComponentInChildren<Animator>();//拿到自己子组件身上的animator的控制权
sr = GetComponentInChildren<SpriteRenderer>();
fx = GetComponent<EntityFX>();拿到的组件上的EntityFX控制权
rb = GetComponent<Rigidbody2D>();
stats = GetComponent<CharacterStats>();
cd = GetComponent<CapsuleCollider2D>();
}
protected virtual void Update()
{
}
protected virtual void Exit()
{
}
public virtual void DamageEffect()
{
fx.StartCoroutine("FlashFX");//IEnumertor本质就是将一个函数分块执行,只有满足某些条件才能执行下一段代码,此函数有StartCoroutine调用
//https://www.zhihu.com/tardis/bd/art/504607545?source_id=1001
StartCoroutine("HitKnockback");//调用被击打后产生后退效果的函数
Debug.Log(gameObject.name+"was damaged");
}
public virtual void SlowEntityBy(float _slowPercentage,float flowDuration)//减缓一切速度函数
{
}
protected virtual void ReturnDefaultSpeed()//动画速度恢复正常函数
{
anim.speed = 1;
}
protected virtual IEnumerator HitKnockback()
{
isKnocked = true;//此值通过卡住SetVelocity函数的方式用来阻止当一个角色被攻击时,会乱动的情况
rb.velocity = new Vector2(knockbackDirection.x * -facingDir, knockbackDirection.y);
yield return new WaitForSeconds(knockbackDuration);
isKnocked = false;
}
//被击打后产生后退效果的函数
#region 速度函数Velocity
public virtual void SetZeroVelocity()
{
if(isKnocked)
{
return;
}
rb.velocity = new Vector2(0, 0);
}//设置速度为0函数
public virtual void SetVelocity(float _xVelocity, float _yVelocity)
{
if(isKnocked)
return;此值通过卡住SetVelocity函数的方式用来阻止当一个角色被攻击时,会乱动的情况
rb.velocity = new Vector2(_xVelocity, _yVelocity);//将rb的velocity属性设置为对应的想要的二维向量。因为2D游戏的速度就是二维向量
FlipController(_xVelocity);//在其他设置速度的时候调用翻转控制器
}//控制速度的函数,此函数在其他State中可能会使用,但仅能通过player.SeVelocity调用
#endregion
#region 翻转函数Flip
public virtual void Flip()
{
facingDir = facingDir * -1;
facingRight = !facingRight;
transform.Rotate(0, 180, 0);//旋转函数,transform不需要额外定义,因为他是自带的
if(onFlipped != null)
onFlipped();
}//翻转函数
public virtual void FlipController(float _x)//目前设置x,目的时能在空中时也能转身
{
if (_x > 0 && !facingRight)//当速度大于0且没有朝右时,翻转
{
Flip();
}
else if (_x < 0 && facingRight)
{
Flip();
}
}
#endregion
#region 碰撞函数Collision
public virtual bool IsGroundDetected()
{
return Physics2D.Raycast(groundCheck.position, Vector2.down, groundCheckDistance, whatIsGround);
}//通过RayCast检测是否挨着地面,https://docs.unity3d.com/cn/current/ScriptReference/Physics2D.Raycast.html
//xxxxxxxx() => xxxxxxxx == xxxxxxxxxx() return xxxxxxxxx;
public virtual bool IsWallDetected()
{
return Physics2D.Raycast(wallCheck.position, Vector2.right * facingDir, wallCheckDistance, whatIsGround);
}//通过RayCast检测是否挨着地面,https://docs.unity3d.com/cn/current/ScriptReference/Physics2D.Raycast.html
//xxxxxxxx() => xxxxxxxx == xxxxxxxxxx() return xxxxxxxxx;
protected virtual void OnDrawGizmos()
{
Gizmos.DrawLine(groundCheck.position, new Vector3(groundCheck.position.x, groundCheck.position.y - groundCheckDistance));//绘制一条从 from(前面的) 开始到 to(后面的) 的线。
Gizmos.DrawLine(wallCheck.position, new Vector3(wallCheck.position.x + wallCheckDistance, wallCheck.position.y));//绘制一条从 from(前面的) 开始到 to(后面的) 的线。
Gizmos.DrawWireSphere(attackCheck.position, attackCheckRadius);//https://docs.unity3d.com/2022.3/Documentation/ScriptReference/Gizmos.DrawWireSphere.html
//绘制具有中心和半径的线框球体。
}//画图函数
#endregion
public void MakeTransprent(bool isClear)
{
if (isClear)
sr.color = Color.clear;
else
sr.color = Color.white;
}
public virtual void Die()
{
}
}
Player.cs
using System.Collections;
using System.Collections.Generic;
using Unity.VisualScripting;
using UnityEngine;
public class Enemy : Entity
{
[SerializeField] protected LayerMask whatIsPlayer;
[Header("Stun Info")]
public float stunnedDuration;//stunned持续时间
public Vector2 stunnedDirection;//stunned改变后的速度
protected bool canBeStunned;//判断是否可以被反击
[SerializeField] protected GameObject counterImage;//一个代表着是否可以被反击的信号
[Header("Move Info")]
public float moveSpeed;
public float idleTime;
public float battleTime;//多久能从battle状态中退出来
private float defaultMoveSpeed;
[Header("Attack Info")]
public float attackDistance;
public float attackCooldown;//攻击冷却
[HideInInspector] public float lastTimeAttacked;//最后一次攻击的时间
#region 类
public EnemyStateMachine stateMachine { get; private set; }
public string lastingAnimBoolName{ get; private set; }
public virtual void AssignLastAnimName(string _animBoolName)
{
lastingAnimBoolName = _animBoolName;
}
#endregion
protected override void Awake()
{
base.Awake();
stateMachine = new EnemyStateMachine();
defaultMoveSpeed = moveSpeed;
}
protected override void Start()
{
base.Start();
}
protected override void Update()
{
base.Update();
stateMachine.currentState.Update();
//Debug.Log(IsPlayerDetected().collider.gameObject.name + "I see");//这串代码会报错,可能使版本的物体,因为在没有找到Player的时候物体是空的,NULL,你想让他在控制台上显示就报错了
}
public override void SlowEntityBy(float _slowPercentage, float _slowDuration)
{
base.SlowEntityBy(_slowPercentage, _slowDuration);
moveSpeed = moveSpeed * (1 - _slowPercentage);
anim.speed = anim.speed * (1 - _slowPercentage);
Invoke("ReturnDefaultSpeed", _slowDuration);
}
protected override void ReturnDefaultSpeed()
{
base.ReturnDefaultSpeed();
moveSpeed = defaultMoveSpeed;
}
public virtual void FreezeTime(bool _timeFrozen)
{
if(_timeFrozen)
{
moveSpeed = 0;
anim.speed = 0;
}
else
{
moveSpeed = defaultMoveSpeed;
anim.speed = 1;
}
}
protected virtual IEnumerator FreezeTimeFor(float _seconds)
{
FreezeTime(true);
yield return new WaitForSeconds(_seconds);
FreezeTime(false);
}
#region Counter Attack Window
public virtual void OpenCounterAttackWindow()//打开可以反击的信号的函数
{
canBeStunned = true;
counterImage.SetActive(true);
}
public virtual void CloseCounterAttackWindow()//关闭可以反击的信号的函数
{
canBeStunned = false;
counterImage.SetActive(false);
}
#endregion
public virtual bool CanBeStunned()//这里的主要目的是完成在被反击过后能立刻关闭提示窗口
{
if (canBeStunned)
{
CloseCounterAttackWindow();
return true;
}
return false;
}
public virtual void AnimationFinishTrigger() => stateMachine.currentState.AnimationFinishTrigger();//动画完成时调用的函数,与Player相同
public virtual RaycastHit2D IsPlayerDetected() => Physics2D.Raycast(wallCheck.position, Vector2.right * facingDir, 7, whatIsPlayer);//用于从射线投射获取信息的结构。
//该函数的返回值可以变,可以只返回bool,也可以是碰到的结构
protected override void OnDrawGizmos()
{
base.OnDrawGizmos();
Gizmos.color = Color.yellow;//把线改成黄色
Gizmos.DrawLine(transform.position, new Vector3(transform.position.x + attackDistance * facingDir, transform.position.y));//用来判别是否进入attackState的线
}
}
Enemy.cs
using System.Collections;
using System.Collections.Generic;
using Unity.VisualScripting;
using UnityEngine;
public class Player : Entity
{
[Header("Attack Details")]
public Vector2[] attackMovement;//每个攻击时获得的速度组
public float counterAttackDuration = .2f;
public bool isBusy{ get; private set; }//防止在攻击间隔中进入move
//
[Header("Move Info")]
public float moveSpeed;//定义速度,与xInput相乘控制速度的大小
public float jumpForce;
public float swordReturnImpact;//在player里设置swordReturnImpact作为击退的参数
private float defaultMoveSpeed;
private float defaultJumpForce;
[Header("Dash Info")]
[SerializeField] private float dashCooldown;
private float dashUsageTimer;//为dash设置冷却时间,在一定时间内不能连续使用
public float dashSpeed;//冲刺速度
public float dashDuration;//持续时间
private float defaultDashSpeed;
public float dashDir { get; private set; }
#region 定义States
public PlayerStateMachine stateMachine { get; private set; }
public PlayerIdleState idleState { get; private set; }
public PlayerMoveState moveState { get; private set; }
public PlayerJumpState jumpState { get; private set; }
public PlayerAirState airState { get; private set; }
public PlayerDashState dashState { get; private set; }
public PlayerWallSlideState wallSlide { get; private set; }
public PlayerWallJumpState wallJump { get; private set; }
public PlayerDeadState deadState { get; private set; }
public PlayerPrimaryAttackState primaryAttack { get; private set; }
public PlayerCounterAttackState counterAttack { get; private set; }
public PlayerAimSwordState aimSword { get; private set; }
public PlayerCatchSwordState catchSword { get; private set; }
public PlayerBlackholeState blackhole { get; private set; }
public SkillManager skill { get; private set; }
public GameObject sword{ get; private set; }//声明sword
#endregion
protected override void Awake()
{
base.Awake();
stateMachine = new PlayerStateMachine();
//通过构造函数,在构造时传递信息
idleState = new PlayerIdleState(this, stateMachine, "Idle");
moveState = new PlayerMoveState(this, stateMachine, "Move");
jumpState = new PlayerJumpState(this, stateMachine, "Jump");
airState = new PlayerAirState(this, stateMachine, "Jump");
dashState = new PlayerDashState(this, stateMachine, "Dash");
wallSlide = new PlayerWallSlideState(this, stateMachine, "WallSlide");
wallJump = new PlayerWallJumpState(this, stateMachine, "Jump");//wallJump也是Jump动画
deadState = new PlayerDeadState(this, stateMachine, "Die");
primaryAttack = new PlayerPrimaryAttackState(this, stateMachine, "Attack");
counterAttack = new PlayerCounterAttackState(this, stateMachine, "CounterAttack");
aimSword = new PlayerAimSwordState(this,stateMachine, "AimSword");
catchSword = new PlayerCatchSwordState(this, stateMachine, "CatchSword");
blackhole = new PlayerBlackholeState(this, stateMachine, "Jump");
//this 就是 Player这个类本身
}//Awake初始化所以State,为所有State传入各自独有的参数,及animBool,以判断是否调用此动画(与animatoin配合完成)
protected override void Start()
{
base.Start();
stateMachine.Initialize(idleState);
skill = SkillManager.instance;
defaultMoveSpeed = moveSpeed;
defaultJumpForce = jumpForce;
defaultDashSpeed = dashSpeed;
}
protected override void Update()//在mano中update会自动刷新但其他没有mano的不会故,需要在这个updata中调用其他脚本中的函数stateMachine.currentState.update以实现 //stateMachine中的update
{
base.Update();
stateMachine.currentState.Update();//反复调用CurrentState的Update函数
CheckForDashInput();
if(Input.GetKeyDown(KeyCode.F))
{
skill.crystal.CanUseSkill();
}
}
public override void SlowEntityBy(float _slowPercentage, float flowDuration)//减缓一切速度函数
{
base.SlowEntityBy(_slowPercentage, flowDuration);
moveSpeed = moveSpeed * (1 - _slowPercentage);
jumpForce = jumpForce * (1 - _slowPercentage);
dashSpeed = dashSpeed * (1 - _slowPercentage);
anim.speed = anim.speed * (1 - _slowPercentage);
Invoke("ReturnDefaultSpeed", flowDuration);
}
protected override void ReturnDefaultSpeed()//全部速度恢复正常函数
{
base.ReturnDefaultSpeed();
moveSpeed = defaultMoveSpeed;
jumpForce = defaultJumpForce;
dashSpeed = defaultDashSpeed;
}
public void AssignNewSword(GameObject _newSword)//保持创造的sword实例的函数
{
sword = _newSword;
}
public void CatchTheSword()//通过player的CatchTheSword进入,及当剑消失的瞬间进入
{
stateMachine.ChangeState(catchSword);
Destroy(sword);
}
public IEnumerator BusyFor(float _seconds)//https://www.zhihu.com/tardis/bd/art/504607545?source_id=1001
{
isBusy = true;
yield return new WaitForSeconds(_seconds);
isBusy = false;
}//p39 4.防止在攻击间隔中进入move,通过设置busy值,在使用某些状态时,使其为busy为true,抑制其进入其他state
//IEnumertor本质就是将一个函数分块执行,只有满足某些条件才能执行下一段代码,此函数有StartCoroutine调用
public void AnimationTrigger() => stateMachine.currentState.AnimationFinishTrigger();
//从当前状态拿到AnimationTrigger进行调用的函数
public void CheckForDashInput()
{
if (IsWallDetected())
{
return;
}//修复在wallslide可以dash的BUG
if (Input.GetKeyDown(KeyCode.LeftShift) && skill.dash.CanUseSkill())//将DashTimer<0 的判断 改成DashSkill里的判断
{
dashDir = Input.GetAxisRaw("Horizontal");//设置一个值,可以将dash的方向改为你想要的方向而不是你的朝向
if (dashDir == 0)
{
dashDir = facingDir;//只有当玩家没有控制方向时才使用默认朝向
}
stateMachine.ChangeState(dashState);
}
}//将Dash切换设置成一个函数,使其在所以情况下都能使用
public override void Die()
{
base.Die();
stateMachine.ChangeState(deadState);
}
}