Forum begins after the advertisement:
[Part 10] Boss doesn’t flip
Home › Forums › Video Game Tutorial Series › Creating a Metroidvania in Unity › [Part 10] Boss doesn’t flip
- This topic has 19 replies, 2 voices, and was last updated 5 months, 3 weeks ago by
Terence.
-
AuthorPosts
-
March 8, 2024 at 7:58 am #13494
Allan Valin
Participant::Lunge doesn’t do damage.
Player stands over the boss sword’s collider while it uses Triple Slash without taking damage (in front of it works).
Attacking the boss makes it climb the player sword’s collider.
Sometimes the boss stops taking damage.
Some animation transitions don’t seem to work. Barrage and Flame Pillar aren’t activating apparently.
When bouncing, the boss might clip through the ground.I added the player prefab to another scene and it wouldn’t move for some reason.
There’s also this bug where the Animator window won’t show the states unless I restart the engine, wtf.BossController
using System.Collections; using System.Collections.Generic; using UnityEngine; public class BossController : Enemy { public static BossController Instance; [SerializeField] GameObject slashEffect; [Header("Ground Check Settings:")] [SerializeField] public Transform groundCheckPoint; //point at which ground check happens [SerializeField] private float groundCheckY = 0.2f; //how far down from ground chekc point is Grounded() checked [SerializeField] private float groundCheckX = 0.5f; //how far horizontally from ground chekc point to the edge of the player is [SerializeField] private LayerMask whatIsGround; //sets the ground layer [Space(5)] [Header("Attack Settings:")] [SerializeField] public Transform SideAttackTransform; //the middle of the side attack area [SerializeField] public Vector2 SideAttackArea; //how large the area of side attack is [SerializeField] public Transform UpAttackTransform; //the middle of the up attack area [SerializeField] public Vector2 UpAttackArea; //how large the area of side attack is [SerializeField] public Transform DownAttackTransform; //the middle of the down attack area [SerializeField] public Vector2 DownAttackArea; //how large the area of down attack is public float attackRange; public float attackTimer; [Space(5)] [HideInInspector] public bool facingRight; int hitCounter; bool stunned, canStun; bool alive; [HideInInspector] public float runSpeed; public GameObject impactParticle; private void Awake() { if (Instance != null && Instance != this) { Destroy(gameObject); } else { Instance = this; } } protected override void Start() { base.Start(); sr = GetComponentInChildren<SpriteRenderer>(); anim = GetComponentInChildren<Animator>(); ChangeState(EnemyStates.Boss_Stage1); alive = true; } public bool Grounded() { if (Physics2D.Raycast(groundCheckPoint.position, Vector2.down, groundCheckY, whatIsGround) || Physics2D.Raycast(groundCheckPoint.position + new Vector3(groundCheckX, 0, 0), Vector2.down, groundCheckY, whatIsGround) || Physics2D.Raycast(groundCheckPoint.position + new Vector3(-groundCheckX, 0, 0), Vector2.down, groundCheckY, whatIsGround)) { return true; } else { return false; } } private void OnDrawGizmos() { Gizmos.color = Color.red; Gizmos.DrawWireCube(SideAttackTransform.position, SideAttackArea); Gizmos.DrawWireCube(UpAttackTransform.position, UpAttackArea); Gizmos.DrawWireCube(DownAttackTransform.position, DownAttackArea); } float bloodCountDown; float bloodTimer; protected override void Update() { base.Update(); if(health <= 0 && alive) { Death(0); } if(!attacking) { attackCountdown -= Time.deltaTime; } if(stunned) { rb.velocity = Vector2.zero; } bloodCountDown -= Time.deltaTime; if(bloodCountDown <= 0 && (currentEnemyState != EnemyStates.Boss_Stage1 && currentEnemyState != EnemyStates.Boss_Stage2)) { GameObject _orangeBlood = Instantiate(orangeBlood, groundCheckPoint.position, Quaternion.identity); Destroy(_orangeBlood, 4f); bloodCountDown = bloodTimer; } } public void Flip() { if(PlayerController.Instance.transform.position.x < transform.position.x && transform.position.x > 0)//.localScale.x > 0) { transform.eulerAngles = new Vector2(transform.eulerAngles.x, 180); //transform.localScale = new Vector2(-1, transform.localScale.y); facingRight = false; } else { transform.eulerAngles = new Vector2(transform.eulerAngles.x, 0); //transform.localScale = new Vector2(1, transform.localScale.y); facingRight = true; } } protected override void UpdateEnemyStates() { if(PlayerController.Instance != null) { switch(GetCurrentEnemyState) { case EnemyStates.Boss_Stage1: canStun = true; //attackTimer = 6; // higher numbers = slower attack speed runSpeed = speed; break; case EnemyStates.Boss_Stage2: canStun = true; //attackTimer = 5; break; case EnemyStates.Boss_Stage3: canStun = false; //attackTimer = 8; bloodTimer = 5f; break; case EnemyStates.Boss_Stage4: canStun = false; //attackTimer = 10; runSpeed = speed / 2; bloodTimer = 1.5f; break; } } } protected override void OnCollisionStay2D(Collision2D _other) { //base.OnCollisionStay2D(_other); } #region attacking #region variables [HideInInspector] public bool attacking; [HideInInspector] public float attackCountdown; [HideInInspector] public bool damagedPlayer = false; [HideInInspector] public bool parrying; [HideInInspector] public Vector2 moveToPosition; [HideInInspector] public bool diveAttack; public GameObject divingCollider; public GameObject pillar; [HideInInspector] public bool barrageAttack; public GameObject barrageFireball; [HideInInspector] public bool outbreakAttack; [HideInInspector] public bool bounceAttack; [HideInInspector] public float rotationDirectionToTarget; [HideInInspector] public int bounceCount; #endregion #region Control public void AttackHandler() { if(currentEnemyState == EnemyStates.Boss_Stage1) { if(Vector2.Distance(PlayerController.Instance.transform.position, rb.position) <= attackRange) { StartCoroutine(TripleSlash()); } else { StartCoroutine(Lunge()); } } if(currentEnemyState == EnemyStates.Boss_Stage2) { if(Vector2.Distance(PlayerController.Instance.transform.position, rb.position) <= attackRange) { StartCoroutine(TripleSlash()); } else { int _attackChosen = Random.Range(1, 3); switch(_attackChosen) { case 1: StartCoroutine(Lunge()); break; case 2: DiveAttackJump(); break; case 3: BarrageBendDown(); break; } } } if(currentEnemyState == EnemyStates.Boss_Stage3) { int _attackChosen = Random.Range(1, 4); if(_attackChosen == 1) { OutbreakBendDown(); } if (_attackChosen == 2) { DiveAttackJump(); } if (_attackChosen == 3) { BarrageBendDown(); } if (_attackChosen == 4) { BounceAttack(); } /*switch(_attackChosen) { case 1: OutbreakBendDown(); break; case 2: DiveAttackJump(); break; case 3: BarrageBendDown(); break; case 4: BounceAttack(); break; }*/ } if(currentEnemyState == EnemyStates.Boss_Stage4) { if(Vector2.Distance(PlayerController.Instance.transform.position, rb.position) <= attackRange) { StartCoroutine(Slash()); } else { BounceAttack(); } } } public void ResetAllAttacks() { attacking = false; StopCoroutine(TripleSlash()); StopCoroutine(Lunge()); StopCoroutine(Parry()); StopCoroutine(Slash()); diveAttack = false; barrageAttack = false; outbreakAttack = false; bounceAttack = false; } #endregion #region Stage 1 IEnumerator TripleSlash() { attacking = true; rb.velocity = Vector2.zero; anim.SetTrigger("Slash"); SlashAngle(); yield return new WaitForSecondsRealtime(0.3f); yield return new WaitForSecondsRealtime(0.3f); anim.ResetTrigger("Slash"); anim.SetTrigger("Slash"); SlashAngle(); yield return new WaitForSecondsRealtime(0.5f); anim.ResetTrigger("Slash"); anim.SetTrigger("Slash"); SlashAngle(); yield return new WaitForSecondsRealtime(0.2f); anim.ResetTrigger("Slash"); ResetAllAttacks(); } void SlashAngle() { // side attack if(PlayerController.Instance.transform.position.x > transform.position.x || PlayerController.Instance.transform.position.x < transform.position.x) { Instantiate(slashEffect, SideAttackTransform); } // up attack else if(PlayerController.Instance.transform.position.y > transform.position.y) { Instantiate(slashEffect, UpAttackTransform); } // down attack else if(PlayerController.Instance.transform.position.y < transform.position.y) { Instantiate(slashEffect, DownAttackTransform); } } void SlashEffectAtAngle(GameObject _slashEffect, int _effectAngle, Transform _attackTransform) { _slashEffect = Instantiate(_slashEffect, _attackTransform); _slashEffect.transform.eulerAngles = new Vector3(0, 0, _effectAngle); _slashEffect.transform.localScale = new Vector2(transform.localScale.x, transform.localScale.y); } IEnumerator Lunge() { Flip(); attacking = true; anim.SetBool("Lunge", true); yield return new WaitForSecondsRealtime(1f); anim.SetBool("Lunge", false); damagedPlayer = false; ResetAllAttacks(); } IEnumerator Parry() { attacking = true; rb.velocity = Vector2.zero; anim.SetBool("Parry", true); yield return new WaitForSecondsRealtime(0.8f); anim.SetBool("Parry", false); parrying = false; ResetAllAttacks(); } IEnumerator Slash() { attacking = true; rb.velocity = Vector2.zero; anim.SetTrigger("Slash"); SlashAngle(); yield return new WaitForSecondsRealtime(0.3f); anim.ResetTrigger("Slash"); ResetAllAttacks(); } #endregion #region Stage 2 void DiveAttackJump() { attacking = true; moveToPosition = new Vector2(PlayerController.Instance.transform.position.x, rb.position.y + 10); diveAttack = true; anim.SetBool("Jump", true); } public void Dive() { anim.SetBool("Dive", true); anim.SetBool("Dive", false); } private void OnTriggerEnter2D(Collider2D _other) { if(_other.GetComponent<PlayerController>() != null && (diveAttack || bounceAttack)) { _other.GetComponent<PlayerController>().TakeDamage(damage * 2); PlayerController.Instance.pState.recoilingX = true; } } public void DivingPillars() { Vector2 _impactPoint = groundCheckPoint.position; float _spawnDistance = 5; for(int i = 0; i < 10; i++) { Vector2 _pillarSpawnPointRight = _impactPoint + new Vector2(_spawnDistance, 0); Vector2 _pillarSpawnPointLeft = _impactPoint - new Vector2(_spawnDistance, 0); Instantiate(pillar, _pillarSpawnPointRight, Quaternion.Euler(0, 0, -90)); Instantiate(pillar, _pillarSpawnPointLeft, Quaternion.Euler(0, 0, -90)); _spawnDistance += 5; } ResetAllAttacks(); } void BarrageBendDown() { attacking = true; rb.velocity = Vector2.zero; barrageAttack = true; anim.SetTrigger("BendDown"); } public IEnumerator Barrage() { rb.velocity = Vector2.zero; float _currentAngle = 30f; for(int i = 0; i < 10; i++) { GameObject _projectile = Instantiate(barrageFireball, transform.position, Quaternion.Euler(0, 0, _currentAngle)); if(facingRight) { _projectile.transform.eulerAngles = new Vector3(_projectile.transform.eulerAngles.x, 0, _currentAngle); } else { _projectile.transform.eulerAngles = new Vector3(_projectile.transform.eulerAngles.x, 180, _currentAngle); } _currentAngle += 5f; yield return new WaitForSecondsRealtime(0.4f); } yield return new WaitForSecondsRealtime(0.1f); anim.SetBool("Cast", false); ResetAllAttacks(); } #endregion #region Stage 3 void OutbreakBendDown() { attacking = true; rb.velocity = Vector2.zero; moveToPosition = new Vector2(transform.position.x, rb.position.y + 5); outbreakAttack = true; anim.SetTrigger("BendDown"); } public IEnumerator Outbreak() { yield return new WaitForSecondsRealtime(1f); anim.SetBool("Cast", true); rb.velocity = Vector2.zero; for(int i = 0; i < 30; i++) { Instantiate(barrageFireball, transform.position, Quaternion.Euler(0, 0, Random.Range(110, 130))); // downwards Instantiate(barrageFireball, transform.position, Quaternion.Euler(0, 0, Random.Range(50, 70))); // diagonally right Instantiate(barrageFireball, transform.position, Quaternion.Euler(0, 0, Random.Range(260, 280))); // diagonally left yield return new WaitForSecondsRealtime(0.2f); } yield return new WaitForSecondsRealtime(0.1f); rb.constraints = RigidbodyConstraints2D.None; rb.constraints = RigidbodyConstraints2D.FreezeRotation; rb.velocity = new Vector2(rb.velocity.x, -10); yield return new WaitForSecondsRealtime(0.1f); anim.SetBool("Cast", false); ResetAllAttacks(); } public void BounceAttack() { attacking = true; bounceCount = Random.Range(2, 5); BounceBendDown(); } int _bounces = 0; public void CheckBounce() { if(_bounces < bounceCount - 1) { _bounces++; BounceBendDown(); } else { _bounces = 0; anim.Play("Boss_Run"); } } public void BounceBendDown() { rb.velocity = Vector2.zero; moveToPosition = new Vector2(PlayerController.Instance.transform.position.x, rb.position.y + 10); bounceAttack = true; anim.SetTrigger("BendDown"); } public void CalculateTargetAngle() { Vector3 _directionToTarget = (PlayerController.Instance.transform.position - transform.position).normalized; float _angleOfTarget = Mathf.Atan2(_directionToTarget.y, _directionToTarget.x) * Mathf.Rad2Deg; rotationDirectionToTarget = _angleOfTarget; } #endregion #endregion public override void EnemyHit(float _damageDone, Vector2 _hitDirection, float _hitForce) { if(!stunned) { if(!parrying) { if(canStun) { hitCounter++; if(hitCounter >= 3) // number of hits to stun { ResetAllAttacks(); StartCoroutine(Stunned()); } } base.EnemyHit(_damageDone, _hitDirection, _hitForce); if(currentEnemyState != EnemyStates.Boss_Stage4) { ResetAllAttacks(); // cancel attacks to avoid bugs StartCoroutine(Parry()); } } else { StopCoroutine(Parry()); ResetAllAttacks(); StartCoroutine(Slash()); // riposte } } else { StopCoroutine(Stunned()); anim.SetBool("Stunned", false); stunned = false; } #region health to state if(health > 20) { ChangeState(EnemyStates.Boss_Stage1); } if(health <= 15 && health < 10) { ChangeState(EnemyStates.Boss_Stage2); } if(health <= 10 && health < 5) { ChangeState(EnemyStates.Boss_Stage3); } if(health < 10) { ChangeState(EnemyStates.Boss_Stage4); } if(health <= 0) { Death(0); } #endregion } public IEnumerator Stunned() { stunned = true; hitCounter = 0; anim.SetBool("Stunned", true); yield return new WaitForSecondsRealtime(6f); anim.SetBool("Stunned", false); stunned = false; } protected override void Death(float _destroyTime) { ResetAllAttacks(); alive = false; rb.velocity = new Vector2(rb.velocity.x, -25); anim.SetTrigger("Die"); bloodTimer = 0.8f; } public void DestroyAfterDeath() { Destroy(gameObject); } }
BossEvents
using System.Collections; using System.Collections.Generic; using UnityEngine; public class BossEvents : MonoBehaviour { void SlashDamagePlayer() { // side attack if (PlayerController.Instance.transform.position.x > transform.position.x || PlayerController.Instance.transform.position.x < transform.position.x) { Hit(BossController.Instance.SideAttackTransform, BossController.Instance.SideAttackArea); } // up attack else if (PlayerController.Instance.transform.position.y > transform.position.y) { Hit(BossController.Instance.UpAttackTransform, BossController.Instance.UpAttackArea); } // down attack else if (PlayerController.Instance.transform.position.y < transform.position.y) { Hit(BossController.Instance.DownAttackTransform, BossController.Instance.DownAttackArea); } } void Hit(Transform _attackTransform, Vector2 _attackArea) { Collider2D[] _objectsToHit = Physics2D.OverlapBoxAll(_attackTransform.position, _attackArea, 0); for (int i = 0; i < _objectsToHit.Length; i++) { if (_objectsToHit[i].GetComponent<PlayerController>() != null) { _objectsToHit[i].GetComponent<PlayerController>().TakeDamage(BossController.Instance.damage); } } } void Parrying() { BossController.Instance.parrying = true; } void BendDownCheck() { if (BossController.Instance.barrageAttack) { StartCoroutine(BarrageAttackTransition()); } if (BossController.Instance.outbreakAttack) { StartCoroutine(OutbreakAttackTransition()); } if (BossController.Instance.bounceAttack) { BossController.Instance.anim.SetTrigger("Bounce1"); } } void BarrageOrOutbreak() { if (BossController.Instance.barrageAttack) { BossController.Instance.StartCoroutine(BossController.Instance.Barrage()); } if (BossController.Instance.outbreakAttack) { BossController.Instance.StartCoroutine(BossController.Instance.Outbreak()); } } IEnumerator BarrageAttackTransition() { yield return new WaitForSecondsRealtime(1f); BossController.Instance.anim.SetBool("Cast", true); } IEnumerator OutbreakAttackTransition() { yield return new WaitForSecondsRealtime(1f); BossController.Instance.anim.SetBool("Cast", true); } void DestroyAfterDeath() { SpawnBoss.Instance.IsNotTrigger(); BossController.Instance.DestroyAfterDeath(); } }
PlayerController
using System.Collections; using System.Collections.Generic; using System.Net.Sockets; using UnityEngine; using UnityEngine.UIElements; using UnityEngine.UI; public class PlayerController : MonoBehaviour { [Header("Horizontal Movement Settings:")] [SerializeField] private float walkSpeed = 1; //sets the players movement speed on the ground [Space(5)] [Header("Vertical Movement Settings")] [SerializeField] private float jumpForce = 45f; //sets how hight the player can jump private int jumpBufferCounter = 0; //stores the jump button input [SerializeField] private int jumpBufferFrames; //sets the max amount of frames the jump buffer input is stored private float coyoteTimeCounter = 0; //stores the Grounded() bool [SerializeField] private float coyoteTime; ////sets the max amount of frames the Grounded() bool is stored private int airJumpCounter = 0; //keeps track of how many times the player has jumped in the air [SerializeField] private int maxAirJumps; //the max no. of air jumps private float gravity; //stores the gravity scale at start [Space(5)] [Header("Wall Jump Settings")] [SerializeField] private float wallSlidingSpeed = 2f; [SerializeField] private Transform wallCheck; [SerializeField] private LayerMask wallLayer; [SerializeField] private float wallJumpingDuration; [SerializeField] private Vector2 wallJumpingPower; float wallJumpingDirection; bool isWallSliding; bool isWallJumping; [Space(5)] [Header("Ground Check Settings:")] [SerializeField] private Transform groundCheckPoint; //point at which ground check happens [SerializeField] private float groundCheckY = 0.2f; //how far down from ground chekc point is Grounded() checked [SerializeField] private float groundCheckX = 0.5f; //how far horizontally from ground chekc point to the edge of the player is [SerializeField] private LayerMask whatIsGround; //sets the ground layer [Space(5)] [Header("Dash Settings")] [SerializeField] private float dashSpeed; //speed of the dash [SerializeField] private float dashTime; //amount of time spent dashing [SerializeField] private float dashCooldown; //amount of time between dashes [SerializeField] GameObject dashEffect; private bool canDash = true, dashed; [Space(5)] [Header("Attack Settings:")] [SerializeField] private Transform SideAttackTransform; //the middle of the side attack area [SerializeField] private Vector2 SideAttackArea; //how large the area of side attack is [SerializeField] private Transform UpAttackTransform; //the middle of the up attack area [SerializeField] private Vector2 UpAttackArea; //how large the area of side attack is [SerializeField] private Transform DownAttackTransform; //the middle of the down attack area [SerializeField] private Vector2 DownAttackArea; //how large the area of down attack is [SerializeField] private LayerMask attackableLayer; //the layer the player can attack and recoil off of private float timeBetweenAttack, timeSinceAttack; [SerializeField] private float damage; //the damage the player does to an enemy [SerializeField] private GameObject slashEffect; //the effect of the slashs [Space(5)] // time restoration bool restoreTime; float restoreTimeSpeed; [Space(5)] [Header("Recoil Settings:")] [SerializeField] private int recoilXSteps = 5; //how many FixedUpdates() the player recoils horizontally for [SerializeField] private int recoilYSteps = 5; //how many FixedUpdates() the player recoils vertically for [SerializeField] private float recoilXSpeed = 100; //the speed of horizontal recoil [SerializeField] private float recoilYSpeed = 100; //the speed of vertical recoil private int stepsXRecoiled, stepsYRecoiled; //the no. of steps recoiled horizontally and verticall [Space(5)] [Header("Health Settings")] public int health; public int maxHealth; public int maxTotalHealth = 10; public int heartPieces = 4; // how many pieces to get a heart [SerializeField] GameObject bloodSpurt; [SerializeField] float hitFlashSpeed; public delegate void OnHealthChangedDelegate(); [HideInInspector] public OnHealthChangedDelegate onHealthChangedCallback; float healTimer; [SerializeField] float timeToHeal; [Space(5)] [Header("Mana Settings")] [SerializeField] UnityEngine.UI.Image manaStorage; [SerializeField] float mana; [SerializeField] float manaDrainSpeed; [SerializeField] float manaGain; [Space(5)] [Header("Spell Settings")] //spell stats [SerializeField] float manaSpellCost = 0.3f; [SerializeField] float timeBetweenCast = 0.5f; [SerializeField] float spellDamage; //upspellexplosion and downspellfireball [SerializeField] float downSpellForce; // desolate dive only //spell cast objects [SerializeField] GameObject sideSpellFireball; [SerializeField] GameObject upSpellExplosion; [SerializeField] GameObject downSpellFireball; float timeSinceCast; float castOrHealTimer; [Space(5)] [Header("Audio")] [SerializeField] AudioClip landingSound; [SerializeField] AudioClip jumpSound; [SerializeField] AudioClip dashAndAttackSound; [SerializeField] AudioClip spellcastSound; [SerializeField] AudioClip hurtSound; private bool landingSoundPlayed; [Space(5)] [HideInInspector] public PlayerStateList pState; private Animator anim; public Rigidbody2D rb; private SpriteRenderer sr; private AudioSource audioSource; //Input Variables private float xAxis, yAxis; private bool attack = false; private bool canFlash = true; bool openInventory; public static PlayerController Instance; [Header("Unlocks")] public bool unlockedWallJump; public bool unlockedDash; public bool unlockedVarJump; public bool unlockedSideCast; public bool unlockedUpCast; public bool unlockedDownCast; private void Awake() { if (Instance != null && Instance != this) { Destroy(gameObject); } else { Instance = this; } DontDestroyOnLoad(gameObject); } // Start is called before the first frame update void Start() { pState = GetComponent<PlayerStateList>(); rb = GetComponent<Rigidbody2D>(); sr = GetComponent<SpriteRenderer>(); anim = GetComponent<Animator>(); audioSource = GetComponent<AudioSource>(); //SaveData.Instance.LoadPlayerData(); gravity = rb.gravityScale; Mana = mana; manaStorage.fillAmount = Mana; Health = maxHealth; } private void OnDrawGizmos() { Gizmos.color = Color.red; Gizmos.DrawWireCube(SideAttackTransform.position, SideAttackArea); Gizmos.DrawWireCube(UpAttackTransform.position, UpAttackArea); Gizmos.DrawWireCube(DownAttackTransform.position, DownAttackArea); } // Update is called once per frame void Update() { if (GameManager.Instance.gameIsPaused) return; if (pState.cutscene) return; if(pState.alive) { GetInputs(); ToggleInventory(); } UpdateJumpVariables(); RestoreTimeScale(); if (pState.dashing) return; if (pState.alive) { if(!isWallJumping) { Flip(); Move(); Jump(); } if (unlockedWallJump) { WallSlide(); WallJump(); } if(unlockedDash) { StartDash(); } Attack(); Heal(); CastSpell(); } FlashWhileInvincible(); } private void OnTriggerEnter2D(Collider2D _other) //for up and down cast spell { if (_other.GetComponent<Enemy>() != null && pState.casting) { _other.GetComponent<Enemy>().EnemyHit(spellDamage, (_other.transform.position - transform.position).normalized, -recoilYSpeed); } } private void FixedUpdate() { if (pState.cutscene) return; if (pState.dashing) return; Recoil(); } void GetInputs() { xAxis = Input.GetAxisRaw("Horizontal"); yAxis = Input.GetAxisRaw("Vertical"); attack = Input.GetButtonDown("Attack"); //openInventory = Input.GetButton("Inventory"); if (Input.GetButtonDown("Inventory")) openInventory = !openInventory; if (Input.GetButton("Cast/Heal")) { castOrHealTimer += Time.deltaTime; } else { castOrHealTimer = 0; } } void ToggleInventory() { if (openInventory) { UIManager.Instance.inventory.SetActive(true); } else { UIManager.Instance.inventory.SetActive(false) ; } } // movement methods void Flip() { if (xAxis < 0) { transform.localScale = new Vector2(-1, transform.localScale.y); pState.lookingRight = false; } else if (xAxis > 0) { transform.localScale = new Vector2(1, transform.localScale.y); pState.lookingRight = true; } } private void Move() { rb.velocity = new Vector2(walkSpeed * xAxis, rb.velocity.y); anim.SetBool("Walking", rb.velocity.x != 0 && Grounded()); } void StartDash() { if (Input.GetButtonDown("Dash") && canDash && !dashed) { StartCoroutine(Dash()); dashed = true; } if (Grounded()) { dashed = false; } } IEnumerator Dash() { canDash = false; pState.dashing = true; anim.SetTrigger("Dashing"); audioSource.PlayOneShot(dashAndAttackSound); rb.gravityScale = 0; int _dir = pState.lookingRight ? 1 : -1; rb.velocity = new Vector2(_dir * dashSpeed, 0); if (Grounded()) Instantiate(dashEffect, transform); yield return new WaitForSeconds(dashTime); rb.gravityScale = gravity; pState.dashing = false; yield return new WaitForSeconds(dashCooldown); canDash = true; } public IEnumerator WalkIntoNewScene(Vector2 _exitDir, float _delay) { //If exit direction is upwards if (_exitDir.y > 0) { rb.velocity = jumpForce * _exitDir; } //If exit direction requires horizontal movement if (_exitDir.x != 0) { xAxis = _exitDir.x > 0 ? 1 : -1; Move(); } Flip(); yield return new WaitForSeconds(_delay); pState.cutscene = false; } // attacking methods void Attack() { timeSinceAttack += Time.deltaTime; if (attack && timeSinceAttack >= timeBetweenAttack) { int _recoilLeftOrRight = pState.lookingRight ? 1 : -1; timeSinceAttack = 0; anim.SetTrigger("Attacking"); audioSource.PlayOneShot(dashAndAttackSound); if (yAxis == 0 || yAxis < 0 && Grounded()) { Hit(SideAttackTransform, SideAttackArea, ref pState.recoilingX, Vector2.right * _recoilLeftOrRight, recoilXSpeed); Instantiate(slashEffect, SideAttackTransform); } else if (yAxis > 0) { Hit(UpAttackTransform, UpAttackArea, ref pState.recoilingY, Vector2.up, recoilYSpeed); SlashEffectAtAngle(slashEffect, 80, UpAttackTransform); } else if (yAxis < 0 && !Grounded()) { Hit(DownAttackTransform, DownAttackArea, ref pState.recoilingY, Vector2.down, recoilYSpeed); SlashEffectAtAngle(slashEffect, -90, DownAttackTransform); } } } void Hit(Transform _attackTransform, Vector2 _attackArea, ref bool _recoilBool, Vector2 _recoilDir, float _recoilStrength) { Collider2D[] objectsToHit = Physics2D.OverlapBoxAll(_attackTransform.position, _attackArea, 0, attackableLayer); if (objectsToHit.Length > 0) { _recoilBool = true; } for (int i = 0; i < objectsToHit.Length; i++) { if (objectsToHit[i].GetComponent<Enemy>() != null) { objectsToHit[i].GetComponent<Enemy>().EnemyHit(damage, _recoilDir, _recoilStrength); if (objectsToHit[i].CompareTag("Enemy")) { Mana += manaGain; } } } } void SlashEffectAtAngle(GameObject _slashEffect, int _effectAngle, Transform _attackTransform) { _slashEffect = Instantiate(_slashEffect, _attackTransform); _slashEffect.transform.eulerAngles = new Vector3(0, 0, _effectAngle); _slashEffect.transform.localScale = new Vector2(transform.localScale.x, transform.localScale.y); } // life and death methods void Recoil() { if (pState.recoilingX) { if (pState.lookingRight) { rb.velocity = new Vector2(-recoilXSpeed, 0); } else { rb.velocity = new Vector2(recoilXSpeed, 0); } } if (pState.recoilingY) { rb.gravityScale = 0; if (yAxis < 0) { rb.velocity = new Vector2(rb.velocity.x, recoilYSpeed); } else { rb.velocity = new Vector2(rb.velocity.x, -recoilYSpeed); } airJumpCounter = 0; } else { rb.gravityScale = gravity; } //stop recoil if (pState.recoilingX && stepsXRecoiled < recoilXSteps) { stepsXRecoiled++; } else { StopRecoilX(); } if (pState.recoilingY && stepsYRecoiled < recoilYSteps) { stepsYRecoiled++; } else { StopRecoilY(); } if (Grounded()) { StopRecoilY(); } } void StopRecoilX() { stepsXRecoiled = 0; pState.recoilingX = false; } void StopRecoilY() { stepsYRecoiled = 0; pState.recoilingY = false; } public void TakeDamage(float _damage) { audioSource.PlayOneShot(hurtSound); if (pState.alive) { Health -= Mathf.RoundToInt(_damage); if(Health <= 0) { Health = 0; StartCoroutine(Death()); } else { StartCoroutine(StopTakingDamage()); } } } IEnumerator StopTakingDamage() { pState.invincible = true; GameObject _bloodSpurtParticles = Instantiate(bloodSpurt, transform.position, Quaternion.identity); Destroy(_bloodSpurtParticles, 1.5f); anim.SetTrigger("TakeDamage"); yield return new WaitForSeconds(1f); pState.invincible = false; } IEnumerator Flash() { sr.enabled = !sr.enabled; canFlash = false; yield return new WaitForSeconds(0.1f); canFlash = true; } void FlashWhileInvincible() { if (pState.invincible) { if (Time.timeScale > 0.2 && canFlash) { StartCoroutine(Flash()); } } else { sr.enabled = true; } } void RestoreTimeScale() { if (restoreTime) { if (Time.timeScale < 1) { Time.timeScale += Time.deltaTime * restoreTimeSpeed; } else { Time.timeScale = 1; restoreTime = false; } } } public void HitStopTime(float _newTimeScale, int _restoreSpeed, float _delay) { restoreTimeSpeed = _restoreSpeed; if (_delay > 0) { StopCoroutine(StartTimeAgain(_delay)); StartCoroutine(StartTimeAgain(_delay)); } else { restoreTime = true; } Time.timeScale = _newTimeScale; } IEnumerator StartTimeAgain(float _delay) { restoreTime = true; yield return new WaitForSeconds(_delay); } IEnumerator Death() { pState.alive = false; Time.timeScale = 1f; GameObject _bloodSpurtParticles = Instantiate(bloodSpurt, transform.position, Quaternion.identity); Destroy(_bloodSpurtParticles, 1.5f); anim.SetTrigger("Death"); yield return new WaitForSecondsRealtime(0.9f); StartCoroutine(UIManager.Instance.ActivateDeathScreen()); //Respawned(); } public void Respawned() { if(!pState.alive) { pState.alive = true; Health = maxHealth; anim.Play("Player_Idle"); mana = 0; } } public int Health { get { return health; } set { if (health != value) { health = Mathf.Clamp(value, 0, maxHealth); if (onHealthChangedCallback != null) { onHealthChangedCallback.Invoke(); } } } } // magic methods void Heal() { if (Input.GetButton("Cast/Heal") && castOrHealTimer > 0.05f && Health < maxHealth && Mana > 0 && !pState.jumping && !pState.dashing) { pState.healing = true; anim.SetBool("Healing", true); //healing healTimer += Time.deltaTime; if (healTimer >= timeToHeal) { Health++; healTimer = 0; } //drain mana Mana -= Time.deltaTime * manaDrainSpeed; } else { pState.healing = false; anim.SetBool("Healing", false); healTimer = 0; } } public float Mana { get { return mana; } set { //if mana stats change if (mana != value) { mana = Mathf.Clamp(value, 0, 1); manaStorage.fillAmount = Mana; } } } void CastSpell() { if (Input.GetButtonUp("Cast/Heal") && castOrHealTimer <= 0.05f && timeSinceCast >= timeBetweenCast && Mana >= manaSpellCost) { pState.casting = true; timeSinceCast = 0; StartCoroutine(CastCoroutine()); } else { timeSinceCast += Time.deltaTime; } if (Grounded()) { //disable downspell if on the ground downSpellFireball.SetActive(false); } //if down spell is active, force player down until grounded if (downSpellFireball.activeInHierarchy) { rb.velocity += downSpellForce * Vector2.down; } } IEnumerator CastCoroutine() { audioSource.PlayOneShot(spellcastSound); anim.SetBool("Casting", true); yield return new WaitForSeconds(0.15f); // check on Animator if it matches //side cast if ((yAxis == 0 || (yAxis < 0 && Grounded())) && unlockedSideCast) { anim.SetBool("Casting", true); yield return new WaitForSeconds(0.15f); // check on Animator if it matches GameObject _fireBall = Instantiate(sideSpellFireball, SideAttackTransform.position, Quaternion.identity); //flip fireball if (pState.lookingRight) { _fireBall.transform.eulerAngles = Vector3.zero; // if facing right, fireball continues as per normal } else { _fireBall.transform.eulerAngles = new Vector2(_fireBall.transform.eulerAngles.x, 180); //if not facing right, rotate the fireball 180 deg } pState.recoilingX = true; Mana -= manaSpellCost; yield return new WaitForSeconds(0.35f); // check Animator } //up cast else if (yAxis > 0 && unlockedUpCast) { anim.SetBool("Casting", true); yield return new WaitForSeconds(0.15f); // check on Animator if it matches Instantiate(upSpellExplosion, transform); rb.velocity = Vector2.zero; Mana -= manaSpellCost; yield return new WaitForSeconds(0.35f); // check Animator } //down cast else if ((yAxis < 0 && !Grounded()) && unlockedDownCast) { anim.SetBool("Casting", true); yield return new WaitForSeconds(0.15f); // check on Animator if it matches downSpellFireball.SetActive(true); Mana -= manaSpellCost; yield return new WaitForSeconds(0.35f); // check Animator } anim.SetBool("Casting", false); pState.casting = false; } // jumping methods public bool Grounded() { if (Physics2D.Raycast(groundCheckPoint.position, Vector2.down, groundCheckY, whatIsGround) || Physics2D.Raycast(groundCheckPoint.position + new Vector3(groundCheckX, 0, 0), Vector2.down, groundCheckY, whatIsGround) || Physics2D.Raycast(groundCheckPoint.position + new Vector3(-groundCheckX, 0, 0), Vector2.down, groundCheckY, whatIsGround)) { return true; } else { return false; } } void Jump() { if (!pState.jumping && jumpBufferCounter > 0 && coyoteTimeCounter > 0) { audioSource.PlayOneShot(jumpSound); rb.velocity = new Vector3(rb.velocity.x, jumpForce); pState.jumping = true; } if (!Grounded() && airJumpCounter < maxAirJumps && Input.GetButtonDown("Jump") && unlockedVarJump) { audioSource.PlayOneShot(jumpSound); pState.jumping = true; airJumpCounter++; rb.velocity = new Vector3(rb.velocity.x, jumpForce); } if (Input.GetButtonUp("Jump") && rb.velocity.y > 3) { rb.velocity = new Vector2(rb.velocity.x, 0); pState.jumping = false; } anim.SetBool("Jumping", !Grounded()); } void UpdateJumpVariables() { if (Grounded()) { if (!landingSoundPlayed) { audioSource.PlayOneShot(landingSound); landingSoundPlayed = true; } pState.jumping = false; coyoteTimeCounter = coyoteTime; airJumpCounter = 0; } else { coyoteTimeCounter -= Time.deltaTime; landingSoundPlayed = false; } if (Input.GetButtonDown("Jump")) { jumpBufferCounter = jumpBufferFrames; } else { jumpBufferCounter--; } } // wall jumping methods private bool Walled() { return Physics2D.OverlapCircle(wallCheck.position, 0.2f, wallLayer); } void WallSlide() { if(Walled() && !Grounded() && xAxis != 0) { isWallSliding = true; // set player speed to wallsliding speed rb.velocity = new Vector2(rb.velocity.x, Mathf.Clamp(rb.velocity.y, -wallSlidingSpeed, float.MaxValue)); } else { isWallSliding = false; } } void WallJump() { if (isWallSliding) { isWallJumping = false; wallJumpingDirection = !pState.lookingRight ? 1 : -1; CancelInvoke(nameof(StopWallJumping)); } if (Input.GetButtonDown("Jump") && isWallSliding) { isWallJumping = true; rb.velocity = new Vector2(wallJumpingDirection * wallJumpingPower.x, wallJumpingPower.y); dashed = false; airJumpCounter = 0; if ((pState.lookingRight && transform.eulerAngles.y == 0) || (!pState.lookingRight && transform.eulerAngles.y != 0)) { pState.lookingRight = !pState.lookingRight; int _yRotation = pState.lookingRight ? 0 : 180; transform.eulerAngles = new Vector2(transform.eulerAngles.x, _yRotation); } Invoke(nameof(StopWallJumping), wallJumpingDuration); } } void StopWallJumping() { isWallJumping = false; } }
Enemy
using System.Collections; using System.Collections.Generic; using UnityEngine; public class Enemy : MonoBehaviour { [SerializeField] protected float health; [SerializeField] protected float recoilLength; [SerializeField] protected float recoilFactor; [SerializeField] protected bool isRecoiling = false; [SerializeField] public float speed; [SerializeField] public float damage; [SerializeField] protected GameObject orangeBlood; [SerializeField] AudioClip hurtSound; protected float recoilTimer; [HideInInspector] public Rigidbody2D rb; protected SpriteRenderer sr; public Animator anim; protected AudioSource audioSource; protected bool hasTakenDamage = false; protected EnemyStates currentEnemyState; protected enum EnemyStates { // Crawler Crawler_Idle, Crawler_Flip, // Bat Bat_Idle, Bat_Chase, Bat_Stunned, Bat_Death, // Charger Charger_Idle, Charger_Surprised, Charger_Charge, // Boss Boss_Stage1, Boss_Stage2, Boss_Stage3, Boss_Stage4, } protected virtual EnemyStates GetCurrentEnemyState { get { return currentEnemyState; } set { if(currentEnemyState != value) { currentEnemyState = value; ChangeCurrentAnimation(); } } } // Start is called before the first frame update protected virtual void Start() { rb = GetComponent<Rigidbody2D>(); sr = GetComponent<SpriteRenderer>(); anim = GetComponent<Animator>(); audioSource = GetComponent<AudioSource>(); } // Update is called once per frame protected virtual void Update() { if (GameManager.Instance.gameIsPaused) return; hasTakenDamage = false; if (isRecoiling) { if (recoilTimer < recoilLength) { recoilTimer += Time.deltaTime; } else { isRecoiling = false; recoilTimer = 0; } } else { UpdateEnemyStates(); } } public virtual void EnemyHit(float _damageDone, Vector2 _hitDirection, float _hitForce) { if (hasTakenDamage) return; health -= _damageDone; if (!isRecoiling) { audioSource.PlayOneShot(hurtSound); GameObject _orangeBlood = Instantiate(orangeBlood, transform.position, Quaternion.identity); Destroy(_orangeBlood, 5.5f); rb.velocity = _hitForce * recoilFactor * _hitDirection; } hasTakenDamage = true; } protected virtual void OnCollisionStay2D(Collision2D _other) { if (_other.gameObject.CompareTag("Player") && !PlayerController.Instance.pState.invincible && health > 0) { Attack(); if(PlayerController.Instance.pState.alive) { PlayerController.Instance.HitStopTime(0, 5, 0.5f); } } if (_other.gameObject.CompareTag("Enemy")) { transform.localScale = new Vector2(transform.localScale.x * -1, transform.localScale.y); } } protected virtual void Death(float _destroyTime) { Destroy(gameObject, _destroyTime); } protected virtual void UpdateEnemyStates() {} protected virtual void ChangeCurrentAnimation() { } protected void ChangeState(EnemyStates _newState) { GetCurrentEnemyState = _newState; } protected virtual void Attack() { PlayerController.Instance.TakeDamage(damage); PlayerController.Instance.HitStopTime(0, 5, 0.5f); } }
SpawnBoss
using System.Collections; using System.Collections.Generic; using UnityEngine; public class SpawnBoss : MonoBehaviour { public static SpawnBoss Instance; [SerializeField] Transform spawnPoint; [SerializeField] GameObject boss; [SerializeField] Vector2 exitDirection; bool callOnce; BoxCollider2D col; private void Awake() { if (Instance != null && Instance != this) { Destroy(gameObject); } else { Instance = this; } } void Start() { col = GetComponent<BoxCollider2D>(); } private void OnTriggerEnter2D(Collider2D _other) { if (_other.CompareTag("Player")) { if (!callOnce) { StartCoroutine(WalkIntoRoom()); callOnce = true; } } } IEnumerator WalkIntoRoom() { StartCoroutine(PlayerController.Instance.WalkIntoNewScene(exitDirection, 1)); yield return new WaitForSecondsRealtime(1f); col.isTrigger = false; Instantiate(boss, spawnPoint.position, Quaternion.identity); } public void IsNotTrigger() { col.isTrigger = true; } }
Boss_Idle
using System.Collections; using System.Collections.Generic; using UnityEngine; public class Boss_Idle : StateMachineBehaviour { public Rigidbody2D rb; // OnStateEnter is called when a transition starts and the state machine starts to evaluate this state override public void OnStateEnter(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) { rb = animator.GetComponentInParent<Rigidbody2D>(); } // OnStateUpdate is called on each Update frame between OnStateEnter and OnStateExit callbacks override public void OnStateUpdate(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) { rb.velocity = Vector2.zero; RunToPlayer(animator); if (BossController.Instance.attackCountdown <= 0) { BossController.Instance.AttackHandler(); BossController.Instance.attackCountdown = Random.Range(BossController.Instance.attackTimer - 1, BossController.Instance.attackTimer + 1); } } void RunToPlayer(Animator animator) { if (Vector2.Distance(PlayerController.Instance.transform.position, rb.position) >= BossController.Instance.attackRange) { animator.SetBool("Run", true); } else { return; } } // OnStateExit is called when a transition ends and the state machine finishes evaluating this state override public void OnStateExit(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) { } }
Boss_Run
using System.Collections; using System.Collections.Generic; using UnityEngine; public class Boss_Run : StateMachineBehaviour { Rigidbody2D rb; // OnStateEnter is called when a transition starts and the state machine starts to evaluate this state override public void OnStateEnter(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) { rb = animator.GetComponentInParent<Rigidbody2D>(); } // OnStateUpdate is called on each Update frame between OnStateEnter and OnStateExit callbacks override public void OnStateUpdate(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) { TargetPlayerPosition(animator); if (BossController.Instance.attackCountdown <= 0) { BossController.Instance.AttackHandler(); BossController.Instance.attackCountdown = Random.Range(BossController.Instance.attackTimer - 1, BossController.Instance.attackTimer + 1); } } void TargetPlayerPosition(Animator animator) { if (BossController.Instance.Grounded()) { BossController.Instance.Flip(); Vector2 _target = new Vector2(PlayerController.Instance.transform.position.x, rb.position.y); Vector2 _newPos = Vector2.MoveTowards(rb.position, _target, BossController.Instance.runSpeed * Time.fixedDeltaTime); //BossController.Instance.runSpeed = BossController.Instance.speed; rb.MovePosition(_newPos); } else { rb.velocity = new Vector2(rb.velocity.x, -25); } if (Vector2.Distance(PlayerController.Instance.transform.position, rb.position) <= BossController.Instance.attackRange) { animator.SetBool("Run", false); } else { return; } } // OnStateExit is called when a transition ends and the state machine finishes evaluating this state override public void OnStateExit(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) { animator.SetBool("Run", false); } }
Boss_Lunge
using System.Collections; using System.Collections.Generic; using UnityEngine; public class Boss_Lunge : StateMachineBehaviour { Rigidbody2D rb; // OnStateEnter is called when a transition starts and the state machine starts to evaluate this state override public void OnStateEnter(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) { rb = animator.GetComponentInParent<Rigidbody2D>(); } // OnStateUpdate is called on each Update frame between OnStateEnter and OnStateExit callbacks override public void OnStateUpdate(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) { rb.gravityScale = 0; int _dir = BossController.Instance.facingRight ? 1 : -1; rb.velocity = new Vector2(_dir * (BossController.Instance.speed * 5), 0f); if (Vector2.Distance(PlayerController.Instance.transform.position, rb.position) <= BossController.Instance.attackRange && !BossController.Instance.damagedPlayer) { PlayerController.Instance.TakeDamage(BossController.Instance.damage); BossController.Instance.damagedPlayer = true; } } void TargetPlayerPosition(Animator animator) { } // OnStateExit is called when a transition ends and the state machine finishes evaluating this state override public void OnStateExit(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) { } }
Boss_Jump
using System.Collections; using System.Collections.Generic; using UnityEngine; public class Boss_Jump : StateMachineBehaviour { Rigidbody2D rb; // OnStateEnter is called when a transition starts and the state machine starts to evaluate this state override public void OnStateEnter(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) { rb = animator.GetComponentInParent<Rigidbody2D>(); } // OnStateUpdate is called on each Update frame between OnStateEnter and OnStateExit callbacks override public void OnStateUpdate(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) { DiveAttack(); } void DiveAttack() { if (BossController.Instance.diveAttack) { BossController.Instance.Flip(); Vector2 _newPos = Vector2.MoveTowards(rb.position, BossController.Instance.moveToPosition, BossController.Instance.speed * 3 * Time.fixedDeltaTime); rb.MovePosition(_newPos); float _distance = Vector2.Distance(rb.position, _newPos); if (_distance < 0.1f) { BossController.Instance.Dive(); } } } // OnStateExit is called when a transition ends and the state machine finishes evaluating this state override public void OnStateExit(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) { } }
DivingPillar
using System.Collections; using System.Collections.Generic; using UnityEngine; public class DivingPillar : MonoBehaviour { private void OnTriggerEnter2D(Collider2D _other) { if (_other.CompareTag("Player")) { _other.GetComponent<PlayerController>().TakeDamage(BossController.Instance.damage); } } }
Boss_Dive
using System.Collections; using System.Collections.Generic; using UnityEngine; public class Boss_Dive : StateMachineBehaviour { Rigidbody2D rb; bool callOnce; // OnStateEnter is called when a transition starts and the state machine starts to evaluate this state override public void OnStateEnter(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) { rb = animator.GetComponentInParent<Rigidbody2D>(); } // OnStateUpdate is called on each Update frame between OnStateEnter and OnStateExit callbacks override public void OnStateUpdate(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) { BossController.Instance.divingCollider.SetActive(true); if (BossController.Instance.Grounded()) { BossController.Instance.divingCollider.SetActive(false); if (!callOnce) { GameObject _impactParticle = Instantiate(BossController.Instance.impactParticle, BossController.Instance.groundCheckPoint.position, Quaternion.identity); Destroy(_impactParticle, 4f); BossController.Instance.DivingPillars(); animator.SetBool("Dive", false); BossController.Instance.ResetAllAttacks(); callOnce = true; } } } // OnStateExit is called when a transition ends and the state machine finishes evaluating this state override public void OnStateExit(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) { callOnce = false; } }
Boss_BendDown
using System.Collections; using System.Collections.Generic; using UnityEngine; public class Boss_BendDown : StateMachineBehaviour { Rigidbody2D rb; // OnStateEnter is called when a transition starts and the state machine starts to evaluate this state override public void OnStateEnter(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) { rb = animator.GetComponentInParent<Rigidbody2D>(); } // OnStateUpdate is called on each Update frame between OnStateEnter and OnStateExit callbacks override public void OnStateUpdate(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) { } void OutbreakAttack() { if (BossController.Instance.outbreakAttack) { Vector2 _newPos = Vector2.MoveTowards(rb.position, BossController.Instance.moveToPosition, BossController.Instance.speed * 1.5f * Time.fixedDeltaTime); rb.MovePosition(_newPos); float _distance = Vector2.Distance(rb.position, _newPos); if (_distance < 0.1f) { BossController.Instance.rb.constraints = RigidbodyConstraints2D.FreezePosition; } } } // OnStateExit is called when a transition ends and the state machine finishes evaluating this state override public void OnStateExit(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) { animator.ResetTrigger("BendDown"); } }
Boss_Bounce1
using System.Collections; using System.Collections.Generic; using UnityEngine; public class Boss_Bounce1 : StateMachineBehaviour { Rigidbody2D rb; // OnStateEnter is called when a transition starts and the state machine starts to evaluate this state override public void OnStateEnter(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) { rb = animator.GetComponentInParent<Rigidbody2D>(); } // OnStateUpdate is called on each Update frame between OnStateEnter and OnStateExit callbacks override public void OnStateUpdate(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) { if (BossController.Instance.bounceAttack) { Vector2 _newPos = Vector2.MoveTowards(rb.position, BossController.Instance.moveToPosition, BossController.Instance.speed * Random.Range(2, 4) * Time.fixedDeltaTime); rb.MovePosition(_newPos); float _distance = Vector2.Distance(rb.position, _newPos); if (_distance < 0.1f) { BossController.Instance.CalculateTargetAngle(); animator.SetTrigger("Bounce2"); } } } // OnStateExit is called when a transition ends and the state machine finishes evaluating this state override public void OnStateExit(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) { animator.ResetTrigger("Bounce1"); } }
Boss_Bounce2
using System.Collections; using System.Collections.Generic; using UnityEngine; public class Boss_Bounce2 : StateMachineBehaviour { Rigidbody2D rb; bool callOnce; // OnStateEnter is called when a transition starts and the state machine starts to evaluate this state override public void OnStateEnter(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) { rb = animator.GetComponentInParent<Rigidbody2D>(); } // OnStateUpdate is called on each Update frame between OnStateEnter and OnStateExit callbacks override public void OnStateUpdate(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) { Vector2 _forceDirection = new Vector2(Mathf.Cos(Mathf.Deg2Rad * BossController.Instance.rotationDirectionToTarget), Mathf.Sin(Mathf.Deg2Rad * BossController.Instance.rotationDirectionToTarget)); rb.AddForce(_forceDirection * 3, ForceMode2D.Impulse); BossController.Instance.divingCollider.SetActive(true); if (BossController.Instance.Grounded()) { BossController.Instance.divingCollider.SetActive(false); if (!callOnce) { GameObject _impactParticle = Instantiate(BossController.Instance.impactParticle, BossController.Instance.groundCheckPoint.position, Quaternion.identity); Destroy(_impactParticle, 4f); BossController.Instance.ResetAllAttacks(); BossController.Instance.CheckBounce(); callOnce = true; } animator.SetTrigger("Grounded"); } } // OnStateExit is called when a transition ends and the state machine finishes evaluating this state override public void OnStateExit(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) { animator.ResetTrigger("Bounce2"); animator.ResetTrigger("Grounded"); callOnce = false; } }
BarrageFireball
using System.Collections; using System.Collections.Generic; using UnityEngine; public class BarrageFireball : MonoBehaviour { [SerializeField] Vector2 startForceMinMax; [SerializeField] float turnSpeed = 0.5f; Rigidbody2D rb; void Start() { rb = GetComponent<Rigidbody2D>(); Destroy(gameObject, 4f); rb.AddForce(transform.right * Random.Range(startForceMinMax.x, startForceMinMax.y), ForceMode2D.Impulse); } void Update() { var _dir = rb.velocity; if (_dir != Vector2.zero) { Vector3 _frontVector = Vector3.right; Quaternion _targetRotation = Quaternion.FromToRotation(_frontVector, _dir - (Vector2)transform.position); if (_dir.x > 0) { transform.rotation = Quaternion.Lerp(transform.rotation, _targetRotation, turnSpeed); transform.eulerAngles = new Vector3(transform.eulerAngles.x, 180, transform.eulerAngles.z); } else { transform.rotation = Quaternion.Lerp(transform.rotation, _targetRotation, turnSpeed); } } } private void OnTriggerEnter2D(Collider2D _other) { if (_other.tag == "Player") { _other.GetComponent<PlayerController>().TakeDamage(BossController.Instance.damage); Destroy(gameObject); } } }
March 8, 2024 at 9:15 am #13495Allan Valin
Participant::Apparently there’s a time limit to edit posts. I forgot to add a greeting and talk about not having compile errors with the boss’ codes (that is, until I did what is mentioned in the post).
Oh yeah, as you can see in the videos, the Fireball seems to stop after a while.
using System.Collections; using System.Collections.Generic; using UnityEngine; public class FireBall : MonoBehaviour { [SerializeField] float damage; [SerializeField] float hitForce; [SerializeField] int speed; [SerializeField] float lifetime = 1; // Start is called before the first frame update void Start() { Destroy(gameObject, lifetime); } private void FixedUpdate() { transform.position += speed * transform.right; } //detect hit private void OnTriggerEnter2D(Collider2D _other) { if (_other.tag == "Enemy") { _other.GetComponent<Enemy>().EnemyHit(damage, (_other.transform.position - transform.position).normalized, -hitForce); } } }
March 8, 2024 at 11:32 pm #13499Terence
Keymaster::Let’s tackle the easier ones first:
Attacking the boss makes it climb the player sword’s collider.
Make sure that your player’s sword collider is a trigger (has Is Trigger marked).
Some animation transitions don’t seem to work. Barrage and Flame Pillar aren’t activating apparently.
You can put your Animator beside your Game view, and see how the animation transitions are firing. For those animations that aren’t playing, check the transitions and see whether the conditions are correct. Check whether your Exit Time settings are correct as well.
When bouncing, the boss might clip through the ground.
Make the boss’s Rigidbody have a Collision Detection mode of Continuous.
To read more about this: https://blog.terresquall.com/2019/12/collision-detection-modes-in-unitys-rigidbody-component/
March 9, 2024 at 12:18 am #13502Allan Valin
Participant::I’m responding to this, but a bit thinking on the answer for part 7 or 9 you just wrote (about
Hit()
).The Collision Detection was indeed Discrete. So I assume it wasn’t mentioned on the first and last videos of the series lol (on the first one I might have messed up, but I don’t think it was the case on the last).
About the sword colliders, they don’t really exist! The code generates them with ray cast or something, I’m assuming.
From all the children objects under the Player, GroundCheck, Wall Check, Side Attack Transform, Up Attack Transform, Down Attack Transform, and Down Spell Fireball, only the last one has any component attached to it (Sprite Renderer, Circle Colider 2D and Animator), all the others only have a Transform. So there’s literally no collider to speak of where I could check IsTrigger.
About the Animator on the boss, I need to check while it runs, but all transitions appear to be correct tbh.
I used the exact values shown on the video for the transitions that should have an Exit Time. The only one I’m not sure is Boss_Death1. I use Die() to get to it, but there’s no conditions on the transitions to Boss_Death2, and from 2 to 3, but it only player until the 2. I might do a video on that later to show it, need to go to the gym before I go crazy first XDMarch 9, 2024 at 12:20 am #13503Allan Valin
Participant::btw, what was the variable that defines the attack speed of the player again? I can’t seem to find it. Because of the recoil I can make the player fly of dig through a ground collider (discrete detection, gotta test with continuous).
March 9, 2024 at 11:43 pm #13511Terence
Keymaster::btw, what was the variable that defines the attack speed of the player again? I can’t seem to find it. Because of the recoil I can make the player fly of dig through a ground collider (discrete detection, gotta test with continuous).
I think the variable you are looking for is
timeSinceAttack
.March 9, 2024 at 11:48 pm #13512Terence
Keymaster::About the sword colliders, they don’t really exist! The code generates them with ray cast or something, I’m assuming.
From all the children objects under the Player, GroundCheck, Wall Check, Side Attack Transform, Up Attack Transform, Down Attack Transform, and Down Spell Fireball, only the last one has any component attached to it (Sprite Renderer, Circle Colider 2D and Animator), all the others only have a Transform. So there’s literally no collider to speak of where I could check IsTrigger.
I just checked. You are right. If we are able to rule out colliders, then another possibility is that the displacement might be caused by your animation instead of a collider.
About the Animator on the boss, I need to check while it runs, but all transitions appear to be correct tbh.
I used the exact values shown on the video for the transitions that should have an Exit Time. The only one I’m not sure is Boss_Death1. I use Die() to get to it, but there’s no conditions on the transitions to Boss_Death2, and from 2 to 3, but it only player until the 2. I might do a video on that later to show it, need to go to the gym before I go crazy first XDIt may also be that the Parameters are not being fired / toggled as expected, so if the transitions are not firing, observe what your Parameters as the boss fight unfolds.
Enjoy your gym sesh!
March 10, 2024 at 1:58 am #13513Allan Valin
Participant::Hey, ChatGPT fixed the
Flip
function for me, turns out you need to declare aVector3 scale = transform.localScale;
so that so thatlocalScale
works properly.public void Flip() { // Get the local scale of the boss object Vector3 scale = transform.localScale; // If the player is to the left of the boss and the boss is facing right if (PlayerController.Instance.transform.position.x < transform.position.x && scale.x > 0) { // Flip the boss by negating the x scale scale.x = -scale.x; // Update the local scale transform.localScale = scale; facingRight = false; } // If the player is to the right of the boss and the boss is facing left else if (PlayerController.Instance.transform.position.x > transform.position.x && scale.x < 0) { // Flip the boss by negating the x scale scale.x = -scale.x; // Update the local scale transform.localScale = scale; facingRight = true; } }
On another matter, on the Lunge animation, should I alter the x value so that the Boss lunges forward or the code should to that? Because my boss is just thrusting forward without moving.
March 10, 2024 at 2:16 am #13514March 10, 2024 at 2:17 am #13515Terence
Keymaster::On another matter, on the Lunge animation, should I alter the x value so that the Boss lunges forward or the code should to that? Because my boss is just thrusting forward without moving.
If you are asking whether you should do it in the animation or in the code, I suggest doing it in the code, unless the tutorial has guided you to set up root motion for your animations (I assume our Metroidvania series doesn’t do that).
March 10, 2024 at 2:23 am #13516Allan Valin
Participant::You can look in the BossController code above, the method should do that tbh (it doesn’t deal damage as well, as mentioned before).
I tried moving it on the animation, but then the collider doesn’t seem to follow the sprite and the boss teleports back to the initial position after the animation ends.March 10, 2024 at 3:54 am #13518Allan Valin
Participant::Damn, chatGPT is saving the day here, sharing some updates.
The video shows this code, which basically makes the boss not deal damage properly when touching the player.
protected override void OnCollisionStay2D(Collision2D _other) { //base.OnCollisionStay2D(_other); }
Updated Flip
public void Flip() { // Get the local scale of the boss object Vector3 scale = transform.localScale; // If the player is to the left of the boss and the boss is facing right if (PlayerController.Instance.transform.position.x < transform.position.x && scale.x > 0) { // Flip the boss by negating the x scale scale.x = -scale.x; // Update the local scale transform.localScale = scale; facingRight = false; // Update facingRight } // If the player is to the right of the boss and the boss is facing left else if (PlayerController.Instance.transform.position.x > transform.position.x && scale.x < 0) { // Flip the boss by negating the x scale scale.x = -scale.x; // Update the local scale transform.localScale = scale; facingRight = true; // Update facingRight } }
Updated Lunge (with new method to deal damage, not sure if needed or working tbh)
IEnumerator Lunge() { Flip(); // Flip the boss if necessary attacking = true; // Set the "Lunge" animation trigger anim.SetBool("Lunge", true); // Get the starting position of the boss Vector2 startPosition = rb.position; // Calculate the direction to the player Vector2 directionToPlayer = (PlayerController.Instance.transform.position - transform.position).normalized; // Calculate the target position by moving 15 units towards the player on the x-axis Vector2 targetPosition = new Vector2(startPosition.x + (facingRight ? 15f : -15f), startPosition.y); // Get the duration of the animation float animationDuration = anim.GetCurrentAnimatorStateInfo(0).length; // Define the time elapsed float elapsedTime = 0f; // Define if the damage has been dealt bool damageDealt = false; // Move the boss gradually towards the target position while (elapsedTime < animationDuration) { // Calculate the interpolation factor float t = elapsedTime / animationDuration; // Interpolate the boss position between the start and target positions rb.MovePosition(Vector2.Lerp(startPosition, targetPosition, t)); // Check if it's time to deal damage if (!damageDealt && t >= 0.5f) // Adjust the timing as needed { DealDamageToPlayer(); damageDealt = true; } // Update the elapsed time elapsedTime += Time.deltaTime; // Wait for the next frame yield return null; } // Reset the "Lunge" animation trigger anim.SetBool("Lunge", false); // Reset attacking state attacking = false; // Freeze the boss's position until the coroutine completes while (true) { // Ensure the boss stays at the target position rb.MovePosition(targetPosition); // Wait for the next frame yield return null; } } void DealDamageToPlayer() { // Check if the player is in range if (Vector2.Distance(transform.position, PlayerController.Instance.transform.position) <= attackRange) { // Deal damage to the player PlayerController.Instance.TakeDamage(damage); } }
Updated OnTriggerEnted2D
private void OnTriggerEnter2D(Collider2D _other) { if (_other.CompareTag("Player") && ((_other.transform.position.x < transform.position.x && facingRight) || (_other.transform.position.x > transform.position.x && !facingRight))) { PlayerController.Instance.TakeDamage(damage); } }
I’m checking the player as well to see if I solve the being hit by himself when attacking problem. Will post if I manage to fix it.
I found out a bug with wall jumping btw. If you land on top of a wall after climbing it, you’d end up flipped on the wrong side.
void WallSlide() { if (Walled() && !Grounded() && xAxis != 0) { isWallSliding = true; // Set player speed to wall sliding speed rb.velocity = new Vector2(rb.velocity.x, Mathf.Clamp(rb.velocity.y, -wallSlidingSpeed, float.MaxValue)); // Update player orientation based on the wall side if (xAxis < 0 && pState.lookingRight) { Flip(); } else if (xAxis > 0 && !pState.lookingRight) { Flip(); } } else { isWallSliding = false; } } void WallJump() { if (isWallSliding) { isWallJumping = false; wallJumpingDirection = !pState.lookingRight ? 1 : -1; // Update player orientation based on the wall side if ((wallJumpingDirection > 0 && !pState.lookingRight) || (wallJumpingDirection < 0 && pState.lookingRight)) { Flip(); } CancelInvoke(nameof(StopWallJumping)); } if (Input.GetButtonDown("Jump") && isWallSliding) { isWallJumping = true; rb.velocity = new Vector2(wallJumpingDirection * wallJumpingPower.x, wallJumpingPower.y); dashed = false; airJumpCounter = 0; // Update player orientation based on the wall side if ((wallJumpingDirection > 0 && !pState.lookingRight) || (wallJumpingDirection < 0 && pState.lookingRight)) { Flip(); } Invoke(nameof(StopWallJumping), wallJumpingDuration); } }
March 10, 2024 at 7:01 pm #13519Terence
KeymasterMarch 11, 2024 at 1:32 am #13530Allan Valin
Participant::I managed to solve the Hit problem by starting from scratch using a real trigger collider, I used code from other tutorials, so it would take way too long to show all the changes to make the player stop taking damage.
Two other things I noticed when messing up with that:
- timeBetweenAttack is declared as private without [SerializeField] and has never a value assigned to it, that made the attacks not be spaced out.
- Player animation: add transition from Jump attack to Idle if jumping is false (I think the video didn’t show that and it sometimes makes the player stuck on the jump attack animation).
btw I started using github Copilot, you can assign which files it can access for cross-reference and it sometimes helps a lot. But often, as it is with AI, if you don’t know where the problem is, it can’t find it that easily lol
March 11, 2024 at 10:20 pm #13537Terence
Keymaster::btw I started using github Copilot, you can assign which files it can access for cross-reference and it sometimes helps a lot. But often, as it is with AI, if you don’t know where the problem is, it can’t find it that easily lol
That’s amazing. Perhaps it is something I should try as well.
-
AuthorPosts
- You must be logged in to reply to this topic.
Advertisement below: