Hello everyone,
I’m developing a Unity 2D game and have encountered an issue with the attack logic of a spider boss. I hope to get some help.
What I’m Trying to Achieve:
- After the spider boss enters the field, it locates the player’s position using two detection boxes in front and behind.
- Once the player is detected, the spider rushes towards the player to attack, with an attack pre-cast of about 1 second.
- After the attack is completed, it enters an attack completion animation.
- This attack process loops twice; after two cycles, the spider retreats to a safe distance and enters a cooldown state.
- The spider repeats this operation continuously.
Current Problems:
- Inconsistent Attack Cycles:
- The spider does not perfectly execute the two-attack cooldown method. It often retreats excessively.
- Spider Stops Detecting Player:
- After several attacks, the spider stands still. Even when the player touches the detection area, the spider does not initiate an attack.
- Spider Starts Bouncing:
- After several attacks, the spider starts bouncing up and down with the ground, and I’m not sure why.
Implemented Attack Logic and Transition Conditions:
- Entrance Phase:
- The spider plays an entrance animation. After the animation ends, it begins detecting the player.
- Uses the
IsEntranceCompleted()method to check if the entrance animation has finished.
- Player Detection:
- Uses two detection boxes (
leftCheckBoxandrightCheckBox) to detect if the player is within range. - If the player is detected, sets
readyToAttack = trueand triggers the attack pre-cast animationSpiderAttackCast.
- Uses two detection boxes (
- Attack Phase:
- During the
SpiderAttackCastphase, the spider moves towards the player until it reaches a certain distance (castMoveDistance). - The pre-cast lasts about 1 second; after that, the
FinishAttack()method is called.
- During the
- Attack Completion:
- When the attack animation’s last frame triggers,
FinishAttack()is called. - The attack counter
attackCounterincrements. - If
attackCounter >= maxAttacks(set to 2), the spider enters the retreat state and resetsattackCounterto 0. - Otherwise, it triggers the
SpiderBossAttackFinishedanimation.
- When the attack animation’s last frame triggers,
- Retreat and Cooldown:
- The spider moves away from the player by a certain distance (
retreatDistance) and enters a cooldown state. - The cooldown lasts for
cooldownDurationseconds. - After the cooldown, the spider resets relevant states and repeats the attack cycle.
- The spider moves away from the player by a certain distance (
- Moving to Player’s X-Axis:
- The spider periodically tries to move to the player’s X-axis position to approach the player.
Transition Conditions and Trigger Logic:
readyToAttack:- Set to
truewhen the player is detected, initiating the attack pre-cast and movement.
- Set to
isLeaving:- Set to
truewhen the attack count reaches the limit (after two attacks), entering the retreat state. - Reset to
falseafter the cooldown ends.
- Set to
isCoolingDown:- Set to
truewhen entering the cooldown state, disabling attacks and player detection. - Reset to
falseafter the cooldown ends.
- Set to
attackCounter:- Records the number of attacks.
- Incremented after each attack is completed.
- Reset to 0 when reaching the maximum number of attacks.
What I’ve Tried:
- Ensuring that state variables (like
isLeaving,isCoolingDown) are correctly reset after attacks. - Changing the movement method to use physics-based movement (
Rigidbody2D.MovePosition) instead of directly modifyingtransform.position. - Disabling gravity effect (
rb.gravityScale = 0f) to prevent the spider from being affected by gravity.
Persistent Issues:
- The spider often does not execute the attack cycles as intended and retreats too much.
- After several attacks, the spider stands still and stops detecting the player.
- The spider starts bouncing up and down after a few attacks.
Questions:
- How can I modify the code or animation state machine so that the spider executes the attack cycles as intended?
- Why does the spider stop detecting the player and not attack even when the player is within the detection area?
- What could be causing the spider to bounce, and how can I fix this?
Additional Information:
-
Game Type: 2D game.
-
Code Snippet:
-
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class BossAttackScript : MonoBehaviour
{
private SpiderBossControlScript spiderBoss;
private bool isAttacking = false;
private bool readyToAttack = false;
private bool thinking = false;
private bool isLeaving = false; // Flag used to return from Finished to Move state
private bool isCoolingDown = false; // Flag used to disable detection and skills
[SerializeField] private float moveSpeed = 6.6f; // Movement speed
[SerializeField] private float attackOffset = 1.3f; // Movement offset, maximum distance to move during attack
[SerializeField] private float castMoveSpeed = 2f; // Movement speed during Cast phase
[SerializeField] private float castMoveDistance = 1f; // Maximum distance to move during Cast phase
[SerializeField] private int maxAttacks = 2; // Limit on the number of attacks per spider attack cycle
[SerializeField] private float cooldownDuration = 2f; // Duration of the cooldown state
[SerializeField] private float retreatDistance = 1.5f; // Retreat distance
[SerializeField] private float idleDuration = 1.3f; // Duration of idle state
[SerializeField] private float xMoveSpeed = 7f; // Speed when moving to player's X-axis
[SerializeField] private float maxXMoveTime = 2.7f; // Maximum time to move
[SerializeField] private float xSkillCooldown = 1.3f; // Cooldown time for the skill
public LayerMask playerLayer;
public LayerMask groundLayer; // Used to detect GroundLayer
public Transform leftCheckBox;
public Transform rightCheckBox;
private Rigidbody2D rb;
private float detectionRadius = 0.3f;
private int attackCounter = 0; // Attack counter
private Vector3 castStartPosition; // Used to record the starting position of movement
private Transform playerTransform; // Used to store the player's position
private bool isUsingXSkill = false; // Used to determine if the X-axis movement skill is being used
void Start()
{
// Get the SpiderBossControlScript component
spiderBoss = GetComponent<SpiderBossControlScript>();
rb = GetComponent<Rigidbody2D>();
rb.simulated = false; // Disable Rigidbody until the entrance animation ends
rb.gravityScale = 1f; // Disable gravity effect (Note: Setting gravityScale to 1f enables gravity. To disable gravity, set it to 0f.)
// Get the player's Transform
GameObject player = GameObject.FindGameObjectWithTag("Player");
if (player != null)
{
playerTransform = player.transform;
}
else
{
Debug.LogError("Cannot find the Player object!");
}
// Immediately start X-axis detection after the entrance animation ends
StartCoroutine(CheckPlayerXSkill());
}
void Update()
{
// If in cooldown state, disable detection and skills
if (isCoolingDown)
{
return; // Disable all skills during cooldown
}
// Confirm if the entrance animation has completed
if (spiderBoss != null && spiderBoss.IsEntranceCompleted() && !thinking && !isAttacking && !readyToAttack && attackCounter < maxAttacks && !isLeaving)
{
rb.simulated = true; // Enable Rigidbody
// Detect if the player is within the detection range
Collider2D leftHit = Physics2D.OverlapCircle(leftCheckBox.position, detectionRadius, playerLayer);
Collider2D rightHit = Physics2D.OverlapCircle(rightCheckBox.position, detectionRadius, playerLayer);
if (leftHit != null || rightHit != null)
{
readyToAttack = true; // Enter attack state
spiderBoss.animator.SetBool("ReadyToAttack", true); // Ensure the Animator parameter is correctly set
spiderBoss.animator.SetTrigger("SpiderAttackCast"); // Switch to the attack pre-cast animation
// Record the player's Transform position
playerTransform = leftHit != null ? leftHit.transform : rightHit.transform;
castStartPosition = transform.position; // Record the spider's starting position
StartCoroutine(DelayedFinishAttack()); // Trigger FinishAttack method after 1 second
}
}
// Move towards the player's direction during the SpiderAttackCast phase
if (readyToAttack && playerTransform != null && Vector3.Distance(transform.position, castStartPosition) < castMoveDistance)
{
MoveTowardsPlayer(); // Move towards the player
}
}
// Execute the function to move towards the player's direction until reaching a distance of 1f
private void MoveTowardsPlayer()
{
// Calculate the position for the next frame
Vector2 targetPosition = Vector2.MoveTowards(rb.position, playerTransform.position, castMoveSpeed * Time.deltaTime);
rb.MovePosition(targetPosition); // Use Rigidbody2D's MovePosition
AdjustFacingDirection(playerTransform.position.x); // Adjust facing direction
}
// Adjust the spider's facing direction (left/right)
private void AdjustFacingDirection(float targetX)
{
// Adjust facing direction based on the player's position
if (targetX < transform.position.x)
{
transform.localScale = new Vector3(0.5f, transform.localScale.y, transform.localScale.z); // Face left
}
else
{
transform.localScale = new Vector3(-0.5f, transform.localScale.y, transform.localScale.z); // Face right
}
}
// Delay triggering the FinishAttack method
private IEnumerator DelayedFinishAttack()
{
yield return new WaitForSeconds(1.0f); // Delay 1 second
FinishAttack(); // Execute the FinishAttack method
}
// Triggered on the last frame of the attack animation
public void FinishAttack()
{
readyToAttack = false; // Reset ReadyToAttack, indicating the attack has ended
spiderBoss.animator.SetBool("ReadyToAttack", false); // Update the ReadyToAttack parameter in Animator
attackCounter++; // Increase the attack count
if (attackCounter >= maxAttacks)
{
isLeaving = true; // Prepare to enter cooldown
spiderBoss.animator.SetBool("isLeaving", true); // Update Animator parameter
spiderBoss.animator.SetTrigger("SpiderBossMove"); // Switch to the move animation
attackCounter = 0; // Reset attack count
}
else
{
spiderBoss.animator.SetTrigger("SpiderBossAttackFinished"); // Switch to the Finished animation
}
}
// Triggered during the SpiderBossMove phase, move away from the player and enter cooldown state
public void StartRetreat()
{
StartCoroutine(RetreatAndCooldown());
}
// Spider moves away from the player and enters cooldown state
private IEnumerator RetreatAndCooldown()
{
// Disable detection and skill functions
isCoolingDown = true;
isUsingXSkill = false; // Disable X-axis skill
// Check if the spider can move away from the player (GroundLayer detection)
bool canRetreat = CanRetreat();
if (canRetreat)
{
// Position to move away from the player: move along the direction opposite from the player to the spider
Vector2 retreatDirection = (rb.position - (Vector2)playerTransform.position).normalized;
Vector2 retreatTarget = rb.position + retreatDirection * retreatDistance;
// Execute movement
float retreatSpeed = 2f;
while (Vector2.Distance(rb.position, retreatTarget) > 0.1f)
{
Vector2 newPosition = Vector2.MoveTowards(rb.position, retreatTarget, retreatSpeed * Time.deltaTime);
rb.MovePosition(newPosition);
yield return null; // Wait for the next frame
}
}
// Enter cooldown state
yield return new WaitForSeconds(cooldownDuration); // Wait for the cooldown time
isCoolingDown = false; // Cooldown ends, re-enable detection
isLeaving = false; // Reset isLeaving state
spiderBoss.animator.SetBool("isLeaving", false); // Update Animator parameter
}
// Check if the spider can move away from the player, avoiding passing through the GroundLayer
private bool CanRetreat()
{
// Raycast backwards to detect GroundLayer
RaycastHit2D leftRay = Physics2D.Raycast(leftCheckBox.position, -transform.right, retreatDistance, groundLayer);
RaycastHit2D rightRay = Physics2D.Raycast(rightCheckBox.position, -transform.right, retreatDistance, groundLayer);
if (leftRay.collider != null || rightRay.collider != null)
{
// Detected ground obstacle
return false;
}
return true;
}
// Triggered on the last frame of the SpiderBossAttackFinished animation, return to move state
public void ReturnToMove()
{
isLeaving = true; // Prepare to return to move state
spiderBoss.animator.SetBool("isLeaving", true); // Update Animator parameter
StartRetreat(); // Start retreat action
}
// Detect the player's X-axis coordinate and move to their X-axis
private IEnumerator CheckPlayerXSkill()
{
yield return new WaitUntil(() => spiderBoss.IsEntranceCompleted()); // Wait for the entrance animation to end
while (true)
{
if (!isCoolingDown && !isUsingXSkill && !readyToAttack && !isLeaving)
{
// Start moving towards the player's X-axis direction
StartCoroutine(MoveTowardsPlayerX());
}
yield return new WaitForSeconds(0.2f); // Check every 0.2 seconds
}
}
// Move towards the player's X-axis direction
private IEnumerator MoveTowardsPlayerX()
{
isUsingXSkill = true;
float elapsedTime = 0f;
while (elapsedTime < maxXMoveTime && playerTransform != null)
{
// Only move on the X-axis
Vector2 targetPosition = new Vector2(playerTransform.position.x, rb.position.y);
AdjustFacingDirection(playerTransform.position.x); // Adjust facing direction
Vector2 newPosition = Vector2.MoveTowards(rb.position, targetPosition, xMoveSpeed * Time.deltaTime);
rb.MovePosition(newPosition);
elapsedTime += Time.deltaTime;
// Play move animation
spiderBoss.animator.SetTrigger("SpiderBossMove");
// If the player is detected and enters attack state, terminate this skill
if (readyToAttack)
{
isUsingXSkill = false;
yield break;
}
yield return null;
}
// If timed out, enter cooldown
isUsingXSkill = false;
yield return new WaitForSeconds(xSkillCooldown); // Skill cooldown
}
// Disable physical collision rebound
void OnCollisionEnter2D(Collision2D collision)
{
if (collision.gameObject.layer == LayerMask.NameToLayer("Player"))
{
// Disable rebound effect but keep gravity
rb.velocity = Vector2.zero; // Stop moving to avoid rebound
}
}
void OnDrawGizmos()
{
// Draw detection area for visualization in the editor
Gizmos.color = Color.red;
if (leftCheckBox != null)
{
Gizmos.DrawWireSphere(leftCheckBox.position, detectionRadius);
}
if (rightCheckBox != null)
{
Gizmos.DrawWireSphere(rightCheckBox.position, detectionRadius);
}
}
}
Thank you very much for your time and assistance!