What I learned building my first game Ninja Frog

Posted on

Ninja Frog was my very first adventure into the world of game development. I set out with the goal of learning the basics of Unity and C#, and by the end of the project I feel I accomplished just that.

My favorite game console of all time is Super Nintendo, so it only felt right to kick things off with a 2D platformer similar to my favorite Mario games.

Video of Ninja Frog first level game play.

I began the project by following a YouTube course by Coding with Flow. This course taught me the basics of the Unity interface, sprites, tilemaps, and C# classes, among other things.

Towards the end of the course, with the help of some friends, I began adding some of my own features and refactoring some of the course code. Below are some of my most important learnings.

Events

Besides the basics, I would say the most important concept I learned during the course of the project is events. Emitting events and subscribing to them has been incredibly helpful throughout my project and allowed me to decouple components cleanly.

public class PlayerDeath : MonoBehaviour {
    // snip
    public event Action OnDeath;

    // snip

    void Die() {
        // snip
        OnDeath?.Invoke();
    }
}
public class PlayerAudio : MonoBehaviour {
    // Snip

    void OnEnable() {
        playerDeath.OnDeath += PlayDeathSound;
    }

    void OnDisable() {
        playerDeath.OnDeath -= PlayDeathSound;
    }

    void PlayDeathSound() {
        soundPlayer?.PlaySound(SoundEffect.Death);
    }
}

In the example above, I’m emitting an OnDeath event from my PlayerDeath class so the audio manager can play the appropriate sound whenever the player dies.

The new Unity input system

The first time I shared a demo of my game, I was asked if I was using the new Unity input system. I thought, “What? A new input system?” and soon discovered I was not.

For those who don’t know, Unity has a revamped input system that you have to add via the package manager. The new input system makes multi-device support a breeze and allows you to switch between multiple input maps if needed.

The Unity Input Manager package
The Unity input manager package

Scriptable objects

I’m not going to lie, these are still confusing to me, but I think scriptable objects allow me to create more of a global component that only gets loaded once. I’ve been told they’re very useful for managers.

I made use of them a few places:

  1. The audio manager
  2. The input manager
  3. The pause manager
using System;
using UnityEngine;
using UnityEngine.InputSystem;

[CreateAssetMenu(fileName = "Player Input Manager", menuName = "ScriptableObjects/Managers/PlayerInputManager")]
public class PlayerInputManager : ScriptableObject {
    // Inputs
    public InputActions Input { get; private set; }

    // Events
    public event Action<InputActionMap> OnActionMapChanged;
    public event Action<Vector2> OnMovementChange;
    public event Action OnJumpPress;
    public event Action OnJumpRelease;
    public event Action OnStartPress;

    public event Action<Vector2> OnMenusMovementChanged;
    public event Action OnMenusSelectPress;


    void OnEnable() {
        Input ??= new InputActions();

        // Player Movement
        Input.Player.Movement.performed += MovementChanged;
        Input.Player.Movement.canceled += MovementChanged;
        Input.Player.Jump.performed += JumpPressed;
        Input.Player.Jump.canceled += JumpReleased;
        Input.Player.Start.performed += StartPressed;
        Input.Player.Enable();

        // Menus
        Input.Menus.Movement.performed += MenusMovementChanged;
        Input.Menus.Select.performed += MenusSelectPressed;
    }

    void OnDisable() {
        Input.Player.Movement.performed -= MovementChanged;
        Input.Player.Movement.canceled -= MovementChanged;
        Input.Player.Jump.performed -= JumpPressed;
        Input.Player.Jump.canceled -= JumpReleased;
        Input.Player.Start.performed -= StartPressed;

        Input.Menus.Movement.performed -= MenusMovementChanged;
        Input.Menus.Select.performed -= MenusSelectPressed;
    }

    void MovementChanged(InputAction.CallbackContext obj) {
        OnMovementChange?.Invoke(obj.ReadValue<Vector2>());
    }

    void JumpPressed(InputAction.CallbackContext obj) {
        OnJumpPress?.Invoke();
    }

    void JumpReleased(InputAction.CallbackContext obj) {
        OnJumpRelease?.Invoke();
    }

    void StartPressed(InputAction.CallbackContext obj) {
        OnStartPress?.Invoke();
    }

    void MenusMovementChanged(InputAction.CallbackContext obj) {
        OnMenusMovementChanged?.Invoke(obj.ReadValue<Vector2>());
    }

    void MenusSelectPressed(InputAction.CallbackContext obj) {
        OnMenusSelectPress?.Invoke();
    }

    public void ChangeActionMap(InputActionMap actionMap) {
        if (!actionMap.enabled) {
            Input.Disable();
            OnActionMapChanged?.Invoke(actionMap);
            actionMap.Enable();
        }
    }
}

In the example above, I combined all three concepts together to create a centralized place to handle everything input-related.

Cinemachine

I also discovered that Unity offers another package called Cinemachine, which allows you to create better and more dynamic cameras. With the help of a this video, I was able to add a much smoother camera to my game, complete with easing and boundaries.

Cinemachine overlay showing lookahead and boundaries

Wrapping up

That’s it! Overall, I would say the project was a success. I’m super happy with the finished demo and am grateful for everything I learned.