← Back to projects

Light of Ko

2D challenging platformer for Global Game Jam 2026

Role Gameplay & Tech Art
Team 2 Developers, 1 GD/Music & 2 Concept Artists
Duration 48 Hours (jam)
Stack Unity / C#

The Context

Light of Ko was created during the 2026 Global Game Jam on the theme of “Mask.” The player takes on the role of Ko, a Kodama who must save the forest after an earthquake, but he is frail and must wear a mask to be able to move around.

In a small group, I worked on the mask system with shaders, but I also programmed part of the gameplay and did bug fixes until the last minute.

My Contributions

  • 01 Shader Implemented fully functional shader system for the mask
  • 02 Gameplay & Bug Fixes Implemented gameplay and bug fixes
  • 03 Sequencer Implemented a fully functional sequencer system for introduction and endings

Learning & Implementation

In Light of Ko, darkness isn't just aesthetic—it's the main antagonist. I needed a way to restrict the player's vision dynamically without relying on heavy transparent sprites.

My solution was to create a custom Fullscreen Shader (Post-Process). By manipulating the Blit Source directly on the GPU, I achieved a smooth, performant transition that acts as a vignetting effect driven by gameplay logic.

01. The Result

The player can activate and deactivate the mask as desired. The transition is interpolated.

02. The Logic

Unity Shader Graph
Hover to Zoom

Fullscreen Shader effect for atmospheric overlay. It captures the URP Blit Source and blends a dark tint based on a sampled texture mask, with controllable intensity.

Create Sequencer System

When finalizing the game, and in order to honor the magnificent work done by the artists, I coded a Sequencer so that I could stage the narration and the mini comic strips that had been created.

For my subsequent projects, I adapted my manager into a logic that could be reused for any input.

Sequencer.cs MAKE IN THE LAST HOUR

using UnityEngine;
using TMPro;
using UnityEngine.SceneManagement;
using System.Collections;
using System.Collections.Generic;

public class Sequencer : MonoBehaviour
{
    [System.Serializable]
    public struct IntroSlide
    {
        public string name;

        public CanvasGroup imageGroup;
        public CanvasGroup textGroup;

        public TMP_Text textComponent;
        [TextArea(3, 10)] public string storyText;
    }

    [Header("Settings")]
    [SerializeField] private float imageFadeInTime = 1f;
    [SerializeField] private float textFadeInTime = 1f;
    [SerializeField] private float displayTime = 6f;
    [SerializeField] private float textFadeOutTime = 1f;

    [SerializeField] private string nextSceneName = "GameScene";
    [SerializeField] private MusicTrack[] introTrack;

    [Header("Sequence Data")]
    [SerializeField] private List slides;

    private string ActiveScene;

    private void Start()
    {
        ActiveScene = SceneManager.GetActiveScene().name;
        if (ActiveScene == "Introduction")
        {
            AudioManager.Instance.ChangeAmbianceMusic(introTrack);
        }
        StartCoroutine(PlayIntroSequence());
    }

    // This duplication below is not ideal, but it was necessary to meet the tight deadline of the jam.
    // Instead of creating a more generic system, I had to quickly implement the specific logic for the introduction and ending sequences, which led to some code repetition.

    private IEnumerator PlayIntroSequence()
    {
        foreach (var slide in slides)
        {
            if (ActiveScene == "Introduction")
            {
                if (slide.name == "2")
                {
                    displayTime = 4f;
                    Debug.Log(slide.name);
                    if (slide.textComponent != null)
                    {
                        slide.textComponent.text = slide.storyText;
                    }

                    slide.imageGroup.alpha = 0f;
                    slide.textGroup.alpha = 0f;

                    yield return StartCoroutine(FadeCanvasGroup(slide.imageGroup, 0f, 1f, imageFadeInTime));

                    yield return StartCoroutine(FadeCanvasGroup(slide.textGroup, 0f, 1f, textFadeInTime));

                    yield return new WaitForSeconds(displayTime);

                    yield return StartCoroutine(FadeCanvasGroup(slide.textGroup, 1f, 0f, textFadeOutTime));
                }
                else
                {
                    Debug.Log(slide.name);
                    if (slide.textComponent != null)
                    {
                        slide.textComponent.text = slide.storyText;
                    }

                    slide.imageGroup.alpha = 0f;
                    slide.textGroup.alpha = 0f;

                    yield return StartCoroutine(FadeCanvasGroup(slide.imageGroup, 0f, 1f, imageFadeInTime));

                    yield return StartCoroutine(FadeCanvasGroup(slide.textGroup, 0f, 1f, textFadeInTime));

                    yield return new WaitForSeconds(displayTime);

                    yield return StartCoroutine(FadeCanvasGroup(slide.textGroup, 1f, 0f, textFadeOutTime));
                }
            } else
            {
                Debug.Log(slide.name);
                if (slide.textComponent != null)
                {
                    slide.textComponent.text = slide.storyText;
                }

                slide.imageGroup.alpha = 0f;
                slide.textGroup.alpha = 0f;

                yield return StartCoroutine(FadeCanvasGroup(slide.imageGroup, 0f, 1f, imageFadeInTime));

                yield return StartCoroutine(FadeCanvasGroup(slide.textGroup, 0f, 1f, textFadeInTime));

                yield return new WaitForSeconds(displayTime);

                yield return StartCoroutine(FadeCanvasGroup(slide.textGroup, 1f, 0f, textFadeOutTime));
            }
        }

        if (ActiveScene == "Introduction")
        {
            LoadNextScene();
        }
    }
    private IEnumerator FadeCanvasGroup(CanvasGroup group, float start, float end, float duration)
    {
        float counter = 0f;
        while (counter < duration)
        {
            counter += Time.deltaTime;
            group.alpha = Mathf.Lerp(start, end, counter / duration);
            yield return null;
        }
        group.alpha = end;
    }

    private void LoadNextScene()
    {
        SceneManager.LoadScene(nextSceneName);
    }
}