Home > Mobile >  Problems after adding a stamina system
Problems after adding a stamina system

Time:11-24

I've added a stamina system in my game, but when I try do call the function there's a few problems happening: I can't jump when my character is sprinting and when I jump and press the sprint button my character doesn't fall anymore, he basically just flies.

PlayerController
private Vector3 playerVelocity;
private bool groundedPlayer;

private CharacterController controller;
private PlayerControls playerControls;
private InputManager inputManager;
public HealthBar healthBar;
public StaminaBar staminaBar;

public int currentHealth;
public int maxHealth = 100;
public int currentStamina;
public int maxStamina = 100;
public int staminaDrain = 10;

[SerializeField]
private float playerSpeed = 2.0f;
[SerializeField]
private float playerRunSpeed= 1f;
[SerializeField]
private float jumpHeight = 1.0f;
[SerializeField]
private float gravityValue = -9.81f;

private Transform cameraTransform;

private void Start()
{  
    currentHealth = maxHealth;
    healthBar.SetMaxHealth(maxHealth);
    currentStamina = maxStamina;
    staminaBar.SetMaxStamina(maxStamina);
    
    controller = GetComponent<CharacterController>();
    inputManager = InputManager.Instance;
    cameraTransform = Camera.main.transform;
    //player = GameObject.Find("Player");
}


void Update()
{
    groundedPlayer = controller.isGrounded;
    if (groundedPlayer && playerVelocity.y < 0)
    {
        playerVelocity.y = 0f;
    }

    Vector2 movement = inputManager.GetPlayerMovement();
    Vector3 move = new Vector3(movement.x, 0f, movement.y);
    move = cameraTransform.forward * move.z   cameraTransform.right * move.x;
    move.y = 0f;
    //controller.Move(move * Time.deltaTime * playerSpeed);
    
    if(inputManager.isRunning && currentStamina > 0)
    {
        controller.Move(move * playerRunSpeed * Time.deltaTime);
        staminaBar.UseStamina(staminaDrain);
        staminaBar.staminaSlider.value = currentStamina;
    }
    else
    {
        controller.Move(move * Time.deltaTime * playerSpeed);
    }

    

    // Changes the height position of the player..

    if (inputManager.PlayerJumpedThisFrame() && groundedPlayer)
    {
        playerVelocity.y  = Mathf.Sqrt(jumpHeight * -3.0f * gravityValue);
    }

    playerVelocity.y  = gravityValue * Time.deltaTime;
    controller.Move(playerVelocity * Time.deltaTime);
}

StaminaBar script

public class StaminaBar : MonoBehaviour
{
    public Slider staminaSlider;
    private PlayerController playerController;
    
    
    private WaitForSeconds regenTick = new WaitForSeconds(0.1f);
    private Coroutine regen;

    public void SetMaxStamina(int stamina){
        staminaSlider.maxValue = stamina;
        staminaSlider.value = stamina;

    }
    public void SetStamina(int stamina){
        staminaSlider.value = stamina;
    }

    public void UseStamina(int amount){
        if(playerController.currentStamina - amount >= 0){
            playerController.currentStamina -= amount;
            staminaSlider.value = playerController.currentStamina;
            Debug.Log("Losing Stamina");
            if(regen != null)
                StopCoroutine(regen);

            regen = StartCoroutine(RegenStamina());
        }
        else
        {
            Debug.Log("NotEnoughStamina");
        }
    }

    private IEnumerator RegenStamina()
    {
        yield return new WaitForSeconds(2);


        while(playerController.currentStamina < playerController.maxStamina){
            playerController.currentStamina  = playerController.maxStamina/100;
            staminaSlider.value = playerController.currentStamina;
            yield return regenTick;
        }
        regen = null;
    }
}

Input Manager

{
    private StaminaBar staminaBar;
    private PlayerController playerController;
    [SerializeField]
    private float bulletHitMissDistance = 25f;
    [SerializeField]
    private Transform bulletParent;
    [SerializeField]
    private Transform barrelTransform;
    [SerializeField]
    private GameObject bulletPrefab;

    [SerializeField]
    private float damage = 100;
    public float impactForce = 30;
    public float fireRate = 8f;
    WaitForSeconds rapidFireWait;

    public bool isRunning;
 

    private static InputManager _instance;
    public static InputManager Instance 
    {
        get {
            return _instance;
        }
    }

   private PlayerControls playerControls;
   private Transform cameraTransform;
   Coroutine fireCoroutine;


   private void Awake()
   {  
        if(_instance != null && _instance != this)
        {
            Destroy(this.gameObject);
        }
        else 
        {
            _instance = this;
        }
        playerControls = new PlayerControls();
        //Cursor.visible = false;


        rapidFireWait = new WaitForSeconds(1/fireRate);
        cameraTransform = Camera.main.transform;

        
        playerControls.Player.RunStart.performed  = x => Running();
        playerControls.Player.RunEnd.performed  = x => RunningStop();
        playerControls.Player.Shoot.started  = _ => StartFiring();
        playerControls.Player.Shoot.canceled  = _ => StopFiring();
   }


   private void  OnEnable()
   {
    playerControls.Enable();
    //playerControls.Player.Shoot.performed  = _ => StartFiring();
   }

   private void OnDisable()
   {
    playerControls.Disable();
    //playerControls.Player.Shoot.performed  = _ => StopFiring();
   }

    void StartFiring()
    {
     fireCoroutine = StartCoroutine(RapidFire());
    }

    void StopFiring()
    {
        if(fireCoroutine != null)
        {
            StopCoroutine(fireCoroutine);
        }
    }

   public Vector2 GetPlayerMovement()
   {
    return playerControls.Player.Movement.ReadValue<Vector2>();
   }

   public Vector2 GetMouseDelta(){
    return playerControls.Player.Look.ReadValue<Vector2>();
   }

   public bool PlayerJumpedThisFrame(){
    return playerControls.Player.Jump.triggered;
   }

   public void Shooting()
   {      
        RaycastHit hit;
        //creates the bullet
        GameObject bullet = GameObject.Instantiate(bulletPrefab, barrelTransform.position, Quaternion.identity, bulletParent);
        BulletController bulletController = bullet.GetComponent<BulletController>();
        //shoots the bullet forwards
        if (Physics.Raycast(cameraTransform.position, cameraTransform.forward, out hit, Mathf.Infinity))
            {   
                //checks if the bullet hit something
                bulletController.target = hit.point;
                bulletController.hit = true;
                //makes enemy take damage
                Enemy takingDamage = hit.transform.GetComponent<Enemy>();
        if (takingDamage != null) 
            {
                takingDamage.TakeDamage(damage);
            }
        //makes enemy go backwards when hit
        if(hit.rigidbody != null)
            {
                hit.rigidbody.AddForce(-hit.normal * impactForce);
            }
            }
        else 
            {
                bulletController.target = cameraTransform.position   cameraTransform.forward * bulletHitMissDistance;
                bulletController.hit = false;
            }
    }
    public IEnumerator RapidFire()
    {
        while(true)
        {
            Shooting();
            yield return rapidFireWait;
        }
    }

    public void Running()
    {
        /* if(playerController.currentStamina > 0){
            isRunning = true;
            staminaBar.UseStamina(playerController.staminaDrain);
            staminaBar.staminaSlider.value = playerController.currentStamina;
        } */
    isRunning = true;
    }

    public void RunningStop(){
        isRunning =false;
    }

}

I'm using unity new input system and tried to call the function in two different ways: in the isRunning and when I actually do the sprint function.

I was expecting the player to lose 10 stamina every time I press the sprint button, I was trying to figure that out before trying to make him lose stamina while the button is pressed.

I've seen a couple videos on YouTube, which is where I got the code from, but can't find out what I'm doing wrong when calling the function, I've had similar problems before when trying to call a TakeDamage function but I guess that's a different question.

CodePudding user response:

So here is what I would do.

Instead of controlling the stamina in multiple places and hve forth and back references (=dependencies) between all your scripts I would rather keep this authority within the PlayerController.

Your StaminaBar component should be purely listening and visualizing the current value without having the authority to modify it.

Next step would be to decide for a general code structure

  • Who is responsible for what?
  • Who knows / controls what?

There are many possible answers to those but for now an this specific case

  • You can either say the PlayerController "knows" the StaminaBar just like it also knows the InputManager and can't live without both
  • Or you could decouple them and let the PlayerController work without having the visualization via the StaminaBar but rather let the StaminaBar listen to the value and just display it .. or not if you want to remove or change this later on

Personally I would go with the second so I will try and give you an example how I would deal with this:

public class PlayerController : MonoBehaviour
{
    [Header("Own References")]
    [SerializeField] private CharacterController _controller;

    [Header("Scene References")]
    [SerializeField] private Transform _cameraTransform;
    [SerializeField] private InputManager _inputManager;

    // In general always make you stuff as encapsulated as possible
    // -> nobody should be able to change these except you via the Inspector
    // (Values you are anyway not gonna change at all you could also convert to "const")
    [Header("Settings")]
    [SerializeField] private float _maxHealth = 100f;
    [SerializeField] private float _maxStamina = 100f;
    [SerializeField] private float _staminaDrainPerSecond = 2f;
    [SerializeField] private float _secondsDelayBeforeStaminaRegen = 1f;
    [SerializeField] private float _staminaRegenPerSecond = 2f;
    [SerializeField] private float _playerSpeed = 1f;
    [SerializeField] private float _playerRunSpeed = 2f;
    [SerializeField] private float _jumpHeight = 1f;
    [SerializeField] private float _gravityValue = -9.81f;

    // Your runtime valus
    private float _staminaRegenDelayTimer;
    private float _currentHealt;
    private float _currentStamina;

    // You only need a single float for this
    private float _currentYVelocity;

    // EVENTS we expose so other classes can react to those
    public UnityEvent OnDeath;
    public UnityEvent<float> OnHealthChanged;
    public UnityEvent<float> OnStaminaChanged;

    // Provide public read-only access to the settings so your visuals can access those for their setup
    public float MaxHealth => _maxHealth;
    public float MaxStamina => _maxStamina;

    // And then use properties for your runtime values
    // whenever you set the value you do additional stuff like cleaning the value and invoke according events
    public float currentHealth
    {
        get => _currentHealt;
        private set
        {
            _currentHealt = Mathf.Clamp(value, 0, _maxHealth);

            OnHealthChanged.Invoke(_currentHealt);

            if (value <= 0f)
            {
                OnDeath.Invoke();
            }
        }
    }

    public float currentStamina
    {
        get => _currentStamina;
        private set
        {
            _currentStamina = Mathf.Clamp(value, 0, _maxStamina);

            OnStaminaChanged.Invoke(_currentStamina);
        }
    }

    private void Awake()
    {
        // As a thumb rule to avoid issues with order I usually initialize everything I an in Awake
        if (!_controller) _controller = GetComponent<CharacterController>();

        currentHealth = MaxHealth;
        currentStamina = MaxStamina;
    }

    private void Start()
    {
        // in start do the things were you depend on others already being initialized
        if (!_inputManager) _inputManager = InputManager.Instance;
        if (!_cameraTransform) _cameraTransform = Camera.main.transform;
    }

    private void Update()
    {
        UpdateStamina();

        UpdateHorizontalMovement();

        UpdateVerticalMovement();
    }

    private void UpdateStamina()
    {
        if (_inputManager.IsRunning)
        {
            // drain your stamina -> also informs all listeners
            currentStamina -= _staminaDrainPerSecond * Time.deltaTime;

            // reset the regen timer
            _staminaRegenDelayTimer = _secondsDelayBeforeStaminaRegen;
        }
        else
        {
            // only if not pressing run start the regen timer
            if (_staminaRegenDelayTimer > 0)
            {
                _staminaRegenDelayTimer -= Time.deltaTime;
            }
            else
            {
                // once timer is finished start regen
                currentStamina  = _staminaRegenPerSecond * Time.deltaTime;
            }
        }
    }

    private void UpdateHorizontalMovement()
    {
        var movement = _inputManager.PlayerMovement;
        var move = _cameraTransform.forward * movement.y   _cameraTransform.right * movement.x;
        move.y = 0f;
        move *= _inputManager.IsRunning && currentStamina > 0 ? _playerRunSpeed : _playerSpeed;
        _controller.Move(move * Time.deltaTime);
    }

    private void UpdateVerticalMovement()
    {
        if (_controller.isGrounded)
        {
            if (_inputManager.JumpedThisFrame)
            {
                _currentYVelocity  = Mathf.Sqrt(_jumpHeight * -3.0f * _gravityValue);
            }
            else if (_currentYVelocity < 0)
            {
                _currentYVelocity = 0f;
            }
        }
        else
        {
            _currentYVelocity  = _gravityValue * Time.deltaTime;
        }

        _controller.Move(Vector3.up * _currentYVelocity * Time.deltaTime);
    }
}

And then your StaminaBar shinks down to really only being a display. The PlayerController doesn't care/even know it exists and can fully work without it.

public class StaminaBar : MonoBehaviour
{
    [SerializeField] private Slider _staminaSlider;
    [SerializeField] private PlayerController _playerController;

    private void Awake()
    {
        // or wherever you get the reference from
        if (!_playerController) _playerController = FindObjectOfType<PlayerController>();

        // poll the setting from the player
        _staminaSlider.maxValue = _playerController.MaxStamina;

        // attach a callback to the event 
        _playerController.OnStaminaChanged.AddListener(OnStaminaChanged);

        // just to be sure invoke the callback once immediately with the current value
        // so we don't have to wait for the first actual event invocation
        OnStaminaChanged(_playerController.currentStamina);
    }

    private void OnDestroy()  
    {
        if(_playerController) _playerController.OnStaminaChanged.RemoveListener(OnStaminaChanged);
    }

    // This will now be called whenever the stamina has changed
    private void OnStaminaChanged(float stamina)
    {
        _staminaSlider.value = stamina;
    }
}

And just for completeness - I also refactored your InputManager a bit on the fly ^^

public class InputManager : MonoBehaviour
{
    [Header("Own references")]
    [SerializeField] private Transform _bulletParent;
    [SerializeField] private Transform _barrelTransform;

    [Header("Scene references")]
    [SerializeField] private Transform _cameraTransform;

    // By using the correct component right away you can later skip "GetComponent"
    [Header("Assets")]
    [SerializeField] private BulletController _bulletPrefab;

    [Header("Settings")]
    [SerializeField] private float _bulletHitMissDistance = 25f;
    [SerializeField] private float _damage = 100;
    [SerializeField] private float _impactForce = 30;
    [SerializeField] private float _fireRate = 8f;

    public static InputManager Instance { get; private set; }

    // Again I would use properties here
    // You don't want anything else to set the "isRunning" flag
    // And the others don't need to be methods either
    public bool IsRunning { get; private set; }
    public Vector2 PlayerMovement => _playerControls.Player.Movement.ReadValue<Vector2>();
    public Vector2 MouseDelta => _playerControls.Player.Look.ReadValue<Vector2>();
    public bool JumpedThisFrame => _playerControls.Player.Jump.triggered;

    private Coroutine _fireCoroutine;
    private PlayerControls _playerControls;
    private WaitForSeconds _rapidFireWait;

    private void Awake()
    {
        if (Instance != null && Instance != this)
        {
            Destroy(gameObject);
        }
        else
        {
            Instance = this;
        }

        _playerControls = new PlayerControls();

        //Cursor.visible = false;

        _rapidFireWait = new WaitForSeconds(1 / _fireRate);
        _cameraTransform = Camera.main.transform;

        _playerControls.Player.RunStart.performed  = _ => Running();
        _playerControls.Player.RunEnd.performed  = _ => RunningStop();
        _playerControls.Player.Shoot.started  = _ => StartFiring();
        _playerControls.Player.Shoot.canceled  = _ => StopFiring();
    }

    private void OnEnable()
    {
        _playerControls.Enable();
    }

    private void OnDisable()
    {
        _playerControls.Disable();
    }

    private void StartFiring()
    {
        _fireCoroutine = StartCoroutine(RapidFire());
    }

    private void StopFiring()
    {
        if (_fireCoroutine != null)
        {
            StopCoroutine(_fireCoroutine);
            _fireCoroutine = null;
        }
    }

    private void Shooting()
    {
        var bulletController = Instantiate(_bulletPrefab, _barrelTransform.position, Quaternion.identity, _bulletParent);

        if (Physics.Raycast(_cameraTransform.position, _cameraTransform.forward, out var hit, Mathf.Infinity))
        {
            bulletController.target = hit.point;
            bulletController.hit = true;

            if (hit.transform.TryGetComponent<Enemy>(out var enemy))
            {
                enemy.TakeDamage(_damage);
            }

            if (hit.rigidbody != null)
            {
                hit.rigidbody.AddForce(-hit.normal * _impactForce);
            }
        }
        else
        {
            bulletController.target = _cameraTransform.position   _cameraTransform.forward * _bulletHitMissDistance;
            bulletController.hit = false;
        }
    }

    private IEnumerator RapidFire()
    {
        while (true)
        {
            Shooting();
            yield return _rapidFireWait;
        }
    }

    private void Running()
    {
        IsRunning = true;
    }

    private void RunningStop()
    {
        IsRunning = false;
    }
}

CodePudding user response:

You're decreasing and increasing the stamina in the same scope. I think you should let the stamina to be drained when sprint is pressed and start regenerating only if it is released.

  • Related