Alex教程每一P的教程原代码加上我自己的理解初步理解写的注释,可供学习Alex教程的人参考
此代码仅为较上一P有所改变的代码
【Unity教程】从0编程制作类银河恶魔城游戏_哔哩哔哩_bilibili
Entity.cs
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 DamageImpact()
{
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 virtual void Die()
{
}
}
EntityFX.cs
cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class EntityFX : MonoBehaviour
{
private SpriteRenderer sr;//定义SR组件来保持要用的组件
[Header("Flash FX")]
[SerializeField] private Material hitMat;//要改成的材料
[SerializeField] private float flashDuration;//闪光的时间
private Material originalMat;//原来的材料
[Header("Aliment colors")]
[SerializeField] private Color[] chillColor;
[SerializeField] private Color[] igniteColor;
[SerializeField] private Color[] shockColor;
private void Start()
{
sr = GetComponentInChildren<SpriteRenderer>();//从子组件中拿到SR组件
originalMat = sr.material;//拿到原来的材料
}
public void MakeTransprent(bool isClear)
{
if (isClear)
sr.color = Color.clear;
else
sr.color = Color.white;
}
private IEnumerator FlashFX()//被打后该触发的函数
{
sr.material = hitMat;
//修复在元素效果期间击中,颜色变红的情况
Color currentColor = sr.color;
sr.color = Color.white;
yield return new WaitForSeconds(flashDuration);
sr.color = currentColor;
sr.material = originalMat;
} //IEnumertor本质就是将一个函数分块执行,只有满足某些条件才能执行下一段代码,此函数有StartCoroutine调用
//https://www.zhihu.com/tardis/bd/art/504607545?source_id=1001
private void RedColorBlink()//使角色闪烁的函数
{
if (sr.color != Color.white)
{
sr.color = Color.white;
}
else
{
sr.color = Color.red;
}
}
private void CancelColorChange()//使角色停止闪烁的函数
{
CancelInvoke();//取消该 MonoBehaviour 上的所有 Invoke 调用。
//https://docs.unity3d.com/cn/current/ScriptReference/MonoBehaviour.CancelInvoke.html
sr.color = Color.white;
}
public void ShockFxFor(float _second)
{
InvokeRepeating("ShockColorFx", 0, .3f);
Invoke("CancelColorChange", _second);
}
public void ChillFxFor(float _second)
{
InvokeRepeating("ChillColor", 0, .3f);
Invoke("CancelColorChange", _second);
}
public void IgniteFxFor(float _second)
{
InvokeRepeating("IgniteColorFX", 0, .3f);
Invoke("CancelColorChange", _second);
}
private void IgniteColorFX()
{
if (sr.color != igniteColor[0])
sr.color = igniteColor[0];
else
sr.color = igniteColor[1];
}
private void ShockColorFx()
{
if (sr.color != shockColor[0])
sr.color = shockColor[0];
else
sr.color = shockColor[1];
}
private void ChillColor()
{
if (sr.color != chillColor[0])
sr.color = chillColor[0];
else
sr.color = chillColor[1];
}
}
CharacterStats.cs
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;
[SerializeField] private GameObject shockStrikePrefab;
private int shockDamage;
public System.Action onHealthChanged;//使角色在Stat里调用UI层的函数
protected bool isDead;
[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(isIgnited)
ApplyIgnitedDamage();
}
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);
}
protected virtual void Die()
{
isDead = true;
}
public virtual void TakeDamage(int _damage)//造成伤害是出特效
{
fx.StartCoroutine("FlashFX");//IEnumertor本质就是将一个函数分块执行,只有满足某些条件才能执行下一段代码,此函数有StartCoroutine调用
//https://www.zhihu.com/tardis/bd/art/504607545?source_id=1001
DecreaseHealthBy(_damage);
GetComponent<Entity>().DamageImpact();
if (currentHealth < 0 && !isDead)
Die();
}
protected virtual void DecreaseHealthBy(int _damage)//此函数用来改变当前生命值,不调用特效
{
currentHealth -= _damage;
if (onHealthChanged != null)
{
onHealthChanged();
}
}
#region Magical damage and ailements
private void ApplyIgnitedDamage()
{
if (ignitedDamageTimer < 0 )
{
DecreaseHealthBy(igniteDamage);
if (currentHealth < 0 && !isDead)
Die();
ignitedDamageTimer = igniteDamageCooldown;
}
}被点燃后,出现多段伤害后点燃停止
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);
//防止循环在所有元素伤害为0时出现死循环
if (Mathf.Max(_fireDamage, _iceDamage, _lightingDamage) <= 0)
return;
//让元素效果取决与伤害
//为了防止出现元素伤害一致而导致无法触发元素效果
//循环判断触发某个元素效果
AttemptyToApplyAilement(_targetStats, _fireDamage, _iceDamage, _lightingDamage);
}
private void AttemptyToApplyAilement(CharacterStats _targetStats, int _fireDamage, int _iceDamage, int _lightingDamage)
{
bool canApplyIgnite = _fireDamage > _iceDamage && _fireDamage > _lightingDamage;
bool canApplyChill = _iceDamage > _lightingDamage && _iceDamage > _fireDamage;
bool canApplyShock = _lightingDamage > _fireDamage && _lightingDamage > _iceDamage;
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));
}
if (canApplyShock)
_targetStats.SetupShockStrikeDamage(Mathf.RoundToInt(_lightingDamage * .1f));
//给点燃伤害赋值
_targetStats.ApplyAilments(canApplyIgnite, canApplyChill, canApplyShock);
}//造成元素效果
public void ApplyAilments(bool _ignite, bool _chill, bool _shock)//判断异常状态
{
bool canApplyIgnite = !isIgnited && !isChilded && !isShocked;
bool canApplyChill = !isIgnited && !isChilded && !isShocked;
bool canApplyShock = !isIgnited && !isChilded;
//使当isShock为真时Shock里的函数仍然可以调用
if (_ignite && canApplyIgnite)
{
isIgnited = _ignite;
ignitedTimer = ailmentsDuration;
fx.IgniteFxFor(ailmentsDuration);
}
if (_chill && canApplyChill)
{
isChilded = _chill;
chilledTimer = ailmentsDuration;
float slowPercentage = .2f;
GetComponent<Entity>().SlowEntityBy(slowPercentage, ailmentsDuration);
fx.ChillFxFor(ailmentsDuration);
}
if (_shock && canApplyShock)
{
if(!isShocked)
{
ApplyShock(_shock);
}
else
{
if (GetComponent<Player>() != null)//防止出现敌人使玩家进入shock状态后也出现闪电
return;
HitNearestTargetWithShockStrike();
}//isShock为真时反复执行的函数为寻找最近的敌人,创建闪电实例并传入数据
}
}
public void ApplyShock(bool _shock)
{
if (isShocked)
return;
isShocked = _shock;
shockedTimer = ailmentsDuration;
fx.ShockFxFor(ailmentsDuration);
}//触电变色效果
private void HitNearestTargetWithShockStrike()
{
Collider2D[] colliders = Physics2D.OverlapCircleAll(transform.position, 25);//找到环绕自己的所有碰撞器
float closestDistance = Mathf.Infinity;//正无穷大的表示形式(只读)
Transform closestEnemy = null;
//https://docs.unity3d.com/cn/current/ScriptReference/Mathf.Infinity.html
foreach (var hit in colliders)
{
if (hit.GetComponent<Enemy>() != null && Vector2.Distance(transform.position, hit.transform.position) > 1)// 防止最近的敌人就是Shock状态敌人自己
{
float distanceToEnemy = Vector2.Distance(transform.position, hit.transform.position);//拿到与敌人之间的距离
if (distanceToEnemy < closestDistance)//比较距离,如果离得更近,保存这个敌人的位置,更改最近距离
{
closestDistance = distanceToEnemy;
closestEnemy = hit.transform;
}
}
if (closestEnemy == null)
closestEnemy = transform;
}
if (closestEnemy != null)
{
GameObject newShockStrike = Instantiate(shockStrikePrefab, transform.position, Quaternion.identity);
newShockStrike.GetComponent<ShockStrike_Controller>().Setup(shockDamage, closestEnemy.GetComponent<CharacterStats>());
}
}//给最近的敌人以雷劈
public void SetupIgniteDamage(int _damage) => igniteDamage = _damage;//给点燃伤害赋值
public void SetupShockStrikeDamage(int _damage) => shockDamage = _damage;//雷电伤害赋值
#endregion
#region Stat calculations
private int CheckTargetResistance(CharacterStats _targetStats, int totleMagicalDamage)//法抗计算
{
totleMagicalDamage -= _targetStats.magicResistance.GetValue() + (_targetStats.intelligence.GetValue() * 3);
totleMagicalDamage = Mathf.Clamp(totleMagicalDamage, 0, int.MaxValue);
return totleMagicalDamage;
}
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;
}//统计生命值函数
#endregion
}
PlayerStat.cs
cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class PlayerStat : CharacterStats
{
private Player player;
protected override void Start()
{
player = GetComponent<Player>();
base.Start();
}
public override void DoDamage(CharacterStats _targetStats)
{
base.DoDamage(_targetStats);
}
public override void TakeDamage(int _damage)
{
base.TakeDamage(_damage);
}
protected override void Die()
{
base.Die();
player.Die();
}
}
EnemyStat.cs
cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class EnemyStat : CharacterStats
{
private Enemy enemy;
public override void DoDamage(CharacterStats _targetStats)
{
base.DoDamage(_targetStats);
}
protected override void Die()
{
base.Die();
enemy.Die();
}
protected override void Start()
{
enemy = GetComponent<Enemy>();
base.Start();
}
public override void TakeDamage(int _damage)
{
base.TakeDamage(_damage);
}
}
Sword_Skill_Controller.cs
cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Sword_Skill_Controller : MonoBehaviour
{
private float returnSpeed;
private bool isReturning;
private Animator anim;
private Rigidbody2D rb;
private CircleCollider2D cd;
private Player player;
private bool canRotate = true;
private float freezeTimeDuration;
[Header("Piece info")]
[SerializeField] float pierceAmount;
[Header("Bounce info")]
private float bounceSpeed;//设置弹跳速度
private bool isBouncing;//判断是否可以弹跳
private int bounceAmount;//弹跳的次数
public List<Transform> enemyTargets;//保存所有在剑范围内的敌人的列表
private int targetIndex;//设置targetIndex作为敌人计数器
[Header("Spin info")]
private float maxTravelDistance;//最大攻击距离
private float spinDuration;//持续时间
private float spinTimer;//计时器
private bool wasStopped;//是否停止
private bool isSpinning;
private float hitTimer;
private float hitColldown;
private float spinDirection;//用来不断推进剑在x轴上移动的值
private void Awake()
{
anim = GetComponentInChildren<Animator>();
rb = GetComponent<Rigidbody2D>();
cd = GetComponent<CircleCollider2D>();
}
private void DestroyMe()
{
Destroy(gameObject);
}
public void ReturnSword()
{
rb.constraints = RigidbodyConstraints2D.FreezeAll;//修复剑只要不触碰到物体就无法收回的bug
//rb.isKinematic = false;
transform.parent = null;
isReturning = true;
}
public void SetupSword(Vector2 _dir,float _gravityScale,Player _player,float _freezeTimeDuration,float _returnSpeed)
{
player = _player;
rb.velocity = _dir;
rb.gravityScale = _gravityScale;
freezeTimeDuration = _freezeTimeDuration;
returnSpeed = _returnSpeed;
if(pierceAmount <= 0)
anim.SetBool("Rotation", true); //其次在pierceAmount > 0时不启动旋转动画
spinDirection = Mathf.Clamp(rb.velocity.x, -1, 1);用来不断推进剑在x轴上移动的值
//Mathf.Clamp,当剑使向右飞的时候,direction为1,反之为-1
//https://docs.unity3d.com/cn/current/ScriptReference/Mathf.Clamp.html
Invoke("DestroyMe",7);//在一定时间后自动销毁剑
}
public void SetupBounce(bool _isBouncing,int _bounceAmount,float _bounceSpeed)
{
isBouncing = _isBouncing;
bounceAmount = _bounceAmount;
bounceSpeed = _bounceSpeed;
}
public void SetupPierce(int _pierceAomunt)
{
pierceAmount = _pierceAomunt;
}
public void SetupSpin(bool _isSpinning, float _maxTravelDistance,float _spinDuration,float _hitCooldown)
{
isSpinning = _isSpinning;
maxTravelDistance = _maxTravelDistance;
spinDuration = _spinDuration;
hitColldown = _hitCooldown;
}
private void Update()
{
if (canRotate)
transform.right = rb.velocity;//使武器随着速度而改变朝向
if (isReturning)
{
transform.position = Vector2.MoveTowards(transform.position, player.transform.position, returnSpeed * Time.deltaTime);//从原来的位置返回到player的位置
//并且随着时间增加而增加速度
if (Vector2.Distance(transform.position, player.transform.position) < 1)//当剑与player的距离小于一定距离,清除剑
{
player.CatchTheSword();
}
}
BounceLogic();
SpinLogic();
}
private void SpinLogic()
{
if (isSpinning)//首先当isSpining为true才可进入
{
if (Vector2.Distance(player.transform.position, transform.position) > maxTravelDistance && !wasStopped)//当剑与角色到达最大攻击距离,stop为true,停止剑运动,给剑一个倒计时
{
StopWhenSpinning();
}
if (wasStopped)//当stop为true,倒计时过了,回归角色
{
spinTimer -= Time.deltaTime; //spinDirection一直是1或者-1,但position是变化的
transform.position = Vector2.MoveTowards(transform.position, new Vector2(transform.position.x + spinDirection, transform.position.y), 1.5f * Time.deltaTime);//让剑在x轴上移动的函数
if (spinTimer < 0)
{
isReturning = true;
isSpinning = false;
}
hitTimer -= Time.deltaTime;
if (hitTimer < 0)
{
hitTimer = hitColldown;
Collider2D[] colliders = Physics2D.OverlapCircleAll(transform.position, 1);
foreach (var hit in colliders)
{
if (hit.GetComponent<Enemy>() != null)
{
hit.GetComponent<Enemy>().DamageImpact();
}
}
}
}
}
}
private void StopWhenSpinning()
{
wasStopped = true;
rb.constraints = RigidbodyConstraints2D.FreezeAll;
spinTimer = spinDuration;
}
private void BounceLogic()
{
if (isBouncing && enemyTargets.Count > 0)
{
transform.position = Vector2.MoveTowards(transform.position, enemyTargets[targetIndex].position, bounceSpeed * Time.deltaTime);
//3.当可以弹跳且列表内数量大于0,调用ToWords,这将使剑能够跳到敌人身上
if (Vector2.Distance(transform.position, enemyTargets[targetIndex].position) < .1f)
{
enemyTargets[targetIndex].GetComponent<Enemy>().DamageImpact();
targetIndex++;
bounceAmount--;//设置弹跳次数
if (bounceAmount <= 0)
{
isBouncing = false;
isReturning = true;
}//这样会使当弹跳次数为0时,返回到角色手中
if (targetIndex >= enemyTargets.Count)
{
targetIndex = 0;
}
}//利用与目标距离的判断,使剑接近目标距离时切换到下一个目标。
//且如果所有目标都跳完了,切回第一个
}
}
private void OnTriggerEnter2D(Collider2D collision)//传入碰到的物体的碰撞器
{
if (isReturning)
{
return;
}//修复在返回时扔出时没有碰到任何物体,但返回时碰到了物体导致剑的一些性质发生改变的问题,及回来的时候调用了OnTriggerEnter2D
if(collision.GetComponent<Enemy>()!=null)
{
Enemy enemy = collision.GetComponent<Enemy>();
SwordSkillDamage(enemy);
}
SetupTargetsForBounce(collision);
StuckInto(collision);
}//打开IsTrigger时才可使用该函数
private void SwordSkillDamage(Enemy enemy)
{
player.stats.DoDamage(enemy.GetComponent<CharacterStats>());
enemy.StartCoroutine("FreezeTimeFor", freezeTimeDuration);
}//造成伤害函数
private void SetupTargetsForBounce(Collider2D collision)
{
if (collision.GetComponent<Enemy>() != null)//首先得碰到敌人,只有碰到敌人才会跳
{
if (isBouncing && enemyTargets.Count <= 0)
{
Collider2D[] colliders = Physics2D.OverlapCircleAll(transform.position, 10);
foreach (var hit in colliders)
{
if (hit.GetComponent<Enemy>() != null)
{
enemyTargets.Add(hit.transform);
}
}
}
}
}
//https://docs.unity3d.com/cn/current/ScriptReference/Collider2D.OnTriggerEnter2D.html
private void StuckInto(Collider2D collision)
{
if(pierceAmount > 0 && collision.GetComponent<Enemy>()!= null)//本质就是能穿过敌人,在amount>0时不执行能让剑卡在敌人里的语句就行
{
pierceAmount--;
return;
}
if (isSpinning)
{
StopWhenSpinning();
return;
}//防止卡在敌人身上
canRotate = false;
cd.enabled = false;//相当于关闭碰撞后触发函数。//https://docs.unity3d.com/cn/current/ScriptReference/Collision2D-enabled.html
rb.isKinematic = true;//开启物理学反应 https://docs.unity3d.com/cn/current/ScriptReference/Rigidbody2D-isKinematic.html
rb.constraints = RigidbodyConstraints2D.FreezeAll;//冻结所有移动
if (isBouncing&&enemyTargets.Count>0)//修复剑卡不到墙上的bug
return;
//终止对动画的改变和终止附在敌人上
transform.parent = collision.transform;//设置sword的父组件为碰撞到的物体
anim.SetBool("Rotation", false);
}
}
Sword_Skill.cs
cs
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public enum SwordType
{
Regular,
Bounce,
Pierce,
Spin
}
public class Sword_Skill : Skill
{
public SwordType swordType = SwordType.Regular;//创建应一个enum列表,后面用来判断切换状态
[Header("Bounce Info")]
[SerializeField] private int bounceAmount;
[SerializeField] private float bounceGravity;
[SerializeField] private float bounceSpeed;
[Header("Pierce info")]
[SerializeField] private int pierceAmount;
[SerializeField] private float pierceGravity;
[Header("Skill Info")]
[SerializeField] private GameObject swordPrefab;//sword预制体
[SerializeField] private Vector2 launchForce;//发射力度
[SerializeField] private float swordGravity;//发射体的重力
[SerializeField] private float freezeTimeDuration;
[SerializeField] private float returnSpeed;
[Header("Spin info")]
[SerializeField] private float hitCooldown = .35f;//攻击冷却
[SerializeField] private float maxTravelDistance = 7;//最大攻击距离
[SerializeField] private float spinDuration = 2;//持续时间
[SerializeField] private float spinGravity = 1;
[Header("Aim dots")]
[SerializeField] private int numberOfDots;//需要的点的数量
[SerializeField] private float spaceBetweenDots;//相隔的距离
[SerializeField] private GameObject dotPrefab;//dot预制体
[SerializeField] private Transform dotsParent;//不是很懂
private GameObject[] dots;//dot组
private Vector2 finalDir;
protected override void Start()
{
base.Start();
GenerateDots();//生成点函数
SetupGravity();
}
private void SetupGravity()//每个剑的状态都对应了不同的剑重力
{
if(swordType == SwordType.Pierce)
{
swordGravity = pierceGravity;
}
if(swordType == SwordType.Bounce)
{
swordGravity = bounceGravity;
}
if(swordType == SwordType.Spin)
{
swordGravity = spinGravity;
}
}
protected override void Update()
{
base.Update();
if (Input.GetKeyUp(KeyCode.Mouse1))
{
finalDir = new Vector2(AimDirection().normalized.x * launchForce.x, AimDirection().normalized.y * launchForce.y);
//将位移量改为单位向量分别与力度的x,y相乘作为finalDir
}
if (Input.GetKey(KeyCode.Mouse1))
{
for (int i = 0; i < dots.Length; i++)
{
dots[i].transform.position = DotsPosition(i * spaceBetweenDots);//用循环为每个点以返回值赋值(传入值为每个点的顺序i*点间距
}
}
}
public void CreateSword()
{
GameObject newSword = Instantiate(swordPrefab, player.transform.position, transform.rotation);//创造实例,初始位置为此时player的位置
Sword_Skill_Controller newSwordScript = newSword.GetComponent<Sword_Skill_Controller>();//获得Controller
if(swordType == SwordType.Bounce)
{
newSwordScript.SetupBounce(true, bounceAmount,returnSpeed);
}
else if(swordType == SwordType.Pierce)
{
newSwordScript.SetupPierce(pierceAmount);
}
else if(swordType == SwordType.Spin)
{
newSwordScript.SetupSpin(true, maxTravelDistance, spinDuration,hitCooldown);
}
newSwordScript.SetupSword(finalDir, swordGravity, player,freezeTimeDuration,bounceSpeed);//调用Controller里的SetupSword函数,给予其速度和重力和player实例
player.AssignNewSword(newSword);//调用在player中保存通过此方法创造出的sword实例
DotsActive(false);//关闭点的显示
}
#region Aim region
public Vector2 AimDirection()
{
Vector2 playerPosition = player.transform.position;//拿到玩家此时的位置
Vector2 mousePosition = Camera.main.ScreenToWorldPoint(Input.mousePosition);//https://docs.unity3d.com/cn/current/ScriptReference/Camera.ScreenToWorldPoint.html
//大概就是返回屏幕上括号里的参数的位置,这里返回了鼠标的位置
//拿到此时鼠标的位置
Vector2 direction = mousePosition - playerPosition;//获得距离的绝对向量
return direction;//返回距离向量
}
public void DotsActive(bool _isActive)
{
for (int i = 0; i < dots.Length; i++)
{
dots[i].SetActive(_isActive);//设置每个点是否显示函数
}
}
private void GenerateDots()//生成点函数
{
dots = new GameObject[numberOfDots];//为dot赋予实例数量
for (int i = 0; i < numberOfDots; i++)
{
dots[i] = Instantiate(dotPrefab, player.transform.position, Quaternion.identity, dotsParent);//对象与世界轴或父轴完全对齐
//https://docs.unity3d.com/cn/current/ScriptReference/Quaternion-identity.html
dots[i].SetActive(false);//关闭dot
}
}
private Vector2 DotsPosition(float t)//传入顺序相关的点间距
{
Vector2 position = (Vector2)player.transform.position +
new Vector2
(AimDirection().normalized.x * launchForce.x,
AimDirection().normalized.y * launchForce.y) * t //基本间距
+ .5f * (Physics2D.gravity * swordGravity) * (t * t)//重力影响
;
//t是控制之间点间距的
return position;//返回位置
}//设置点间距函数
#endregion
}
Clone_Skill_Controller.cs
cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
//Clone_Skill_Controller是绑在Clone体上的也就是Prefah上的
public class Clone_Skill_Controller : MonoBehaviour
{
private Player player;
private Animator anim;//声明animator
private SpriteRenderer sr;//定义Sr
[SerializeField] private float colorLoosingSpeed;//加速消失时间
private float cloneTimer;//定时器
[SerializeField]private Transform attackCheck;
[SerializeField] private float attackCheckRadius = .8f;
private Transform closestEnemy;
private int facingDir = 1;//这个是控制位置的,产生的克隆体的位置能在敌人外侧
private bool canDuplicateClone;
private float chanceToDuplicate;
private void Awake()
{
sr = GetComponent<SpriteRenderer>();//拿到Sr
anim = GetComponent<Animator>();//拿到anim
}
private void Update()
{
cloneTimer -= Time.deltaTime;
if(cloneTimer < 0)
{
sr.color = new Color(1, 1, 1,sr.color.a-(Time.deltaTime * colorLoosingSpeed));//设置sr消失
}
if(sr.color.a<0)
{
Destroy(gameObject);
}
}
public void SetupClone(Transform _newTransform,float _cloneDuration,bool _canAttack,Vector3 _offset,Transform _closestEnemy,bool _canDuplicateClone,float _chanceToDuplicate,Player _player)
{
if(_canAttack)
{
anim.SetInteger("AttackNumber", Random.Range(1, 3));//返回[minInclusive..maxInclusive](范围包括在内)内的随机浮点值。如果minInclusive大于maxInclusive,则数字会自动交换。
}
player = _player;
//Random.Range()//https://docs.unity3d.com/cn/current/ScriptReference/Random.Range.html
transform.position = _newTransform.position+_offset;//这个函数实现了将克隆出来的对象的位置与Dash之前的位置重合的效果
cloneTimer = _cloneDuration;
closestEnemy = _closestEnemy;
canDuplicateClone = _canDuplicateClone;
chanceToDuplicate = _chanceToDuplicate;
FaceCloseTarget();
}
private void AnimationTrigger()
{
cloneTimer = -.1f;
}
private void AttackTrigger()
{
Collider2D[] colliders = Physics2D.OverlapCircleAll(attackCheck.position, attackCheckRadius);//创建一个碰撞器组,保存所有圈所碰到的碰撞器
//https://docs.unity3d.com/2022.3/Documentation/ScriptReference/Physics2D.OverlapCircleAll.html
foreach (var hit in colliders)//https://blog.csdn.net/m0_52358030/article/details/121722077
{
if (hit.GetComponent<Enemy>() != null)
{
player.stats.DoDamage(hit.GetComponent<CharacterStats>());
//使角色克隆体的攻击有概率产生新的克隆体
if (canDuplicateClone)
{
if(Random.Range(1,100)<chanceToDuplicate)
{
SkillManager.instance.clone.CreateClone(hit.transform, new Vector3(1.5f*facingDir, 0));
}
}
}
}
}
private void FaceCloseTarget()
{
if(closestEnemy != null)
{
if(transform.position.x>closestEnemy.position.x)//敌人在左面,转一圈
{
facingDir = -1;//这个是控制位置的
transform.Rotate(0,180,0);
}
}
}
}
Clone_Skill.cs
cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Clone_Skill : Skill
{
[Header("Clone Info")]
[SerializeField] private GameObject clonePrefab;//克隆原型
[SerializeField] private float cloneDuration;//克隆持续时间
[SerializeField] private bool canAttack;// 判断是否可以攻击
[SerializeField] private bool createCloneOnDashStart;
[SerializeField] private bool createCloneOnDashOver;
[SerializeField] private bool canCreateCloneOnCounterAttack;
[Header("Clone can duplicate")]
[SerializeField] private bool canDuplicateClone;
[SerializeField] private float chanceToDuplicate;
[Header("Crystal instead of clone")]
public bool crystalInsteadOfClone;
public void CreateClone(Transform _clonePosition,Vector3 _offset)//传入克隆位置
{
if(crystalInsteadOfClone)
{
SkillManager.instance.crystal.CreateCrystal();
return;
}//让所有的生成克隆的技能都变成生成水晶
GameObject newClone = Instantiate(clonePrefab);//创建新的克隆//克隆 original 对象并返回克隆对象。
//https://docs.unity3d.com/cn/current/ScriptReference/Object.Instantiate.html
newClone.GetComponent<Clone_Skill_Controller>().SetupClone(_clonePosition,cloneDuration,canAttack,_offset,FindClosestEnemy(newClone.transform),canDuplicateClone,chanceToDuplicate,player);//调试clone的位置,同时调试克隆持续时间 //Controller绑在克隆原型上的,所以用GetComponent
}
//让冲刺留下来的克隆在开始和结束各有一个
public void CreateCloneOnDashStart()
{
if (createCloneOnDashStart)
CreateClone(player.transform, Vector3.zero);
}
public void CreateCloneOnDashOver()
{
if(createCloneOnDashOver)
CreateClone(player.transform, Vector3.zero);
}
//反击后产生一个克隆被刺敌人
public void CanCreateCloneOnCounterAttack(Transform _enemyTransform)
{
if (canCreateCloneOnCounterAttack)
StartCoroutine(CreateCloneWithDelay(_enemyTransform, new Vector3(1 * player.facingDir, 0, 0)));
}
//整个延迟生成
private IEnumerator CreateCloneWithDelay(Transform _enemyTransform, Vector3 _offset)
{
yield return new WaitForSeconds(.4f);
CreateClone(_enemyTransform, _offset);
}
}
Blackhole_Skill_Controller.cs
cs
using System.Collections;
using System.Collections.Generic;
using TMPro;
using UnityEngine;
public class Blackhole_Skill_Controller : MonoBehaviour
{
[SerializeField] private GameObject hotKeyPrefab;
[SerializeField] private List<KeyCode> KeyCodeList;
private float maxSize;//最大尺寸
private float growSpeed;//变大速度
private float shrinkSpeed;//缩小速度
private float blackholeTimer;
private bool canGrow = true;//是否可以变大
private bool canShrink;//缩小
private bool canCreateHotKeys = true;专门控制后面进入的没法生成热键
private bool cloneAttackReleased;
private bool playerCanDisaper = true;
private int amountOfAttacks = 4;
private float cloneAttackCooldown = .3f;
private float cloneAttackTimer;
private List<Transform> targets = new List<Transform>();
private List<GameObject> createdHotKey = new List<GameObject>();
public bool playerCanExitState { get; private set; }
public void SetupBlackhole(float _maxSize,float _growSpeed,float _shrinkSpeed,int _amountOfAttacks,float _cloneAttackCooldown,float _blackholeDuration)
{
maxSize = _maxSize;
growSpeed = _growSpeed;
shrinkSpeed = _shrinkSpeed;
amountOfAttacks = _amountOfAttacks;
cloneAttackCooldown = _cloneAttackCooldown;
blackholeTimer = _blackholeDuration;
if (SkillManager.instance.clone.crystalInsteadOfClone)//释放水晶时角色不消失
playerCanDisaper = false;
}
private void Update()
{
blackholeTimer -= Time.deltaTime;
cloneAttackTimer -= Time.deltaTime;
if(blackholeTimer <= 0)
{
blackholeTimer = Mathf.Infinity;//防止重复检测
if (targets.Count > 0)//只有有target才释放攻击
{
ReleaseCloneAttack();//释放攻击
}
else
FinishBlackholeAbility();//缩小黑洞
}
if (Input.GetKeyDown(KeyCode.R)&& targets.Count > 0)
{
ReleaseCloneAttack();
}
CloneAttackLogic();
if (canGrow && !canShrink)
{
//这是控制物体大小的参数
transform.localScale = Vector2.Lerp(transform.localScale, new Vector2(maxSize, maxSize), growSpeed * Time.deltaTime);
//类似MoveToward,不过是放大到多少大小 https://docs.unity3d.com/cn/current/ScriptReference/Vector2.Lerp.html
}
if (canShrink)
{
transform.localScale = Vector2.Lerp(transform.localScale, new Vector2(0, 0), shrinkSpeed * Time.deltaTime);
if (transform.localScale.x <= 1f)
{
Destroy(gameObject);
}
}
}
//释放技能
private void ReleaseCloneAttack()
{
cloneAttackReleased = true;
canCreateHotKeys = false;
DestroyHotKeys();
if(playerCanDisaper)
{
playerCanDisaper = false;
PlayerManager.instance.player.fx.MakeTransprent(true);
}
}
private void CloneAttackLogic()
{
if (cloneAttackTimer < 0 && cloneAttackReleased&&amountOfAttacks>0)
{
cloneAttackTimer = cloneAttackCooldown;
int randomIndex = Random.Range(0, targets.Count);
//限制攻击次数和设置攻击偏移量
float _offset;
if (Random.Range(0, 100) > 50)
_offset = 1.5f;
else
_offset = -1.5f;
if (SkillManager.instance.clone.crystalInsteadOfClone)
{
SkillManager.instance.crystal.CreateCrystal(); //让生成克隆变成生成水晶
SkillManager.instance.crystal.CurrentCrystalChooseRandomTarget(); //让黑洞里替换出来的水晶能够随机选择目标
}
else
{
SkillManager.instance.clone.CreateClone(targets[randomIndex], new Vector3(_offset, 0, 0));
}
amountOfAttacks--;
if (amountOfAttacks <= 0)
{
Invoke("FinishBlackholeAbility", 0.5f);
}
}
}
//完成黑洞技能后
private void FinishBlackholeAbility()
{
DestroyHotKeys();
canShrink = true;
cloneAttackReleased = false;
playerCanExitState = true;
}
private void OnTriggerEnter2D(Collider2D collision)
{
if(collision.GetComponent<Enemy>()!=null)
{
collision.GetComponent<Enemy>().FreezeTime(true);
CreateHotKey(collision);
}
}
private void OnTriggerExit2D(Collider2D collision)
{
if (collision.GetComponent<Enemy>() != null)
{
collision.GetComponent<Enemy>().FreezeTime(false);
}
}
//创建QTE函数
private void CreateHotKey(Collider2D collision)
{
if(KeyCodeList.Count == 0)//当所有的KeyCode都被去除,就不在创建实例
{
return;
}
if(!canCreateHotKeys)//这是当角色已经开大了,不在创建实例
{
return;
}
//创建实例
GameObject newHotKey = Instantiate(hotKeyPrefab, collision.transform.position + new Vector3(0, 2), Quaternion.identity);
//将实例添加进列表
createdHotKey.Add(newHotKey);
//随机KeyCode传给HotKey,并且传过去一个毁掉一个
KeyCode choosenKey = KeyCodeList[Random.Range(0, KeyCodeList.Count)];
KeyCodeList.Remove(choosenKey);
Blackhole_Hotkey_Controller newHotKeyScript = newHotKey.GetComponent<Blackhole_Hotkey_Controller>();
newHotKeyScript.SetupHotKey(choosenKey, collision.transform, this);
}
//添加点击hotkey后对应的敌人进入敌人列表
public void AddEnemyToList(Transform _myEnemy)
{
targets.Add(_myEnemy);
}
//销毁Hotkey
private void DestroyHotKeys()
{
if(createdHotKey.Count <= 0)
{
return;
}
for (int i = 0; i < createdHotKey.Count; i++)
{
Destroy(createdHotKey[i]);
}
}
}
PlayerBlackholeState.cs
cs
using System.Collections;
using System.Collections.Generic;
using Unity.VisualScripting;
using UnityEngine;
public class PlayerBlackholeState : PlayerState
{
private float flyTime = .4f;//飞行时间
private bool skillUsed;//技能是否在被使用
private float defaultGravity;
public PlayerBlackholeState(Player _player, PlayerStateMachine _stateMachine, string _animBoolName) : base(_player, _stateMachine, _animBoolName)
{
}
public override void AnimationFinishTrigger()
{
base.AnimationFinishTrigger();
}
public override void Enter()
{
base.Enter();
skillUsed = false;
stateTimer = flyTime;
defaultGravity = rb.gravityScale;
rb.gravityScale = 0;
}
public override void Exit()
{
base.Exit();
rb.gravityScale = defaultGravity;
player.fx.MakeTransprent(false);
}
public override void Update()
{
base.Update();
//使角色释放技能后能飞起来
if (stateTimer > 0)
{
rb.velocity = new Vector2(0, 15);
}
if(stateTimer < 0)
{
rb.velocity = new Vector2(0, -.1f);
if(!skillUsed)
{
if(player.skill.blackhole.CanUseSkill())//创建实体
skillUsed = true;
}
}
if(player.skill.blackhole.SkillCompleted())
{
stateMachine.ChangeState(player.airState);
}
}
}