Back
Pack Up And Leaf
Info

Role: Gameplay Programmer & VFX

Genre: 3D Platformer

Engine: Unity

Team Size: 11

Platform: PC

Time: 4 weeks

About

Pack Up & Leaf is a 3D Adventure Platformer game inspired by old classics like Spyro and Banjo Kazooie. Being a platformer and having a broad target audience, the game heavily depends on how the player's controls feel and how easy they are to pick up. That's the challenge we took which required extensive testing and tweaking.

Highlights
  • Third person camera controller that can orbit or follow and rotate towards a target if there's no player input. It also will react to collision and occlusion as expected and adjust its distance to the target accordingly. Camera also has a free move zone as seen in many AAA games.
  • Camera management system to easily possess and control other cameras during runtime. Used mostly for switching cameras to play cutscenes.
  • Modular interactable object system
Third Person Camera
OrbitCamera.cs CameraController.cs CameraCollision.cs

Since platformers depend so much on how the movement and camera feels, I took the latter as my main focus for this project. I had to work very closely with the programmer that was working on the player’s movement to make sure that both components fit seamlessly. There’s also a lot of dependencies between camera and movement so we had to plan ahead to make sure the needed properties were easily accessible across components.

Orbit Camera

The player can move the camera freely around the target in a sphere with radius. The vertical angle is constrained as expected. The euler angles are also looped so that when the angle reaches 360, it starts back at 0.

Property: Function:
Target Target's Transform component
Radius Orbit sphere radius
Orbit Angles Yaw and Pitch
Angle Constraints Min. and max. pitch angles
Code - Orbit Camera
private void ExecuteOrbit()
{
    ConstrainAngles();
    LoopAngle();
    Quaternion newRotation = Quaternion.Euler(new Vector3(orbitAngles.y, orbitAngles.x, 0f));
    _lookRotation = newRotation;
    
    // looking at target
    Vector3 lookDirection = _lookRotation * Vector3.forward;
    Vector3 lookPosition = _focusPoint - lookDirection * currentRadius;
    
    // set pos and rotation
    transform.SetPositionAndRotation(lookPosition, _lookRotation);
}
Follow Camera

Camera will smoothly rotate behind the player when outside the free zone area. If the player is facing the camera, then the camera won’t try to rotate behind the player and it will simply follow its position. The camera will also reset to its default position behind the player when there's no input from the player.


Controller support was one of the goals for this game so it was crucial that the camera could follow the player smoothly when there’s no camera input.

Property: Function:
Default Height Pitch angle to default to
Facing Angle Angle threshold when target is facing camera
Code - Follow Camera
private void ExecuteFollow()
{
    LoopAngle();
    Vector3 direction = target.position - _focusPoint;
    if (direction.magnitude < 0.1f)
    {
        transform.rotation = transform.localRotation;
        return;
    }

    // Convert angles here

    #region Orbit
    // speed for when inside and outside free zone area
    float speed = direction.magnitude < focusRadius - 0.01f ? 0.1f : rotationSpeed;
    
    if (!isFacingCamera)
    {
        if (GameManager.Instance.GetPlayerEntity().GetComponent<PlayerManager>().isGrounded)
        {
            orbitAngles.x = Mathf.LerpAngle(orbitAngles.x, relativeAngle, speed * Time.unscaledDeltaTime);
        }
        else
        {
            orbitAngles.x = Mathf.LerpAngle(orbitAngles.x, relativeAngle, onAirRotationSpeed * Time.unscaledDeltaTime);
        }
    }

    orbitAngles.y = Mathf.LerpAngle(orbitAngles.y, defaultHeight, speed * Time.unscaledDeltaTime);
    Quaternion newRotation = Quaternion.Euler(new Vector3(orbitAngles.y, orbitAngles.x, 0f));
    _lookRotation = newRotation;
    
    // looking at target
    Vector3 lookDirection = _lookRotation * Vector3.forward;
    Vector3 lookPosition = _focusPoint - lookDirection * currentRadius;
    
    // set pos and rotation
    transform.SetPositionAndRotation(lookPosition, _lookRotation);
    #endregion
}
Free Move Zone

The free move zone is an area where the player can move with very little to no camera reaction. The camera targets this area which will move towards the player position depending on how far it is from the center. In our case, we’re just using two easing functions to lerp the position depending if you’re inside or outside the zone.

Property: Function:
Focus Radius Free move zone radius
Focus Centering How fast to center back to target's position
Code - Updating Focus Point
private void UpdateFocusPoint()
{
    Vector3 targetPoint = target.position;
    if (focusRadius > 0f)
    {
        float distance = (targetPoint - _focusPoint).magnitude;
        float t = 1f;
        if (distance > 0.001f && focusCentering > 0f)
        {
            t = Mathf.Pow(1f - focusCentering, Time.unscaledDeltaTime);
        }
        if (distance > focusRadius)
        {
            t = Mathf.Min(t, focusRadius / distance);
        }
        _focusPoint = Vector3.Lerp(targetPoint, _focusPoint, t);
    }
    else
    {
        _focusPoint = targetPoint;
    }
}
Collision Handling

If there’s an object between the target and the camera, the camera will move closer to the target in order to clear it and so it’s not in front anymore. Both collision and occlusion were solved using the following method:

  • 5 Raycasts are "shot" from the camera to the target's position: 4 from each corner and 1 from the center.
  • The shortest distance from the hit points to the target is calculated and the camera radius is set to this value.

CameraCollision.cs
Code - Collision with clip points
        if (CollisionWithClipPoints(clipPoints, _orbitCamera.Target.position))
        {
            // Shortest collision distance
            _minDistance = GetShortestDistance(clipPoints, _orbitCamera.Target.position);
            _minDistance = Mathf.Clamp(_minDistance, 0, _orbitCamera.Radius);
            _orbitCamera.CurrentRadius = Mathf.SmoothDamp(_orbitCamera.CurrentRadius, _minDistance, ref _camVelocity, smoothTime);

            colliding = true;
        }
        else
        {
            _minDistance = _orbitCamera.Radius;
            _orbitCamera.CurrentRadius = Mathf.SmoothDamp(_orbitCamera.CurrentRadius, _minDistance, ref _camVelocity, smoothReset);
            colliding = false;
        }
    }
Interactions

Using events to invoke different interactions depending on the interacted object. This allowed for a very modular and reusable system.

When interacting with chests, loot will spawn around the object between a range of distance and the instances will never overlap.

Code - Loot dropping
public void DropRandom()
    {
        int lootToSpawn = Random.Range(lootRange.Min, lootRange.Max);
        for (int i = 0; i < lootToSpawn; i++)
        {
            float size = Random.Range(distanceRange.Min, distanceRange.Max);
            
            float angle = i * (2 * Mathf.PI / lootToSpawn);
            float xPos = Mathf.Cos(angle);
            float zPos = Mathf.Sin(angle);
            
            Vector3 position = transform.position + new Vector3(xPos, 0f, zPos) * size + Vector3.up * spawnOffset;
    
            if (loots.Length == 0) { return; }
            int index = RandomIndex();
            GameObject instance = Instantiate(loots[index].Prefab, position, Quaternion.identity);
        }
    }
    
Shaders & VFX
Main Takeaways
© 2022 João Freire. All rights reserved.