Brut

What is Brut?

Brut is a 3D and platforming puzzle game made by an talented team at the Global Game Jam.

Inspired by Russian Suprematism you must solve visual puzzles and change the colors of the world to alter your perception and make your way through a series of abstract landscapes.

This game was born from a necessity, since we couldn’t find an art team member for the game jam, we decided to make that our strength by creating a unique visual style with simple cubes.

My responsibilities in the jam were:

  • Implementing the player movement.
  • Building the Door/Switches puzzles.

Check out the game here!

Without further ado, let’s see how the code was made without sleeping and with a bunch of Monster drinks!

Giphy

Player Movement

Given my experience making A Cruel Angel Thesis with Unity (and not having self-love), I decided to make the Player Movement using RigidBody instead of CharacterController.

PlayerMovement.cs

void HandleMovement()
    {
        Vector3 fw = cameraTransform.forward;
        fw.y = 0f;
        fw = fw.normalized;
       
        Vector3 move = cameraTransform.right * moveInput.x + fw * moveInput.y;
        Vector3 targetVelocity = move * moveSpeed;

        movement = Vector3.Lerp(movement,targetVelocity, moveAcceleration * Time.deltaTime);

        RaycastHit wallHit;
        if (Physics.Raycast(transform.position, movement.normalized, out wallHit, 0.7f, groundMask))
        {
            Vector3 slideDirection = Vector3.ProjectOnPlane(movement, wallHit.normal) ;
            movement = slideDirection;
        }

        _rigidbody.linearVelocity = new Vector3(movement.x, _rigidbody.linearVelocity.y, movement.z);

        bool hasInput = moveInput.sqrMagnitude > (minMoveThreshold * minMoveThreshold);
        if (IsGrounded() && hasInput && _stepTimer <= 0f)
        {
            SoundManager.PlaySound(SoundManager.SoundType.Steps, null, 1f, 1f);
            _stepTimer = stepInterval;
        }
    }

After making the player able to move we started thinking… what else can a player do in a platform game?… OH YES, jump!

PlayerMovement.cs

    void HandleJump()
    {
        if (_hasJumped) return;
        
        if (_jumpBufferTimer > 0f && _coyoteTimer > 0f)
        {
            _rigidbody.linearVelocity = new Vector3(
                _rigidbody.linearVelocity.x,
                jumpForce,
                _rigidbody.linearVelocity.z
            );
            
            _jumpBufferTimer = 0f;
            _coyoteTimer = 0f;
            _hasJumped = true;
            SoundManager.PlaySound(SoundManager.SoundType.Jump, null, 1f, 1f);
        }
    }

As you can see here, we also added a Coyote Jump because in the middle of my 3rd Monster drink, one of my team members told me that the Jump felt a little weird…
Here is a visual description:

Giphy
PlayerMovement.cs

void Update()
    {
        if (Player.instance.stopInput || PauseMenu._isPaused)
        {
            _rigidbody.linearVelocity = Vector3.zero;
            return;
        }

        ReadInput();
        HandleJump();
        
        // Coyote time logic
        if (IsGrounded())
        {
            _coyoteTimer = coyoteTime;
            _hasJumped = false;
        }
        else
            _coyoteTimer -= Time.deltaTime;
        
        // --- Jump buffer ---
        if (_playerInput.Player.Jump.WasPressedThisFrame())
            _jumpBufferTimer = jumpBufferTime;
        else
            _jumpBufferTimer -= Time.deltaTime;

        // decrement step timer in Update (frame-timed)
        if (_stepTimer > 0f) _stepTimer -= Time.deltaTime;
    }

Puzzles

So first we needed a way to grab an object, so I created a GrabComponent.

GrabComponent.cs

void Update()
    {
        Ray ray = new Ray(Camera.main.transform.position, Camera.main.transform.forward);
        RaycastHit hit;
        if(Physics.Raycast(ray, out hit, grabRange, grabbableLayer) || Physics.Raycast(ray, out hit, grabRange, handleLayer))
        {
            _grabPointImage.transform.localScale = Vector3.one * _grabPointScaleMultiplier;
        }
        else
        {
            _grabPointImage.transform.localScale = Vector3.one;
        }
        
        if (_playerInput.Player.Interact.WasPressedThisFrame())
        {
            if (_grabbedObject != null)
            {
                Physics.Raycast(ray, out hit, grabRange, handleLayer, QueryTriggerInteraction.Ignore);
                if (_grabbedObject.CompareTag("Key") && hit.collider)
                {
                    SoundManager.PlaySound(SoundManager.SoundType.DoorUnlock, null, 1f, 1f);
                    KeyInteract(hit);
                }
                else
                {
                    SoundManager.PlaySound(SoundManager.SoundType.ItemUngrab, null, 1f, 1f);
                    ReleaseGrab();
                }
            }
            else if (Physics.Raycast(ray, out hit, grabRange, handleLayer))
            {
                SoundManager.PlaySound(SoundManager.SoundType.DoorOpen, null, 1f, 1f);
                DoorInteract(hit);
            }
            else  if (_grabbedObject == null && Physics.Raycast(ray, out hit, grabRange, grabbableLayer))
            {
                SoundManager.PlaySound(SoundManager.SoundType.ItemGrab, null, 1f, 1f);
                TryGrab(hit);
            }
        }
    }

I know… I know… It’s ugly, look at that million ifs… it was our first Game Jam so we decided to sacrifice structure for time.

Basically what this code does is checking if we have an object or not, and whether that object can be used as a key or not.

But wait! We also need the object.

GrabbableObject.cs

    public void Update()
    {
        if (colorGameObject.active && !isGrabbed) 
        {
            idleTime += Time.deltaTime;
            if (idleTime > timeToDisable) 
            {
                colorGameObject.Respawn();
                idleTime = 0;
                isGrabbed = false;
            }
        }
    }

    public void Grab()
    {
        isGrabbed = true;
    }

    public void Release()
    {
        isGrabbed = false;
        idleTime = 0;
    }

Now, as we saw before, something happens with a key and so on… but what?? Well, a door will open, but how?

GrabComponent.cs

    void DoorInteract(RaycastHit hit)
    {
        if (hit.collider)
        {
            ColorGameObject cgo = hit.collider.GetComponent();
            if (cgo.active)
            {
                hit.collider.GetComponent()?.HandleDoor();
            }
        }
    }

    void KeyInteract(RaycastHit hit)
    {
        if (hit.collider)
        {
            ColorGameObject cgo = hit.collider.GetComponent();
            if (cgo.active)
            {
                hit.collider.GetComponent()?.SetHasKey(true);
                Destroy(_grabbedObject.gameObject);
            }
        }
    }

Sometimes there are doors that don’t need a key, and sometimes they do. But how the door handles this?

DoorHandle.cs

public class DoorHandle : MonoBehaviour
{
    public Animator doorAnimator;
    public bool needKey = false;
    private bool hasKey = false;
    
    public void HandleDoor()
    {
        if (!needKey || hasKey)
        {
            doorAnimator = GetComponentInParent();
            doorAnimator.Play("OPEN");
        }
    }
    
    public bool SetHasKey(bool value)
    {
        hasKey = value;
        HandleDoor();
        return hasKey;
    }
}

And that would be all for now.

Keep in mind that (spoiler alert) we decided to continue with this project so the code that you may see here does not reflect the current state of the game! So keep an eye on our social media to check updates of the game!

And once again, thank you so much for checking the post!

Giphy
Have Project in Mind?

Let’s turn your dreams
into reality

Scroll to Top