本章节优化了击退效果。
源代码
Entity.cs
新增代码
public int knockbackDir { get; private set; }
//设置击退方向
public virtual void SetupKnockbackDir(Transform _damageDir)
{
if (_damageDir.position.x > transform.position.x)
knockbackDir = -1;
else
knockbackDir = 1;
}
protected virtual IEnumerator HitKnockback()
{
isKnocked = true;
rb.velocity = new Vector2(knockbackPower.x * knockbackDir, knockbackPower.y);
yield return new WaitForSeconds(knockbackDuration);
isKnocked = false;
}
完整代码
using System.Collections;
using UnityEngine;
public class Entity : MonoBehaviour
{
#region Component
public Animator anim { get; private set; }
public Rigidbody2D rb { get; private set; }
public EntityFX fx { get; private set; }
public SpriteRenderer sr { get; private set; }
public CharacterStats stats { get; private set; }
public CapsuleCollider2D cd { get; private set; }
#endregion
[Header("Knockback info")]
[SerializeField] protected Vector2 knockbackPower;
[SerializeField] protected float knockbackDuration;
protected bool isKnocked;
[Header("Collision info")]
public Transform attackCheck;
public float attackCheckRadius;
[SerializeField] protected Transform groundCheck;
[SerializeField] protected float groundCheckDistance;
[SerializeField] protected Transform wallCheck;
[SerializeField] protected float wallCheckDistance;
[SerializeField] protected LayerMask whatIsGround;
public int knockbackDir { get; private set; }
public int facingDir { get; private set; } = 1;
protected bool facingRight = true;
public System.Action onFlipped;
protected virtual void Awake()
{
}
protected virtual void Start()
{
sr = GetComponentInChildren<SpriteRenderer>();
anim = GetComponentInChildren<Animator>();
rb = GetComponent<Rigidbody2D>();
fx = GetComponent<EntityFX>();
stats = GetComponentInChildren<CharacterStats>();
cd = GetComponent<CapsuleCollider2D>();
}
protected virtual void Update()
{
}
public virtual void SlowEnitityBy(float _slowPercentage, float _slowDuration)
{
}
public virtual void ReturnDefaultSpeed()
{
anim.speed = 1;
}
public virtual void DamageImpact() => StartCoroutine("HitKnockback");
//设置击退方向
public virtual void SetupKnockbackDir(Transform _damageDir)
{
if (_damageDir.position.x > transform.position.x)
knockbackDir = -1;
else
knockbackDir = 1;
}
protected virtual IEnumerator HitKnockback()
{
isKnocked = true;
rb.velocity = new Vector2(knockbackPower.x * knockbackDir, knockbackPower.y);
yield return new WaitForSeconds(knockbackDuration);
isKnocked = false;
}
#region Velocity
public virtual void SetZeroVelocity()
{
if (isKnocked)
return;
rb.velocity = new Vector2(0, 0);
}
public virtual void SetVelocity(float _xVelocity, float _yVelocity)
{
if (isKnocked)
return;
rb.velocity = new Vector2(_xVelocity, _yVelocity);
FlipController(_xVelocity);
}
#endregion
#region Collosion
public virtual bool IsGroundDetected() => Physics2D.Raycast(groundCheck.position, Vector2.down, groundCheckDistance, whatIsGround);
public virtual bool IsWallDetected() => Physics2D.Raycast(wallCheck.position, Vector2.right * facingDir, wallCheckDistance, whatIsGround);
protected virtual void OnDrawGizmos()
{
Gizmos.DrawLine(groundCheck.position, new Vector3(groundCheck.position.x, groundCheck.position.y - groundCheckDistance));
Gizmos.DrawLine(wallCheck.position, new Vector3(wallCheck.position.x + wallCheckDistance, wallCheck.position.y));
Gizmos.DrawWireSphere(attackCheck.position, attackCheckRadius);
}
#endregion
#region Flip
public virtual void Flip()
{
facingDir *= -1;
facingRight = !facingRight;
transform.Rotate(0, 180, 0);
if (onFlipped != null)
onFlipped();
}
public virtual void FlipController(float _x)
{
if (_x > 0 && !facingRight)
{
Flip();
}
else if (_x < 0 && facingRight)
{
Flip();
}
}
#endregion
public virtual void Die()
{
}
}
CharacterStats.cs
新增代码
//设置击退方向
_targetStats.GetComponent<Entity>().SetupKnockbackDir(transform);
完整代码
using System.Collections;
using UnityEngine;
public enum StatType
{
strength,
agility,
intelgence,
vitality,
damage,
critChance,
critPower,
health,
armor,
evasion,
magicRes,
fireDamage,
iceDamage,
lightingDamage
}
public class CharacterStats : MonoBehaviour
{
private EntityFX fx;
[Header("Major stats")]
public Stat strength; //力量 提升1点伤害和1点暴击伤害
public Stat agility; //敏捷 提升1点闪避和1点暴击率
public Stat intelligence; //智力 提升1点魔法伤害和3点魔法抗性
public Stat vitality; //活力 提升5点生命
[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 isChilled; //降低护甲
public bool isShocked; //降低20命中率
[SerializeField] private float ailmentDuration = 2;
private float ignitedTimer;
private float chilledTimer;
private float shockedTimer;
private float igniteDamageCooldown = .3f;
private float igniteDamageTimer;
private int igniteDamage;
[SerializeField] private GameObject shockStrikePrefab;
private int shockDamage;
public int currentHealth;
public System.Action onHealthChanged;
public bool isDead { get; private set; }
private bool isVlnerable;
protected virtual void Start()
{
critPower.SetDefaultValue(150);
currentHealth = GetMaxHealth();
fx = GetComponent<EntityFX>();
}
protected virtual void Update()
{
ignitedTimer -= Time.deltaTime;
chilledTimer -= Time.deltaTime;
shockedTimer -= Time.deltaTime;
igniteDamageTimer -= Time.deltaTime;
if (ignitedTimer < 0)
isIgnited = false;
if (chilledTimer < 0)
isChilled = false;
if (shockedTimer < 0)
isShocked = false;
if (isIgnited)
ApplyIgniteDamage();
}
public void MakeVulnerableFor(float _duration) => StartCoroutine(VulnerableCorutine(_duration));
private IEnumerator VulnerableCorutine(float _duration)
{
isVlnerable = true;
yield return new WaitForSeconds(_duration);
isVlnerable = false;
}
public virtual void IncreaseStatBy(int _modifier, float _duration, Stat _statToModify)
{
StartCoroutine(StatModCoroutine(_modifier, _duration, _statToModify));
}
private IEnumerator StatModCoroutine(int _modifier, float _duration, Stat _statToModify)
{
_statToModify.AddModifier(_modifier);
yield return new WaitForSeconds(_duration);
_statToModify.RemoveModifier(_modifier);
}
public virtual void DoDamage(CharacterStats _targetStats)
{
if (TargetCanAvoidAttack(_targetStats))
return;
//设置击退方向
_targetStats.GetComponent<Entity>().SetupKnockbackDir(transform);
int totalDamage = damage.GetValue() + strength.GetValue();
if (CanCrit())
{
totalDamage = CalculaterCriticalDamage(totalDamage);
}
totalDamage = CheckTargetArmor(_targetStats, totalDamage);
_targetStats.TakeDamage(totalDamage);
DoMagicDamage(_targetStats); //测试魔法伤害
}
#region Magical damage and ailments
public virtual void DoMagicDamage(CharacterStats _targetStats)
{
int _fireDamage = fireDamage.GetValue();
int _iceDamage = iceDamage.GetValue();
int _lightingDamage = lightingDamage.GetValue();
int totalMagicalDamage = _fireDamage + _iceDamage + _lightingDamage + intelligence.GetValue();
totalMagicalDamage = CheckTargetResistance(_targetStats, totalMagicalDamage);
_targetStats.TakeDamage(totalMagicalDamage);
if (Mathf.Max(_fireDamage, _iceDamage, _lightingDamage) <= 0)
return;
AttemptyToApplyAilments(_targetStats, _fireDamage, _iceDamage, _lightingDamage);
}
private void AttemptyToApplyAilments(CharacterStats _targetStats, int _fireDamage, int _iceDamage, int _lightingDamage)
{
bool canApplyIgnite = _fireDamage > _iceDamage && _fireDamage > _lightingDamage;
bool canApplyChill = _iceDamage > _fireDamage && _iceDamage > _lightingDamage;
bool canApplyShock = _lightingDamage > _fireDamage && _lightingDamage > _iceDamage;
while (!canApplyIgnite && !canApplyChill && !canApplyShock)
{
if (Random.value < .3f && _fireDamage > 0)
{
canApplyIgnite = true;
_targetStats.ApplyAilments(canApplyIgnite, canApplyChill, canApplyShock);
return;
}
if (Random.value < .5f && _iceDamage > 0)
{
canApplyChill = true;
_targetStats.ApplyAilments(canApplyIgnite, canApplyChill, canApplyShock);
return;
}
if (Random.value < .5f && _lightingDamage > 0)
{
canApplyShock = true;
_targetStats.ApplyAilments(canApplyIgnite, canApplyChill, canApplyShock);
return;
}
}
if (canApplyIgnite)
_targetStats.SetupIgniteDamage(Mathf.RoundToInt(_fireDamage * 0.15f));
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 && !isChilled && !isShocked;
bool canApplyChill = !isIgnited && !isChilled && !isShocked;
bool canApplyShock = !isIgnited && !isChilled;
if (_ignite && canApplyIgnite)
{
isIgnited = _ignite;
ignitedTimer = ailmentDuration;
fx.IgniteFxFor(ailmentDuration);
}
if (_chill && canApplyChill)
{
isChilled = _chill;
chilledTimer = ailmentDuration;
float slowPrecentage = .2f;
GetComponent<Entity>().SlowEnitityBy(slowPrecentage, ailmentDuration);
fx.ChillFxFor(ailmentDuration);
}
if (_shock && canApplyShock)
{
if (!isShocked)
{
ApplyShock(_shock);
}
else
{
if (GetComponent<Player>() != null)
return;
HitNearestTargetWithShockStrike();
}
}
}
public void ApplyShock(bool _shock)
{
if (isShocked)
return;
isShocked = _shock;
shockedTimer = ailmentDuration;
fx.ShockFxFor(ailmentDuration);
}
private void HitNearestTargetWithShockStrike()
{
Collider2D[] colliders = Physics2D.OverlapCircleAll(transform.position, 25);
float closestDistance = Mathf.Infinity;
Transform closestEnemy = null;
foreach (var hit in colliders)
{
if (hit.GetComponent<Enemy>() != null && Vector2.Distance(transform.position, hit.transform.position) > 1)
{
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>());
}
}
private void ApplyIgniteDamage()
{
if (igniteDamageTimer < 0)
{
DecreaseHealthBy(igniteDamage);
if (currentHealth < 0 && !isDead)
Die();
igniteDamageTimer = igniteDamageCooldown;
}
}
public void SetupIgniteDamage(int _damage) => igniteDamage = _damage;
public void SetupShockStrikeDamage(int _damage) => shockDamage = _damage;
#endregion
public virtual void TakeDamage(int _damage)
{
DecreaseHealthBy(_damage);
GetComponent<Entity>().DamageImpact();
fx.StartCoroutine("FlashFX");
if (currentHealth < 0 && !isDead)
Die();
}
public virtual void IncreaseHealthBy(int _amoout)
{
currentHealth += _amoout;
if (currentHealth > GetMaxHealth())
currentHealth = GetMaxHealth();
if (onHealthChanged != null)
onHealthChanged();
}
protected virtual void DecreaseHealthBy(int _damage)
{
if (isVlnerable)
_damage = Mathf.RoundToInt(_damage * 1.1f);
currentHealth -= _damage;
if (onHealthChanged != null)
onHealthChanged();
}
protected virtual void Die()
{
isDead = true;
}
#region Stat calculations
protected int CheckTargetArmor(CharacterStats _targetStats, int totalDamage)
{
if (_targetStats.isChilled)
totalDamage -= Mathf.RoundToInt(_targetStats.armor.GetValue() * .8f);
else
totalDamage -= _targetStats.armor.GetValue();
totalDamage = Mathf.Clamp(totalDamage, 0, int.MaxValue);
return totalDamage;
}
private int CheckTargetResistance(CharacterStats _targetStats, int totalMagicalDamage)
{
totalMagicalDamage -= _targetStats.magicResistance.GetValue() + (_targetStats.intelligence.GetValue() * 3);
totalMagicalDamage = Mathf.Clamp(totalMagicalDamage, 0, int.MaxValue);
return totalMagicalDamage;
}
public virtual void OnEvasion(Transform _enemy)
{
}
protected bool TargetCanAvoidAttack(CharacterStats _targetStats)
{
int totalEvasion = _targetStats.evasion.GetValue() + _targetStats.agility.GetValue();
if (isShocked)
totalEvasion += 20;
if (Random.Range(0, 100) < totalEvasion)
{
_targetStats.OnEvasion(this.transform);
return true;
}
return false;
}
protected bool CanCrit()
{
int totalCriticalChance = critChance.GetValue() + agility.GetValue();
if (Random.Range(0, 100) <= totalCriticalChance)
{
return true;
}
return false;
}
protected int CalculaterCriticalDamage(int _damage)
{
float totalCriticalPower = ((float)critPower.GetValue() + (float)strength.GetValue()) * .01f;
float critDamage = _damage * totalCriticalPower;
return Mathf.RoundToInt(critDamage);
}
public int GetMaxHealth()
{
return maxHealth.GetValue() + vitality.GetValue() * 5;
}
#endregion
public Stat GetStat(StatType _statType)
{
switch (_statType)
{
case StatType.strength: return strength;
case StatType.agility: return agility;
case StatType.intelgence: return intelligence;
case StatType.vitality: return vitality;
case StatType.damage: return damage;
case StatType.critChance: return critChance;
case StatType.critPower: return critPower;
case StatType.health: return maxHealth;
case StatType.armor: return armor;
case StatType.evasion: return evasion;
case StatType.magicRes: return magicResistance;
case StatType.fireDamage: return fireDamage;
case StatType.iceDamage: return iceDamage;
case StatType.lightingDamage: return lightingDamage;
}
return null;
}
}
Clone_Skill_Controller.cs
新增代码
//设置击退方向
hit.GetComponent<Entity>().SetupKnockbackDir(transform);
完整代码
using UnityEngine;
public class Clone_Skill_Controller : MonoBehaviour
{
private Player player;
private SpriteRenderer sr;
private Animator anim;
[SerializeField] private float colorLoosingSpeed;
private float cloneTimer;
private float attackMultiplier;
[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>();
anim = GetComponent<Animator>();
}
private void Update()
{
cloneTimer -= Time.deltaTime;
if (cloneTimer < 0)
{
sr.color = new Color(1, 1, 1, sr.color.a - (Time.deltaTime * colorLoosingSpeed));
if (sr.color.a < 0)
{
Destroy(this.gameObject);
}
}
}
public void SetupClone(Transform _newTransform, float _cloneDuration, bool _canAttack, Vector3 _offset, Transform _closestEnemy, bool _canDuplicate, float _chanceToDuplicate, Player _player, float _attackMultiplier)
{
if (_canAttack)
anim.SetInteger("AttackNumber", Random.Range(1, 4));
attackMultiplier = _attackMultiplier;
player = _player;
transform.position = _newTransform.position + _offset;
cloneTimer = _cloneDuration;
closestEnemy = _closestEnemy;
canDuplicateClone = _canDuplicate;
chanceToDuplicate = _chanceToDuplicate;
FaceClosestTarget();
}
private void AnimationTrigger()
{
cloneTimer = -.1f;
}
private void AttackTrigger()
{
//攻击音效
AudioManager.instance.PlaySFX(2, null);
Collider2D[] colliders = Physics2D.OverlapCircleAll(attackCheck.position, attackCheckRadius);
foreach (var hit in colliders)
{
if (hit.GetComponent<Enemy>() != null)
{
//player.stats.DoDamage(hit.GetComponent<CharacterStats>());
//设置击退方向
hit.GetComponent<Entity>().SetupKnockbackDir(transform);
PlayerStats playerStats = player.GetComponent<PlayerStats>();
EnemyStats enemyStats = hit.GetComponent<EnemyStats>();
playerStats.CloneDoDamage(enemyStats, attackMultiplier);
if (player.skill.clone.canApplyOnHitEffect)
{
//判断有没有装备武器
ItemData_Equipment weaponData = Inventory.instance.GetEquipment(EquipmentType.武器);
if (weaponData != null)
weaponData.Effect(hit.transform);
}
if (canDuplicateClone)
{
if (Random.Range(0, 100) < chanceToDuplicate)
{
SkillManager.instance.clone.CreateClone(hit.transform, new Vector3(.5f * facingDir, 0));
}
}
}
}
}
private void FaceClosestTarget()
{
if (closestEnemy != null)
{
if (this.transform.position.x > closestEnemy.position.x)
{
facingDir = -1;
this.transform.Rotate(0, 180, 0);
}
}
}
}
Crystal_Skill_Controller.cs
新增代码
//设置击退方向
hit.GetComponent<Entity>().SetupKnockbackDir(transform);
完整代码
using UnityEngine;
public class Crystal_Skill_Controller : MonoBehaviour
{
private Animator anim => GetComponent<Animator>();
private CircleCollider2D cd => GetComponent<CircleCollider2D>();
private Player player;
private float crystalExistTimer;
private bool canExplode;
private bool canMove;
private float moveSpeed;
private bool canGrow;
private float growSpeed = 5;
private Transform closestTarget;
[SerializeField] private LayerMask whatIsEnemy;
public void SetupCrystal(float _crystalDuration, bool _canExplode, bool _canMove, float _moveSpeed, Transform _closestEnemy, Player _player)
{
player = _player;
crystalExistTimer = _crystalDuration;
canExplode = _canExplode;
canMove = _canMove;
moveSpeed = _moveSpeed;
closestTarget = _closestEnemy;
}
private void Update()
{
crystalExistTimer -= Time.deltaTime;
if (crystalExistTimer < 0)
{
canMove = false;
FinishCrystal();
}
if (canMove)
{
if (closestTarget == null)
return;
transform.position = Vector2.MoveTowards(transform.position, closestTarget.position, moveSpeed * Time.deltaTime);
if (Vector2.Distance(transform.position, closestTarget.position) < 1)
{
FinishCrystal();
canMove = false;
}
}
if (canGrow)
transform.localScale = Vector2.Lerp(transform.localScale, new Vector2(3, 3), growSpeed * Time.deltaTime);
}
public void ChooseRandomEnemy()
{
float radius = SkillManager.instance.blackhole.GetBlackholeRadius();
Collider2D[] colliders = Physics2D.OverlapCircleAll(transform.position, radius, whatIsEnemy);
if (colliders.Length > 0)
closestTarget = colliders[Random.Range(0, colliders.Length)].transform;
}
private void AnimationExplodeEvent()
{
Collider2D[] colliders = Physics2D.OverlapCircleAll(transform.position, cd.radius);
foreach (var hit in colliders)
{
if (hit.GetComponent<Enemy>() != null)
{
//设置击退方向
hit.GetComponent<Entity>().SetupKnockbackDir(transform);
player.stats.DoMagicDamage(hit.GetComponent<CharacterStats>());
ItemData_Equipment equipedAmulet = Inventory.instance.GetEquipment(EquipmentType.护身符);
if (equipedAmulet != null)
equipedAmulet.Effect(hit.transform);
}
}
}
public void FinishCrystal()
{
if (canExplode)
{
anim.SetTrigger("Explode");
canGrow = true;
}
else
SelfDestory();
}
public void SelfDestory() => Destroy(gameObject);
}