Forum begins after the advertisement:


[Part 6] Enemy problems

Viewing 10 posts - 1 through 10 (of 10 total)
  • Author
    Posts
  • #13418
    Allan Valin
    Participant
    Helpful?
    Up
    0
    ::

    Hey guys,
    I followed the video as closely as possible, but the charger moonwalks towards the player, and he and the crawler only recognize the player (as in speed it towards him) when you go on their right. If the bat spawns close to the player it works, otherwise it runs away when you get close. Crawler and charger don’t turn around and walk the other way on a ledge (but they flip). Both also glitch if they are on a platform above the player, not knowing if to go left or right. Using a gizmo if shows the the ledge checkers seem to be on the wrong side.
    On the player code I had to remove the “if alive” condition on Update because the controls stopped working because of that.
    Also my heart containers stopped being filled? Last time I opened the editor last month they were working lol, any ideas on what broke?

    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] protected float speed;
    
        [SerializeField] protected float damage;
        [SerializeField] protected GameObject orangeBlood;
    
        protected float recoilTimer;
        protected Rigidbody2D rb;
        protected SpriteRenderer sr;
        protected Animator anim;
    
        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,
        }
    
        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>();
        }
        // Update is called once per frame
        protected virtual void Update()
        {
            
            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)
            {
                GameObject _orangeBlood = Instantiate(orangeBlood, transform.position, Quaternion.identity);
                Destroy(_orangeBlood, 5.5f);
                rb.velocity = _hitForce * recoilFactor * _hitDirection;
            }
            hasTakenDamage = true;
    
        }
        protected 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);
        }
    
    }

    Bat

    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    
    public class Bat : Enemy
    {
        [SerializeField] private float chaseDistance;
        [SerializeField] private float stunDuration;
        float timer;
    
        // Start is called before the first frame update
        protected override void Start()
        {
            base.Start();
            ChangeState(EnemyStates.Bat_Idle);
        }
    
        protected override void Update()
        {
            base.Update();
    
            if (!PlayerController.Instance.pState.alive)
            {
                ChangeState(EnemyStates.Crawler_Idle);
            }
        }
    
        protected override void UpdateEnemyStates()
        {
            float _dist = Vector2.Distance(transform.position, PlayerController.Instance.transform.position);
    
            switch(GetCurrentEnemyState)
            {
                case EnemyStates.Bat_Idle:
                    if(_dist < chaseDistance) ChangeState(EnemyStates.Bat_Chase);
                    break;
                case EnemyStates.Bat_Chase:
                    rb.MovePosition(Vector2.MoveTowards(transform.position, PlayerController.Instance.transform.position, Time.deltaTime * speed));
                    FlipBat();
                    break;
                case EnemyStates.Bat_Stunned:
                    timer += Time.deltaTime;
                    if(timer > stunDuration)
                    {
                        ChangeState(EnemyStates.Bat_Idle);
                        timer = 0;
                    }
                    break;
                case EnemyStates.Bat_Death: 
                    Death(Random.Range(5, 10));
                    break;
            }
        }
    
        public override void EnemyHit(float _damageDone, Vector2 _hitDirection, float _hitForce)
        {
            base.EnemyHit(_damageDone, _hitDirection, _hitForce);
    
            if (health > 0) ChangeState(EnemyStates.Bat_Stunned);
            else ChangeState(EnemyStates.Bat_Death);
            
        }
    
        protected override void Death(float _destroyTime)
        {
            rb.gravityScale = 12.0f;
            base.Death(_destroyTime);
        }
    
        protected override void ChangeCurrentAnimation()
        {
            anim.SetBool("Idle", GetCurrentEnemyState == EnemyStates.Bat_Idle);
            anim.SetBool("Chase", GetCurrentEnemyState == EnemyStates.Bat_Chase);
            anim.SetBool("Stunned", GetCurrentEnemyState == EnemyStates.Bat_Stunned);
    
            if(GetCurrentEnemyState == EnemyStates.Bat_Death) anim.SetTrigger("Death");
        }
    
        void FlipBat()
        {
            sr.flipX = PlayerController.Instance.transform.position.x < transform.position.x;
        }
    
    }
    

    Charger

    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    
    public class Charger : Enemy
    {
        float timer;
        [SerializeField] private float ledgeCheckX;
        [SerializeField] private float ledgeCheckY;
        [SerializeField] private float chargeSpeedMultiplier;
        [SerializeField] private float chargeDuration;
        [SerializeField] private float jumpForce;
        [SerializeField] private LayerMask whatIsGround;
    
        Vector3 _ledgeCheckStart;
        Vector2 _wallCheckDir;
    
        protected override void Start()
        {
            base.Start();
            ChangeState(EnemyStates.Charger_Idle);
            rb.gravityScale = 12f;
        }
    
        // Update is called once per frame
        protected override void Update()
        {
            base.Update();
    
            if (!PlayerController.Instance.pState.alive)
            {
                ChangeState(EnemyStates.Charger_Idle);
            }
    
            if (!isRecoiling)
            {
                transform.position = Vector2.MoveTowards
                    (transform.position, new Vector2(PlayerController.Instance.transform.position.x, transform.position.y),
                    speed * Time.deltaTime);
            }
        }
    
        protected override void UpdateEnemyStates()
        {
            if (health <= 0)
            {
                Death(0.05f);
            }
            _ledgeCheckStart = transform.localScale.x > 0 ? new Vector3(ledgeCheckX, 0) : new Vector3(-ledgeCheckX, 0);
            _wallCheckDir = transform.localScale.x > 0 ? transform.right : -transform.right;
    
            switch (GetCurrentEnemyState)
            {
                case EnemyStates.Charger_Idle:
                    
    
                    if (!Physics2D.Raycast(transform.position + _ledgeCheckStart, Vector2.down, ledgeCheckY, whatIsGround) ||
                        Physics2D.Raycast(transform.position, _wallCheckDir, ledgeCheckX, whatIsGround))
                    {
                        transform.localScale = new Vector2(transform.localScale.x * -1, transform.localScale.y);
                    }
    
                    RaycastHit2D _hit = Physics2D.Raycast(transform.position + _ledgeCheckStart, _wallCheckDir, ledgeCheckX * 10);
                    if (_hit.collider != null && _hit.collider.gameObject.CompareTag("Player")) ChangeState(EnemyStates.Charger_Surprised);
    
                    if (transform.localScale.x > 0) rb.velocity = new Vector2(speed, rb.velocity.y);
                    else rb.velocity = new Vector2(-speed, rb.velocity.y);
    
                    break;
    
                case EnemyStates.Charger_Surprised:
                    rb.velocity = new Vector2(0, jumpForce);
                    ChangeState(EnemyStates.Charger_Charge);
                    break;
                
                case EnemyStates.Charger_Charge:
                    timer += Time.deltaTime;
    
                    if(timer < chargeDuration)
                    {
                        if(Physics2D.Raycast(transform.position, Vector2.down, ledgeCheckY, whatIsGround))
                        {
                            if (transform.localScale.x > 0) rb.velocity = new Vector2(speed * chargeSpeedMultiplier, rb.velocity.y);
                            else rb.velocity = new Vector2(-speed * chargeSpeedMultiplier, rb.velocity.y);
                        }
                        else
                        {
                            rb.velocity = new Vector2(0, rb.velocity.y);
                        }
                        
                    }
                    else
                    {
                        timer = 0;
                        ChangeState(EnemyStates.Charger_Idle);
                    }
                    break;
            }
        }
    
        protected override void ChangeCurrentAnimation()
        {
            if(GetCurrentEnemyState == EnemyStates.Charger_Idle)
            {
                anim.speed = 1;
            }
            if (GetCurrentEnemyState == EnemyStates.Charger_Charge)
            {
                anim.speed = chargeSpeedMultiplier;
            }
    
        }
    
        private void OnDrawGizmosSelected()
        {
            _ledgeCheckStart = transform.localScale.x > 0 ? new Vector3(ledgeCheckX, 0) : new Vector3(-ledgeCheckX, 0);
            Gizmos.color = Color.blue;
            Gizmos.DrawRay(transform.position + _ledgeCheckStart, Vector2.down * ledgeCheckY);
        }
    }
    

    Crawler

    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    
    public class Crawler : Enemy
    {
        float timer;
        [SerializeField] private float flipWaitTime;
        [SerializeField] private float ledgeCheckX;
        [SerializeField] private float ledgeCheckY;
        [SerializeField] private LayerMask whatIsGround;
    
        Vector3 _ledgeCheckStart;
        Vector2 _wallCheckDir;
        protected override void Start()
        {
            base.Start();
            rb.gravityScale = 12f;
        }
    
        // Update is called once per frame
        protected override void Update()
        {
            base.Update();
    
            if(!PlayerController.Instance.pState.alive)
            {
                ChangeState(EnemyStates.Crawler_Idle);
            }
    
            if (!isRecoiling)
            {
                transform.position = Vector2.MoveTowards
                    (transform.position, new Vector2(PlayerController.Instance.transform.position.x, transform.position.y),
                    speed * Time.deltaTime);
            }
        }
      
    
        protected override void UpdateEnemyStates()
        {
            if (health <= 0)
            {
                Death(0.05f);
            }
    
            switch (GetCurrentEnemyState)
            {
                case EnemyStates.Crawler_Idle:
                    _ledgeCheckStart = transform.localScale.x > 0 ? new Vector3(ledgeCheckX, 0) : new Vector3(-ledgeCheckX, 0);
                    _wallCheckDir = transform.localScale.x > 0 ? transform.right : -transform.right;
    
                    if (!Physics2D.Raycast(transform.position + _ledgeCheckStart, Vector2.down, ledgeCheckY, whatIsGround) ||
                        Physics2D.Raycast(transform.position, _wallCheckDir, ledgeCheckX, whatIsGround))
                    {
                        ChangeState(EnemyStates.Crawler_Flip);
                    }
    
                    if (transform.localScale.x > 0) rb.velocity = new Vector2(speed, rb.velocity.y);
                    else rb.velocity = new Vector2(-speed, rb.velocity.y);
                    break;
                case EnemyStates.Crawler_Flip:
                    timer += Time.deltaTime;
    
                    if(timer > flipWaitTime)
                    {
                        timer = 0;
                        transform.localScale = new Vector2(transform.localScale.x * -1, transform.localScale.y);
                        ChangeState(EnemyStates.Crawler_Idle);
                    }
    
                    break;
            }
        }
    
        private void OnDrawGizmosSelected()
        {
            _ledgeCheckStart = transform.localScale.x > 0 ? new Vector3(ledgeCheckX, 0) : new Vector3(-ledgeCheckX, 0);
            Gizmos.color = Color.blue;
            Gizmos.DrawRay(transform.position + _ledgeCheckStart, Vector2.down * ledgeCheckY);
        }
    
    }
    

    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("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, timeSinceAttck;
    
        [SerializeField] private float damage; //the damage the player does to an enemy
    
        [SerializeField] private GameObject slashEffect; //the effect of the slashs
        [Space(5)]
    
        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;
        [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)]
    
        [HideInInspector] public PlayerStateList pState;
        private Animator anim;
        public Rigidbody2D rb;
        private SpriteRenderer sr;
    
        //Input Variables
        private float xAxis, yAxis;
        private bool attack = false;
        private bool canFlash = true;
    
        public static PlayerController Instance;
    
        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>();
    
            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 (pState.cutscene) return;
    
            GetInputs();
            UpdateJumpVariables();
            RestoreTimeScale();
    
            if (pState.dashing) return;
            Flip();
            Move();
            Jump();
            StartDash();
            Attack();
            FlashWhileInvincible();
            Heal();
            CastSpell();
        }
        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");
    
            if (Input.GetButton("Cast/Heal"))
            {
                castOrHealTimer += Time.deltaTime;
            }
            else
            {
                castOrHealTimer = 0;
            }
        }
    
        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");
            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;
        }
        void Attack()
        {
            timeSinceAttck += Time.deltaTime;
            if (attack && timeSinceAttck >= timeBetweenAttack)
            {
                int _recoilLeftOrRight = pState.lookingRight ? 1 : -1;
                timeSinceAttck = 0;
                anim.SetTrigger("Attacking");
    
                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);
        }
        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)
        {
            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);
    
        }
    
        public int Health
        {
            get { return health; }
            set
            {
                if (health != value)
                {
                    health = Mathf.Clamp(value, 0, maxHealth);
    
                    if (onHealthChangedCallback != null)
                    {
                        onHealthChangedCallback.Invoke();
                    }
                }
            }
        }
    
        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;
            }
        }
    
        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()
        {
            anim.SetBool("Casting", true);
            yield return new WaitForSeconds(0.15f); // check on Animator if it matches
    
            //side cast
            if (yAxis == 0 || (yAxis < 0 && Grounded()))
            {
                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;
            }
    
            //up cast
            else if (yAxis > 0)
            {
                Instantiate(upSpellExplosion, transform);
                rb.velocity = Vector2.zero;
            }
    
            //down cast
            else if (yAxis < 0 && !Grounded())
            {
                downSpellFireball.SetActive(true);
            }
    
            Mana -= manaSpellCost;
            yield return new WaitForSeconds(0.35f); // check Animator
            anim.SetBool("Casting", false);
            pState.casting = false;
        }
        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)
            {
                rb.velocity = new Vector3(rb.velocity.x, jumpForce);
    
                pState.jumping = true;
            }
            
            if (!Grounded() && airJumpCounter < maxAirJumps && Input.GetButtonDown("Jump"))
            {
                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())
            {
                pState.jumping = false;
                coyoteTimeCounter = coyoteTime;
                airJumpCounter = 0;
            }
            else
            {
                coyoteTimeCounter -= Time.deltaTime;
            }
    
            if (Input.GetButtonDown("Jump"))
            {
                jumpBufferCounter = jumpBufferFrames;
            }
            else
            {
                jumpBufferCounter--;
            }
        }
    }
    #13419
    Terence
    Keymaster
    Helpful?
    Up
    0
    ::

    Allan, can you take a video of your enemies moonwalking? In the video, show me how the prefab looks like when selected with Local / Pivot selection.

    Also, what do you mean by the Is Alive check? Because in the PlayerController there are checks for other things but there doesn’t seem to be an Is Alive check.

    #13421
    Allan Valin
    Participant
    Helpful?
    Up
    0
    ::

    Yes, because I removed it. The If Alive check was to avoid that the player to continue moving during the death screen (which I solved by deciding to not have a death screen and only using respawn lol). The code was simply to put if(…pState.alive) and inside all the controller checks that come from inputs.

    I used the capture from Windows to record (Win + Alt + R), my current pc doesn’t work well with the microphone.

    Gameplay

    Prefabs (I forgot and recorded again, the HD quality is still loading at the time of writing, in case you see it in the first half an hour after me writing)

    #13431
    Terence
    Keymaster
    Helpful?
    Up
    0
    ::

    Hi Allan, I’ve looked at your code and they seem largely fine. I didn’t find anything specifically that would cause these issues. Check your ground tiles and make sure that they belong to the correct layer (i.e. “Ground”).

    In your code I found a couple of minor differences:

    In Bat.cs, you use EnemyStates.Crawler_Idle in Update():

        protected override void Update()
        {
            base.Update();
    
            if (!PlayerController.Instance.pState.alive)
            {
                ChangeState(EnemyStates.Crawler_Idle);
            }
        }

    In your Charger.cs and Crawler.cs, the following variables are not needed:

    public class Charger : Enemy
    {
        float timer;
        [SerializeField] private float ledgeCheckX;
        [SerializeField] private float ledgeCheckY;
        [SerializeField] private float chargeSpeedMultiplier;
        [SerializeField] private float chargeDuration;
        [SerializeField] private float jumpForce;
        [SerializeField] private LayerMask whatIsGround;
    
        Vector3 _ledgeCheckStart;
        Vector2 _wallCheckDir;
        ...

    In your video, your Ledge Check Y for the Charger might be too high as well. Try setting it to 1 and see if it fixes the collision issue.

    #13432
    Terence
    Keymaster
    Helpful?
    Up
    0
    ::

    Regarding the if(pState.alive) check you mentioned, it will be best to not remove that, because it will affect the other parts of the code and may cause other bugs.

    If it is not working, just check that the Player State List component in your player prefab has Alive checked whenever you start the game.

    View post on imgur.com

    #13433
    Allan Valin
    Participant
    Helpful?
    Up
    0
    ::

    That helped fix the bat and I really didn’t click the Alive flag there. The two variables are outside the scope of UpdateEnemyStates() so that the gyzmo can have access to it (re-adding them to work only inside the method does nothing different).

    But now I noticed the following: you don’t show in the video a Flip function for the crawler and charger, maybe because they are supposed to follow a path and only react when the player is in front of them?
    But using the gizmo it’s clear that the lack of flip function makes so the detection area is always pointing right, even if the enemy is moving left.
    While it is shown on the video how you implement the Flip function for the bat, nothing is shown for the other enemies. Later in the video you tell to summon the flip function for them, but you never implemented them in the first place (I copied it from the bat, but it clearly doesn’t work).

    What I see as a problem is that the crawler and charger are supposed to only react to the player if he is in their detection area, but the code you give makes it so that they always are moving towards the player (like the bat) and are even able to detect him according to the Y-axis, which seem to be wrong in my opinion (that leads them to get stuck in place when the player is below them).

    How should their flip function work, so that the detection area moves left if they are moving in negative x values? Also, how to avoid them to react to the player when he shouldn’t be visible to them? I don’t get how the “same code” would work in the video and not for me (to be honest it’s clear that both of you guys have different codes when doing a video after the other, even function names are different sometimes, like instead of EnemyHit it’s like EnemyIsHit or whatever).

    #13434
    Allan Valin
    Participant
    Helpful?
    Up
    0
    ::

    I fixed the Charger by moving the MoveTowardsfrom Updateto inside of case EnemyStates.Charger_Surprised and clicking Flip: X in the Sprite Renderer. I changed the values to the ones mentioned in the video again, but the chargers then runs to the edge of the platform, like coyote time, and gets stuck there for some reason (it won’t run down the platform). It takes a couple surprised jumps to make it work again.

    Charger

    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    
    public class Charger : Enemy
    {
        float timer;
        [SerializeField] private float ledgeCheckX;
        [SerializeField] private float ledgeCheckY;
        [SerializeField] private float chargeSpeedMultiplier;
        [SerializeField] private float chargeDuration;
        [SerializeField] private float jumpForce;
        [SerializeField] private LayerMask whatIsGround;
    
        //Vector3 _ledgeCheckStart;
        //Vector2 _wallCheckDir;
    
        protected override void Start()
        {
            base.Start();
            ChangeState(EnemyStates.Charger_Idle);
            rb.gravityScale = 12f;
        }
    
        // Update is called once per frame
        protected override void Update()
        {
            base.Update();
    
            if (!PlayerController.Instance.pState.alive)
            {
                ChangeState(EnemyStates.Charger_Idle);
            }
    
            
        }
    
        protected override void UpdateEnemyStates()
        {
            if (health <= 0)
            {
                Death(0.05f);
            }
            Vector3 _ledgeCheckStart = transform.localScale.x > 0 ? new Vector3(ledgeCheckX, 0) : new Vector3(-ledgeCheckX, 0);
            Vector2 _wallCheckDir = transform.localScale.x > 0 ? transform.right : -transform.right;
    
            switch (GetCurrentEnemyState)
            {
                case EnemyStates.Charger_Idle:
                    
    
                    if (!Physics2D.Raycast(transform.position + _ledgeCheckStart, Vector2.down, ledgeCheckY, whatIsGround) ||
                        Physics2D.Raycast(transform.position, _wallCheckDir, ledgeCheckX, whatIsGround))
                    {
                        transform.localScale = new Vector2(transform.localScale.x * -1, transform.localScale.y);
                    }
    
                    RaycastHit2D _hit = Physics2D.Raycast(transform.position + _ledgeCheckStart, _wallCheckDir, ledgeCheckX * 10);
                    if (_hit.collider != null && _hit.collider.gameObject.CompareTag("Player")) ChangeState(EnemyStates.Charger_Surprised);
    
                    if (transform.localScale.x > 0) rb.velocity = new Vector2(speed, rb.velocity.y);
                    else rb.velocity = new Vector2(-speed, rb.velocity.y);
    
                    break;
    
                case EnemyStates.Charger_Surprised:
                    rb.velocity = new Vector2(0, jumpForce);
                    ChangeState(EnemyStates.Charger_Charge);
                    
                    
                    if (!isRecoiling)
                    {
                        transform.position = Vector2.MoveTowards
                            (transform.position, new Vector2(PlayerController.Instance.transform.position.x, transform.position.y),
                            speed * Time.deltaTime);
                    }
    
                    break;
                
                case EnemyStates.Charger_Charge:
                    timer += Time.deltaTime;
    
                    if(timer < chargeDuration)
                    {
                        if(Physics2D.Raycast(transform.position, Vector2.down, ledgeCheckY, whatIsGround))
                        {
                            if (transform.localScale.x > 0) rb.velocity = new Vector2(speed * chargeSpeedMultiplier, rb.velocity.y);
                            else rb.velocity = new Vector2(-speed * chargeSpeedMultiplier, rb.velocity.y);
                        }
                        else
                        {
                            rb.velocity = new Vector2(0, rb.velocity.y);
                        }
                        
                    }
                    else
                    {
                        timer = 0;
                        ChangeState(EnemyStates.Charger_Idle);
                    }
                    break;
            }
        }
    
        protected override void ChangeCurrentAnimation()
        {
            if(GetCurrentEnemyState == EnemyStates.Charger_Idle)
            {
                anim.speed = 1;
            }
            if (GetCurrentEnemyState == EnemyStates.Charger_Charge)
            {
                anim.speed = chargeSpeedMultiplier;
            }
    
        }
        /*
        private void OnDrawGizmosSelected()
        {
            _ledgeCheckStart = transform.localScale.x > 0 ? new Vector3(ledgeCheckX, 0) : new Vector3(-ledgeCheckX, 0);
            Gizmos.color = Color.blue;
            Gizmos.DrawRay(transform.position + _ledgeCheckStart, Vector2.down * ledgeCheckY);
        }
        */
    }
    #13435
    Allan Valin
    Participant
    Helpful?
    Up
    0
    ::

    The Crawler had the problem of the edge detection being written inside the case-switch for some reason lol

    I just decided to have it not react to the player, because otherwise there wouldn’t be much of a difference from it and the charger, except the speeds and the latter jumping before speeding up. To do that, I simply used the standard Updatemethod. I also had to Flip X in the Sprite renderer (that seems to have been the main problem all along).

    #13436
    Terence
    Keymaster
    Helpful?
    Up
    0
    ::

    Glad you managed to fix your issue. Here is the Flip() function from the Part 6 article, in case it helps:

    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;
        }
    }

    Thanks for documenting your issues here as well. I’ll be assembling a team to check through and refresh the content for this series in mid March. As you’ve mentioned, there are some synchronicity issues that we have to address in the videos.

    #13437
    Terence
    Keymaster
    Helpful?
    Up
    0
    ::

    By the way, I’ve given you a few more bug reporter / content contributor badges as thanks for all your prior topics documenting all the issues. Thanks again for your help brother.

Viewing 10 posts - 1 through 10 (of 10 total)
  • You must be logged in to reply to this topic.

Go to Login Page →


Advertisement below: