2D Platformer Game


Overview

This project is a 2D platformer mobile game developed in a month as part of a coursework project in 2023. It covers the core gameplay features typical of the genre and is structured around a simple finite state machine (FSM) to manage overall game flow and state transitions.

⚠️ Note: No design documents or assets were provided. I planned the game, built the system, and chose all assets myself.

View Repository
Watch Demo


Game Showcase

Reach the red flag at the top within 5 minutes. Avoid traps, navigate moving platforms, and either dodge or defeat monsters. You have 3 lives to complete the challenge.


Core Features

⚠️ Note: All code snippets are simplified versions. Full implementation in the repository

1. GameManager Class (FSM structure)

The GameManager script functions as the central controller for runtime game flow. It uses a FSM pattern to transition between three major states: PLAY, PAUSE, and GAMEOVER.

public class GameManager : MonoBehaviour
{
    enum eGameState { PLAY, PAUSE, GAMEOVER };
    eGameState m_StateBeforePause;
    eGameState m_GameState;
    
    void Start()
    {
        InPlay();
    }
    
	void Update()
	{
		switch (m_GameState)
		{
			case eGameState.PLAY:
				ModifyPlay();
				break;
			case eGameState.GAMEOVER:
				ModifyGameOver();
				break;
		}
	}
    
	// FSM Play //
    void InPlay()
    {
	    m_GameState = eGameState.PLAY;
    }
    void ModifyPlay()
    {
	    if (!Player.instance.IsAlive())
	    {
		    InGameOver();
		    return;
	    }
    }
 
	// FSM Pause //
	void InPause()
	{
		m_StateBeforePause = m_GameState;
		m_GameState = eGameState.PAUSE;
		
		Time.timeScale = 0.0f;
		UIPause.instance.Invoke_Pause(true);
		SoundManager.instance.PauseBGM();
	}
	void UnPause()
	{
		m_GameState = m_StateBeforePause;
		
		Time.timeScale = 1.0f;
		UIPause.instance.Invoke_Pause(false);
		SoundManager.instance.UnPauseBGM();
	}
 
	// FSM GameOver //
    void InGameOver()
    {
	    m_GameState = eGameState.GAMEOVER;
    }
    void ModifyGameOver()
    {
	    SceneManager.LoadScene("GameOverScene");
    }
}
InPlay(), InPause(), and InGameOver() serve as entry points for each state, while Update() calls the appropriate state handler such as ModifyPlay() or ModifyGameOver() every frame, depending on the current game state.


2. Tilemap Layer Structure

To manage both visual clarity and development efficiency, the Tilemap objects in this project were structured into a clean hierarchy. Although not code-based, this design choice played a key role in organizing gameplay elements and maintaining consistent render order.

This hierarchical setup contributed to faster iteration and more reliable scene organization.


3. Parallax Scrolling Background

Background layers scroll based on the camera’s position, which itself follows the player, creating a parallax effect tied to player movement.

This system is implemented through two core classes, CameraControl for camera tracking and BackgroundController for background scrolling based on depth.


CameraControl Class

public void TrackPlayer()
{
	Vector3 targetPos = new Vector3
	(
		m_Target.position.x + m_Offset.x,
		m_Target.position.y + m_Offset.y,
		m_FixedZ
	);
 
	transform.position = Vector3.Lerp(transform.position, targetPos, m_Smooth * Time.deltaTime);
 
	UpdateCameraBoundary();
 
	BackgroundController.instance.TrackBackGround();
}
The camera smoothly follows the player using Lerp(), then clamps its position within level bounds via UpdateCameraBoundary(), and finally updates background layers each frame.


BackgroundController Class

public void TrackBackGround()
{
	foreach (BackgroundInfo bgInfo in m_BackgroundList)
	{
		Vector3 pos = m_Cam.position + new Vector3(-m_Cam.position.x * bgInfo.m_Percent, 0, m_DepthZ);
		
		bgInfo.m_Background.position = pos;
	}
}
Each background layer moves at a different horizontal speed based on its m_Percent value. Layers farther from the camera scroll more slowly, creating depth through parallax scrolling.


4. Player Interaction Zones

This section explains how the player interacts with the environment and game objects. These interactions include stomping enemies, taking damage, collecting items, and detecting ground contact. The system supports modular collision handling with a focus on precision.

  • The player object uses one collider and two detection zones:
    • 🟢 Green Capsule:
      • CapsuleCollider2D.
      • Collides only with ground.
      • Disabled while jumping up; re-enabled when falling.
    • 🟨 Yellow box:
      • Hit detection zone.
      • Only detects traps, enemies, and items.
    • 🟥 Red box:
      • Stomp zone.
      • Detects enemies underfoot.
      • Used to kill enemies on landing.

PlayerController Class - Key Methods

Method #1: Green Capsule - Ground collision via CapsuleCollider2D

bool IsGrounded()
{
	if (!m_PlayerFootCollider.enabled)
		return false;
 
	Collider2D groundCollider = Physics2D.OverlapArea(m_FeetPoint1.position, m_FeetPoint2.position, m_PlatformLayerMask);
 
	return groundCollider != null;
}
 
void UpdateFootColliderState()
{
	if (!IsGrounded())
	{
		if (m_Rb.velocity.y > 0.0f)
			m_PlayerFootCollider.enabled = false;
		else
			m_PlayerFootCollider.enabled = true;
	}
}
The green capsule is the main CapsuleCollider2D for ground collision. It’s disabled when jumping up to pass through platforms and re-enabled when falling to detect landing.


Method #2: Yellow box - Hit zone for traps, enemies, and items

void CheckPlayerHitbox()
{
	Vector2 p1 = m_BodyPoint1.position;
	Vector2 p2 = m_BodyPoint2.position;
 
	// Death check
	Collider2D threat = Physics2D.OverlapArea(p1, p2, m_DeathLayerMask);
	if (threat != null)
	{
		PlayerManager.instance.LoseLife();
	}
 
	// Coin check
	Collider2D coin = Physics2D.OverlapArea(p1, p2, LayerMask.GetMask("Coin"));
	if (coin != null)
	{
		PlayerManager.instance.ObtainedCoin();
		Destroy(coin.gameObject);
		SoundManager.instance.PlaySFX("Coin");
	}
}
The yellow box is a Gizmo-drawn hit zone used to detect traps, enemies, and coins. It ignores platforms, allowing the player to pass through them when jumping.


Method #3: Red box - Stomp zone for killing enemies on landing

private void CheckEnemyUnderfoot()
{
	Collider2D enemy = Physics2D.OverlapArea(m_FeetPoint1.position, m_FeetPoint2.position, m_KillLayerMask);
	if (enemy == null)
		return;
 
	m_Rb.velocity = Vector3.up * m_JumpVelocity * 0.5f;
	PlayerManager.instance.KilledEnemy();
	Destroy(enemy.gameObject);
	SoundManager.instance.PlaySFX("EnemyKill");
}
The red box is a Gizmo-drawn stomp zone below the player’s feet. It detects enemies during a downward jump; if hit, the enemy is killed and the player bounces slightly upward.


Conclusion

This project focused on refining player interaction and game responsiveness through precise collision handling and trigger logic. While simple in scope, it served as a strong foundation for understanding Unity’s 2D platformer and building scalable gameplay features.