I am working on a 2D mobile platformer where there will be different characters with unique skill sets. I am working on a character that should be able to perform chained attacks.
I have a button in my UI for attacking. I have added the listener for the ChainableAttack
in the script below. The manner in which the normal attacks work is that the character starts attacking and sets the hit counter to 0. In the FixedUpdate
function, there are some control statements that will accordingly trigger the EnemyAttacked
function up until the character has been hit (This is so that the attack is continuous until it hits the enemy while the attack is ongoing. Otherwise, earlier it would only hit the enemy if it happened to be in the attackRange
when the attack button is clicked).
The normal attack function is working perfectly, but I am struggling to make the follow-up attack work. The character attacks the enemy, but when I hit attack again while the attacking flag is true, it doesn't hit the enemy.
The chainHitCounter
is returning 0 and the chainAttack
flag is returning true. However, the enemy is not responding to the attack.
The fact that the debug statements in the FixedUpdate
are being called but the enemy attacked function isn't, is really confusing me. I can't figure it out, any help would be appreciated. Thank you for your time!
This is the code:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class PlayerAttackBehavior : MonoBehaviour
{
public Transform attackPoint;
public float attackRange = 0.5f;
public Transform kickPoint;
public float kickRange = 0.5f;
public int attackDamage = 40;
[SerializeField] private Animator playerAnimator;
[SerializeField] LayerMask attackLayer;
[SerializeField] float disabledAttackDuration = 1f;
private PlayerMethods pm;
private Collider2D[] hitEnemies;
public bool kicking; //To handle movement in warrior behavior. But it might be being handled by canAttack. Acc very specific behavior.
private int hitCounter;
public bool canAttack = true; // Add a flag to control the player's ability to attack
public bool attacking;
public float chainAttackRange = 7f;
public bool chainAttack;
private int chainCounter;
private int chainHitCounter;
public bool canChainAttack;
private Rigidbody2D rb;
void Start(){
pm = GetComponent<PlayerMethods> ();
chainCounter = 0;
rb = GetComponent<Rigidbody2D>();
hitCounter = 0;
attacking = false;
chainAttack = false;
}
void Update(){
//While attacking is true and int = 0, call enemy attacked function and when an enemy is hit increase the counter to stop the EnemyAttacked function.
//As such the attack box continuously searches for the enemy while the player is attacking until the enemy is hit. Set counter to zero,
//at the beginning of the attack function to reset hitcounter when attacked.
if(attacking && hitCounter == 0){
//Checks for the enemies in attack range until there is a hit.
EnemyAttacked(hitEnemies);
}
if(chainAttack && chainHitCounter == 0){
//The debug logs are being called but EnemyAttacked isn't being called.
Debug.Log("Chain Attack: " + chainAttack);
Debug.Log("Chain Hit Counter: " + chainHitCounter);
EnemyAttacked(hitEnemies);
}
}
public void Attack(bool kick)
{
if(pm.beenHit) return;
if(canAttack){
canAttack = false; //change so if chain Attack is true
attacking = true;
Invoke("SetChainAttack", disabledAttackDuration);
hitCounter = 0;
if(pm != null) {
pm.SetCanTakeDamage(false);
StartCoroutine(pm.DelayedCanTakeDamage(0.65f)); // Re-enable the player's ability to take damage after a delay
}
if(kick) {
playerAnimator.SetTrigger("Kick");
hitEnemies = Physics2D.OverlapCircleAll(kickPoint.position, kickRange,attackLayer);
kicking = true;
Invoke("Unkick", 0.7f);
} else {
playerAnimator.SetTrigger("Attack");
hitEnemies = Physics2D.OverlapCircleAll(attackPoint.position, attackRange,attackLayer);
}
//EnemyAttacked(hitEnemies);
}
}
public void ChainableAttack(bool kick)
{
//pm.animator.GetCurrentAnimatorStateInfo(0).IsName("ChainAttack")
if(pm.beenHit) return;
if(canAttack){
// pm.animator.GetCurrentAnimatorStateInfo(0).IsName("ChainAttack"
if(pm != null) {
pm.SetCanTakeDamage(false);
StartCoroutine(pm.DelayedCanTakeDamage(0.65f)); // Re-enable the player's ability to take damage after a delay
}
//if the attack button is clicked while the attacking is true it calls the chain attack. Or else it does the normal attack
if(attacking){
Debug.Log("Chain attack?");
//Did this incase it was interfering... I don't think it was.
StopCoroutine(pm.DelayedCanTakeDamage(0.65f));
StartCoroutine(pm.DelayedCanTakeDamage(0.65f));
//I put this in because I wanted the chain attack to be called when the player is done with the normal attack is done
//Because I thought that both were occuring simultaneously. The animation for the normal attack has exitTime = 1;
Invoke("SetChainAttack", disabledAttackDuration/2);
//Stops the chainAttack in the Update function once the chain attack is no longer running.
Invoke("StopChainAttack", disabledAttackDuration*2);
} else {
//flag for normal attack used in the FixedUpdate and to trigger chained attack.
attacking = true;
//flag for normal attack.
hitCounter = 0;
Invoke("StopAttack", disabledAttackDuration);
}
if(kick) {
playerAnimator.SetTrigger("Kick");
hitEnemies = Physics2D.OverlapCircleAll(kickPoint.position, kickRange,attackLayer);
kicking = true;
Invoke("Unkick", 0.7f);
} else {
if(chainAttack) {
playerAnimator.SetTrigger("ChainAttack");
hitEnemies = Physics2D.OverlapCircleAll(attackPoint.position, chainAttackRange,attackLayer);
}
else {
playerAnimator.SetTrigger("Attack");
hitEnemies = Physics2D.OverlapCircleAll(attackPoint.position, attackRange,attackLayer);
}
}
}
}
public void SetAttack(){
canAttack = true;
attacking = false;
}
public void StopAttack(){
canAttack = false;
if(chainAttack) return;
attacking = false;
Invoke("SetAttack", disabledAttackDuration);
}
public void SetChainAttack(){
attacking = false;
canAttack = false;
chainHitCounter = 0;
chainAttack = true;
}
public void StopChainAttack(){
SetAttack();
chainAttack = false;
}
void EnemyAttacked(Collider2D[] enemiesHit){
foreach (Collider2D enemy in enemiesHit)
{
Debug.Log("We hit a " + enemy.name);
//Earlier I was using the animation state ... but there is no difference
if(!attacking){
chainHitCounter++;
//This is not working... Earlier I had it
} else {
hitCounter++;
//This is working
}
// chainHitCounter++;
// hitCounter++;
if (enemy.CompareTag("Slime"))
{
enemy.GetComponent<EnemyBehavior>().TakeDamage(attackDamage);
} else if (enemy.name.StartsWith("Bat"))
{
//Vector2 attackDirection = transform.position - enemy.transform.position;
// enemy.GetComponent<Rigidbody2D>().AddForce(attackDirection * 10f , ForceMode2D.Impulse);
enemy.GetComponent<EnemyBehavior>().TakeDamage(attackDamage);
}
else if (enemy.name.StartsWith("Dino"))
{
enemy.GetComponent<EnemyBehavior>().TakeDamage(attackDamage);
}
else if (enemy.name.StartsWith("Bandit"))
{
enemy.GetComponent<EnemyBehavior>().TakeDamage(attackDamage);
}
else if (enemy.CompareTag("Bullet"))
{
Bullet bullet = enemy.GetComponent<Bullet>();
bullet.BeenDeflected();
}
else if (enemy.CompareTag("Vase"))
{
enemy.GetComponent<BreakInteraction>().BreakVase();
}
else if (enemy.CompareTag("MafiaBoss"))
{
//int randomNumber = Random.Range(1, 4);
//if(MafiaBossScript.ready && randomNumber!= 3){
if(!BoomerangManScript.vulnerable){
enemy.GetComponent<BoomerangManScript>().Block();
GetComponent<PlayerController>().EnemyCollision(5f, 0, 1.2f, false);
} else if(BoomerangManScript.vulnerable){
enemy.GetComponent<BoomerangManScript>().TakeDamage(attackDamage);
}
}
else if (enemy.CompareTag("Chicken")){
enemy.GetComponent<EnemyBehavior>().TakeDamage(attackDamage);
enemy.GetComponent<ChickenBehavior>().deflected = true;
} else if(enemy.CompareTag("Scarecrow")){
FindObjectOfType<ScarecrowBehavior>().BeenHit();
}
}
}
void Unkick(){
kicking = false;
}
void OnDrawGizmosSelected()
{
if (attackPoint == null) return;
Gizmos.DrawWireSphere(attackPoint.position, attackRange);
}
// private IEnumerator DelayedCanTakeDamage(float delay, PlayerController pc)
// {
// yield return new WaitForSeconds(delay);
// pc.SetCanTakeDamage(true); // Enable player's ability to take damage
// }
//Moved to player methods which holds all the general functions for all the different characters.
}
if
s checking for different types then doing largely the same few things, this is a job for polymorphism. Add anIDamageable
interface that exposes aTakeDamage
method. This interface can be implemented by theHealth
component on enemies, theDeflectable
component on bullets, and theBreakable
component on vases, each doing the right thing for their respective types. Then you don't need to edit every damage-dealer every time you add a new type of damage-taker: they only need to know the target isIDamageable
. \$\endgroup\$