Metroid clone ball help

I have a player, which can turn into a ball (Metroid-clone)
I have a tile map, inside a grid (Layer is ground).
I also have an empty game object named boundaries (That represent the boundaries of the camera) and obviously the layer for this is “No collision”

I went to my project setting and turn off all the interaction with that layer

void Update() {
  
            #region Check if the player is grounded
  
            _isGrounded = Physics2D.OverlapCircle(_groundPoint.position, 0.3f, _whatIsGround);
  
            #endregion
  
            #region Handle dashing and Movement
  
            if (Input.GetKeyDown(KeyCode.Z) && _canDash && _playrSprites[0].activeSelf) {
                StartCoroutine(DashCoroutine(0.5f));
            } else {
                float horizontalInput = Input.GetAxisRaw("Horizontal");
  
                _rb2D.velocity = new Vector2(horizontalInput * _moveSpeed, _rb2D.velocity.y);
  
                SetLocalScale();
            }
  
  
            #endregion
  
            #region Handle jumping
            if (Input.GetKeyDown(KeyCode.X) && (_isGrounded || _canDoubleJump)) {
  
                if (_isGrounded) {
                    _canDoubleJump = true;
                } else {
                    _canDoubleJump = false;
  
                    _animator.SetTrigger("isDoubleJumping");
                }
  
                _rb2D.velocity = new Vector2(_rb2D.velocity.x, _jumpForce);
  
            } else if (Input.GetKeyUp(KeyCode.X) && _rb2D.velocity.y > 0) {
                _rb2D.velocity = new Vector2(_rb2D.velocity.x, _jumpForce * 0.5f);
            }
  
  
            #endregion
  
            #region Handle shooting
  
            if (Input.GetKeyDown(KeyCode.C) && !_isBall) {
                Instantiate(_bullets, _shotPoint.position, Quaternion.identity).ChangeDirr(transform.localScale.x);
                _animator.SetTrigger("fireShot");
            }
            #endregion
  
            #region Ball mode
  
            if (Input.GetKeyDown(KeyCode.X)) {
                if (!_isBall) {
                    StartCoroutine(TurnOnBallCoroutine(1.5f));
                }
            }
            if (Input.GetKeyUp(KeyCode.X)) {
                RaycastHit2D hit = Physics2D.Raycast(transform.position, Vector2.up, raycastDistance);
                Debug.DrawRay(transform.position, Vector2.up * raycastDistance, rayColor);
                Debug.Log(hit.collider.name);
                if (hit.collider == null) { // Raycast did not hit anything, allow turning off ball mode
                    Debug.Log("turning off ball mode");
                    TurnOffBall();
                }
            }
  
            #endregion
  
            // Update the animator variables
            _animator.SetFloat("isRunning", Mathf.Abs(_rb2D.velocity.x));
            _animator.SetBool("isGrounded", _isGrounded);
        }
  
        IEnumerator DashCoroutine(float dashDuration) {
            float timer = 0f;
            while (timer < dashDuration) {
                _canDash = false;
                _isDashing = true;
                _rb2D.velocity = new Vector2(_dashSpeed * transform.localScale.x, _rb2D.velocity.y);
                timer += Time.deltaTime;
                ShowAfterImage();
                yield return null;
            }
            _canDash = true;
            _isDashing = false;
        }
  
        IEnumerator TurnOnBallCoroutine(float time) {
  
            _isBall = true;
            yield return new WaitForSeconds(time);
            Debug.Log("ball");
            TurnOnBall();
        }
  
        private void SetLocalScale() {
            if (Input.GetAxisRaw("Horizontal") > 0) {
                transform.localScale = new Vector3(1, 1, 1);
            } else if (Input.GetAxisRaw("Horizontal") < 0) {
                transform.localScale = new Vector3(-1, 1, 1);
            }
        }
  
        private void ShowAfterImage() {
            if (Time.time >= _nextAfterImageSpawnTime) {
                SpriteRenderer _spriteRenderer = Instantiate(_afterImageSpriteRender,
                    transform.position,
                    transform.rotation);
  
                _spriteRenderer.sprite = _mySpriteRenderer.sprite;
                _spriteRenderer.transform.localScale = transform.localScale;
                _spriteRenderer.color = _afterImageColor;
  
                Destroy(_spriteRenderer.gameObject, _afterImageLifeTime);
                _nextAfterImageSpawnTime = Time.time + _afterImageSpacing;
            }
        }
  
        private void TurnOnBall() {
            Debug.Log("Turned Ball");
            _isBall = true;
            _playrSprites[1].SetActive(true);
            _playrSprites[0].SetActive(false);
  
        }
  
        private void TurnOffBall() {
            Debug.Log("Turn off ball");
            _isBall = false;
            _playrSprites[1].SetActive(false);
            _playrSprites[0].SetActive(true);
        }

So, I have my ball, to be a child of my player, and by default is not active, and if I hold the X key, I run a coroutine, that turns my ball on, and I added a raycast, to see if I have something above me, if this is the case I don’t turn of the ball
But I have some problems with this

  • if I jump, when I hit the floor, I change into a ball
  • I can’t turn back’
    If I print what the rayast hit, it was the “Camera boundaries” and of course I is not null


[1]: /storage/temp/205319-screenshot-2023-03-05-055207.png
[2]: /storage/temp/205320-screenshot-2023-03-05-055219.png

It’s not clear specifically what you’re asking here.

Are you simply saying that you perform a raycast and don’t expect to “hit” the “Camera boundaries”? If so, you should see that you’re not specifying which layer(s) you want to hit as you do in the OverlapCircle call.

That works, many thanks, now I cannot turn back if there is something in the way perfect

I have another problem

#region Ball mode

        if (Input.GetKeyDown(KeyCode.X)) {
            if (!_isBall) {
                StartCoroutine(TurnOnBallCoroutine(1.5f));
            }
        }
        if (Input.GetKeyUp(KeyCode.X)) {
            layerMask = LayerMask.GetMask("Ground");
            RaycastHit2D hit = Physics2D.Raycast(transform.position, Vector2.up, raycastDistance, layerMask);
            Debug.DrawRay(transform.position, Vector2.up * raycastDistance, rayColor);
            if (hit.collider == null) { // Raycast did not hit anything, allow turning off ball mode
                Debug.Log("turning off ball mode");
                TurnOffBall();
            }

IEnumerator TurnOnBallCoroutine(float time) {
        _isBall = true;
        yield return new WaitForSeconds(time);
        Debug.Log("ball");
        TurnOnBall();
    }
    }
    private void TurnOnBall() {
        Debug.Log("Turned Ball");
        _isBall = true;
        _playrSprites[1].SetActive(true);
        _playrSprites[0].SetActive(false);
    }
    private void TurnOffBall() {
        Debug.Log("Turn off ball");
        _isBall = false;
        _playrSprites[1].SetActive(false);
         _playrSprites[0].SetActive(true);

Demo

problem

Sometimes when I jump, I tune into a ball for no apparent reason.

I should be only allowed, to turn into a ball, if I hold the x button

and then I must press x again to turn off ball mode

What I want to do

Press and hold, to turn into a ball, if my raycast doesn’t collide with anything, then I return to normal

and I assumed that

Input.GetKeyUp(KeyCode.X)) would take care of this

Looking at your code you are using X for turning into a ball, but also using X to do a jump or double-jump. Change the key that you use for either the ball or the jump to be different, otherwise you are getting a clash between the two bits of code.

Yes, I know but I want to turn into a ball when I hold it for some time (1.5).
If a press the I should doble jump

as I understand, one thing is pressing another thing is holding, there shouldn’t be any conflict

Not sure how you think there is no conflict as I can see nothing in your code where you do a counter to determine if the player has held the X key for any duration of time. GetKeyDown will simply be true if the key has been pushed down this frame, not after any amount of time and not if you are holding it. From the manual for GetKeyDown: “Returns true during the frame the user starts pressing down the key”

So you have a conflict. As soon as you press X it will try to jump/double jump if possible, but then within the same frame it will check that same key down press and it will also change you into the ball.

You need to rethink the way you are doing it so that the keys are mapped differently, or so that you use a timer to check if the key is being held and then released within a certain amount of time to figure out if it should be a jump (key not held long enough) or change into the ball (key was held long enough).

I thought my courutine was supposed to take care of that that is 1.5 seconds

Btw thank you so much for helping me, and I am sorry if my questions are kind of stupid

Nope, afraid not. All that is doing is putting a delay in before it runs the code after WaitForSeconds. At a first thought about what you are wanting, you would need to have a timer variable and a threshold variable (for checking how long the player needs to hold the key to change into the ball) and use GetKey instead of GetKeyDown. In the GetKey check you would just increase your timer by Time.deltaTime. Then all of your jump and ball logic would be in a GetKeyUp, and in there you would check the timer and if it is above your threshold value it would do the ball change, otherwise (it’s less than the threshold) it would do the jump/double-jump logic. Then regardless of whether it is the ball change or the jump, you would reset the timer to 0 ready for the next time the player presses the key.

No worries. Everyone starts somewhere, and I’m one of the first to say that I am probably only intermediate level in Unity. There are very few stupid questions.

Coud you provide a basic example

I’m not trying to tell you how to design your own game, but just an opinion: that is a very poor control setup.

Most players will instinctively hold down the jump button after performing a jump (whether it’s a single or double-jump) simply because most games–even Metroid and Super Metroid!–feature a variable jump height based on how long the button has been held.

Instead, you ought to strongly consider either moving it to an entirely different button or just handling it the same way the old Metroid games do by requiring players to press down to morph.

Thank you so much. I put it in the down arrow.

But I have an unexpected behavior, I want the player to be able to turn off ball mode, as soon as I don’t have anything above my head.

In other wor

        if (Input.GetKeyDown(KeyCode.DownArrow) && !_isBall) {
            TurnOnBall();
            int layerMask = LayerMask.GetMask("Ground");
            RaycastHit2D hit = Physics2D.Raycast(transform.position, Vector2.up, raycastDistance, layerMask);
            Debug.DrawRay(transform.position, Vector2.up * raycastDistance, rayColor);
        }
        if (Input.GetKeyUp(KeyCode.DownArrow) && _isBall) {
            int layerMask = LayerMask.GetMask("Ground");
            RaycastHit2D hit = Physics2D.Raycast(transform.position, Vector2.up, raycastDistance, layerMask);
            Debug.DrawRay(transform.position, Vector2.up * raycastDistance, rayColor);
            if (hit.collider != null) {
                return;
            } // Raycast did not hit anything, allow turning off ball mode
            TurnOffBall();

ds, I want you to hold down the arrow down to turn ball mode and turn it of if you release the down arrow and there is no ground above you

this works but if i enter a narrow space, I must press again the down arrow, to return to normal

recording-2023-03-07-00-19-17.webm (reccloud.com)

as you can see when I exit the tunnel and lift the key, it doesn’t turn off ball mode