Hello everyone,
I’ve been working on my own 2D combat system for the past three days, spending around five hours on it. Since there aren’t many good tutorials on YouTube, I tried to figure everything out myself using my own knowledge and creativity. Overall, everything seems to be working as expected: Attacks can be executed, combos are detected correctly, and the system runs smoothly.
The Problem:
- If I attack once and then stop, the first attack deals damage correctly, but the second one does not.
- If I spam the attack button and chain combos together, no damage is dealt after the first hit.
I’m about 99% sure that I’m correctly calling my methods inside the animation events, and I have already debugged everything. All methods triggered by animations are marked with comments in my code.
Here is my complete script:
using UnityEngine;
using UnityEngine.InputSystem;
public class PlayerCombatSystem : MonoBehaviour
{
[SerializeField] private bool StartTimerForComboWindow = false;
[SerializeField] private ComboAttackData comboAttackDataScribtible;
[SerializeField] private PlayerAnimationsCombatManager playerAnimationsCombatManagerScriptRef;
[SerializeField] private PlayerAnimationsScript playerAnimationsScriptRef;
[SerializeField] private Camera_System camera_SystemScriptRef;
[SerializeField] private PlayerAttackData playerAttackDataScriptible;
[SerializeField] private PlayerAttackStates playerAttackStatesScriptRef;
private PlayerInput playerInput; // Referenz zum neuen Input System
[Header("Combat Settings")]
[SerializeField] private PolygonCollider2D AttackColliderFirstAttack;
[SerializeField] private PolygonCollider2D StrikeDownCollider;
[HideInInspector] public bool isCurrentlyAttacking = false;
void Awake()
{
if (!playerAttackStatesScriptRef) playerAttackStatesScriptRef = GetComponent<PlayerAttackStates>();
if (!comboAttackDataScribtible) comboAttackDataScribtible = GetComponent<ComboAttackData>();
if (!playerAnimationsCombatManagerScriptRef) playerAnimationsCombatManagerScriptRef = GetComponent<PlayerAnimationsCombatManager>();
if (!playerAnimationsScriptRef) playerAnimationsScriptRef = GetComponent<PlayerAnimationsScript>();
if (!camera_SystemScriptRef) camera_SystemScriptRef = GetComponent<Camera_System>();
if (!playerAttackDataScriptible) playerAttackDataScriptible = GetComponent<PlayerAttackData>();
// Setze den aktuellen State auf inAttack1
playerAttackStatesScriptRef.currentAttackState = PlayerAttackStates.PlayerAttackState.inAttack1;
//Folgender Code, deaktivert einfach nur alle verfügbaren Colliders.
if (AttackColliderFirstAttack != null)
AttackColliderFirstAttack.enabled = false;
if (StrikeDownCollider != null)
StrikeDownCollider.enabled = false;
playerInput = GetComponent<PlayerInput>();
}
void Update()
{
if (StartTimerForComboWindow)
{
comboAttackDataScribtible.InputComboTimer += Time.deltaTime; // Timer hochzählen
// Überprüfen, ob der Spieler innerhalb des Zeitfensters angreift
if (playerInput.actions["Attack"].WasPressedThisFrame())
{
//Starte die Angriffs Animation
playerAnimationsCombatManagerScriptRef.HandleCombatAttacks();
playerAttackStatesScriptRef.currentAttackState = PlayerAttackStates.PlayerAttackState.inAttack2; //im zweiten Angriffs State (Wichtig, immer im Überblick bahalten!)
StartTimerForComboWindow = false; // Deaktiviere den Timer
comboAttackDataScribtible.InputComboTimer = 0f; // Timer zurücksetzen, für bugs die ich schon hundertmal gemacht habe....
}
}
// Falls der Timer überschritten wird
if (comboAttackDataScribtible.InputComboTimer > comboAttackDataScribtible.MaxTimeForComboAttack)
{
StartTimerForComboWindow = false; // Timer deaktivieren
comboAttackDataScribtible.InputComboTimer = 0f; // Timer zurücksetzen
playerAttackStatesScriptRef.currentAttackState = PlayerAttackStates.PlayerAttackState.inAttack1; //Spieler befindet sich diesesmal aber jetzt im ersten Zustand
}
MirrorAttackDetection(); //Das spielgelt beide verfügbaren Colliders, damit der Spieler auch nach Rechts schlageb kann.
}
public void OnAttack()
{
if (AttackColliderFirstAttack == null)
{
DebugLimiter.LogError("Der Angirff Collider wurde nicht gesetzt, Fixen");
return;
}
if (!camera_SystemScriptRef)
camera_SystemScriptRef = GetComponent<Camera_System>();
isCurrentlyAttacking = true; //Eine Extrem Wichtig, es sorgt für flüßiege Animationen.
AttackColliderFirstAttack.isTrigger = true;
StrikeDownCollider.isTrigger = true;
}
// Methode wird von Animation Event aufgerufen, Der Timer der dann für den zweiten Schlag fenster offen ist.
private void SetTimerFlagTrue()
{
StartTimerForComboWindow = true;
comboAttackDataScribtible.InputComboTimer = 0f; // Timer starten
}
// Das wird im Animation Window aktivert, und deaktviert immer nur den benötigten Collider für den Angriff
public void ActivateAttackCollider()
{
Debug.Log("Spieler hat etwas getroffen! Aktueller Angriffszustand: " + playerAttackStatesScriptRef.currentAttackState);
if(playerAttackStatesScriptRef.currentAttackState == PlayerAttackStates.PlayerAttackState.inAttack1)
{
AttackColliderFirstAttack.enabled = true;
StrikeDownCollider.enabled = false;
}
else if(playerAttackStatesScriptRef.currentAttackState == PlayerAttackStates.PlayerAttackState.inAttack2)
{
AttackColliderFirstAttack.enabled = false;
StrikeDownCollider.enabled = true;
}
}
// Wird im Animation Evnt aufgeuffen, und deatkviert hier nur alles, WICHRTG Auch der isCurrentlyAttacking = false; wird auf falsch!!!
public void DeactivateAttackCollider()
{
AttackColliderFirstAttack.enabled = false;
StrikeDownCollider.enabled = false;
isCurrentlyAttacking = false;
}
// Hier wird der Collider gespielt, zusammenkomminaktions Script ist hier bei -playerAnimationsScriptRef-
private void MirrorAttackDetection()
{
if (playerAnimationsScriptRef.lookingToRight)
{
if (AttackColliderFirstAttack.enabled)
AttackColliderFirstAttack.transform.rotation = Quaternion.Euler(0f, 180f, 0f);
if (StrikeDownCollider.enabled)
StrikeDownCollider.transform.rotation = Quaternion.Euler(0f, 180f, 0f);
}
else
{
if (AttackColliderFirstAttack.enabled)
AttackColliderFirstAttack.transform.rotation = Quaternion.Euler(0f, 0f, 0f);
if (StrikeDownCollider.enabled)
StrikeDownCollider.transform.rotation = Quaternion.Euler(0f, 0f, 0f);
}
}
public void OnAttackEnd() //Sicherheitshalber nochmal eine Methode was aufgerufen wird um isCurrentlyAttacking auf false zu setzen. Je nach bedarf einsetzen.
{
isCurrentlyAttacking = false;
}
// Kollisions-Logik: Wird aufgerufen, wenn der Collider mit einem Gegner kollidiert
private void OnTriggerEnter2D(Collider2D other)
{
if (!isCurrentlyAttacking || camera_SystemScriptRef == null) return;
if (LayerMask.LayerToName(other.gameObject.layer) != "RatEnemy") return;
if (!other.TryGetComponent(out EnemyHealth enemyHealth)) return;
camera_SystemScriptRef.TriggerShake(); // Kamera schütteln
int damage = 0;
// Falls Spieler im ersten Angriff ist
if (playerAttackStatesScriptRef.currentAttackState == PlayerAttackStates.PlayerAttackState.inAttack1)
{
damage = PlayerMovement.isGrounded ? playerAttackDataScriptible.attackDamage : playerAttackDataScriptible.inAirAttackDamage;
}
// Falls Spieler im zweiten Angriff ist und auf dem Boden steht
else if (playerAttackStatesScriptRef.currentAttackState == PlayerAttackStates.PlayerAttackState.inAttack2 && PlayerMovement.isGrounded)
{
damage = comboAttackDataScribtible.ComboAttackDamage;
}
// Falls kein Schaden gesetzt wurde, verlasse die Methode (z. B. falls inAttack2 in der Luft ist)
if (damage == 0) return;
enemyHealth.TakeDamage(damage);
}
}
Additional Info:
My attack system works with several other scripts, but I have included the most relevant ones here. It consists of three main scripts:
- The main combat script
- The PlayerAnimationsScript, which manages animations
- The PlayerAnimationsCombatManager script, which controls attack animations
Here are the relevant scripts:
1. PlayerAnimationsScript
using UnityEngine;
public class PlayerAnimationsScript : MonoBehaviour
{
[Header("Component References")]
[SerializeField] private PlayerAttackData playerAttackDataScriptible;
[SerializeField] private Animator playerAnimator;
[SerializeField] private SpriteRenderer playerSpriteRenderer;
[SerializeField] private PlayerMovement playerMovementScriptRef;
[SerializeField] private ComboAttackData comboAttackDataScriptible;
[SerializeField] private PlayerCombatSystem playerCombatSystemScriptRef;
[Header("Flip Sprite")]
[HideInInspector] public bool lookingToRight;
[Header("PauseFrames")]
private Coroutine CoroutineForAnimationFreeze;
[Header("Animation Parameters")]
private const string WALK_PARAM = "isWalking";
private const string JUMP_TRIGGER = "isJumping";
private const string ATTACK_TRIGGER = "isAttack1";
private const string COMBO_ATTACKTRIGGER = "isDownAttack";
[SerializeField] private bool allowAnimatePlayer = true;
private void Awake()
{
if (!playerAnimator) playerAnimator = GetComponent<Animator>();
if (!playerSpriteRenderer) playerSpriteRenderer = GetComponent<SpriteRenderer>();
if (!playerMovementScriptRef) playerMovementScriptRef = GetComponent<PlayerMovement>();
if (!comboAttackDataScriptible)comboAttackDataScriptible = GetComponent<ComboAttackData>();
if (!playerCombatSystemScriptRef) playerCombatSystemScriptRef = GetComponent<PlayerCombatSystem>();
}
private void Update()
{
if (!allowAnimatePlayer)
{
ResetToIdle();
return;
}
}
public void HandleMovementAnimation()
{
bool isMoving = Mathf.Abs(PlayerMovement.movementDirectionValue) > 0.1f;
playerAnimator.SetBool(WALK_PARAM, isMoving);
}
public void ChangePlayerSprite()
{
if (Input.GetKey(KeyCode.A) && PlayerMovement.isWalkingAllowed && PlayerMovement.movementDirectionValue < 0)
{
lookingToRight = playerSpriteRenderer.flipX = true;
}
else if (Input.GetKey(KeyCode.D) && PlayerMovement.isWalkingAllowed && PlayerMovement.movementDirectionValue > 0)
{
lookingToRight = playerSpriteRenderer.flipX = false;
}
}
public void HandleJumpAnimation()
{
if (Input.GetButtonDown("Jump") && PlayerMovement.isGrounded)
{
playerAnimator.SetTrigger(JUMP_TRIGGER);
}
}
public void ResetToIdle()
{
playerAnimator.SetBool(WALK_PARAM, false);
playerAnimator.ResetTrigger(JUMP_TRIGGER);
playerAnimator.ResetTrigger(ATTACK_TRIGGER);
playerAnimator.ResetTrigger(COMBO_ATTACKTRIGGER);
}
private void StopAnimationForFrames()
{
playerAnimator.speed = playerAttackDataScriptible.AnimationSpeedOnFrezze;
}
private void StartAnimationAfterFrames()
{
playerAnimator.speed = playerAttackDataScriptible.AnimationSpeed;
}
#region Combat Animations
public void HandleAttackAnimation()
{
if (playerCombatSystemScriptRef.isCurrentlyAttacking)
{
// Für den ersten Angriff
playerAnimator.SetTrigger(ATTACK_TRIGGER);
playerAnimator.SetBool(WALK_PARAM, false);
}
else
{
playerAnimator.ResetTrigger(ATTACK_TRIGGER);
}
// Sicherheitsprüfung für Second Attack
}
#endregion
}
- PlayerAnimationsCombatManager
using UnityEngine;
public class PlayerAnimationsCombatManager : MonoBehaviour
{
[Header("References")]
[SerializeField] private PlayerAnimationsScript playerAnimationsScriptRef;
[SerializeField] private Animator playerAnimator;
void Awake()
{
if(!playerAnimationsScriptRef) playerAnimationsScriptRef = GetComponent<PlayerAnimationsScript>();
if(!playerAnimator) playerAnimator = GetComponent<Animator>();
}
void Update()
{
playerAnimationsScriptRef.HandleAttackAnimation();
}
public void HandleCombatAttacks()
{
playerAnimator.Play("isDownAttack");
}
}
Maybe I’m missing something? Does anyone have an idea what could be causing this issue?
Thanks for any help! ![]()