Creallies - 2025

     Platform :

    Mobile

Team size :

5 Developers

3 Artists

     Time span :

       10 days

About the game :

This game play around creatures named "Allies", they are mysterious beast that hide in the world,  you can collect them through your phone camera by scanning QR Code and once it's done you can either fight it or catch it so he can help you later.

What I worked on :

1 : Hierarchy

One of my first priorities on this project was to establish a clean and understandable project hierarchy, essential when working in a collaborative team environment. A well-structured hierarchy directly improves workflow, readability, and long-term maintainability.

For the Artists, most of whom were discovering Unity for the first time, it was crucial to provide a clear structure. Without a proper framework, they had no reference for organizing their assets, materials, and prefabs. Creating an intuitive and easy-to-use folder hierarchy ensured that they could focus on content creation rather than searching for the right directories.

For the Developers, we were a team of five programmers frequently interacting with each other’s scripts. A consistent and well-organized structure enabled smooth collaboration, reduced merge conflicts, and kept the overall workflow faster, cleaner, and more scalable throughout the project.

2 : "Pokedex" / Data-Driven progression

I worked on a scene inspired by a Pokédex, designed to display all the creatures encountered by the player across their progression. The objective was to build a flexible and data-driven progression system capable of persisting information across multiple scenes.

All runtime information is centralized within a singleton-based GameData manager, which stores player data, captured creatures, and Pokédex entries. This ensures consistent synchronization between all systems without duplication or desynchronization.

The entire dataset is serialized into JSON files, allowing fast and reliable save/load operations while keeping the structure human-readable and easy to debug. Each creature uses a custom serializable class storing stats, rarity, shiny state, and its origin zone, making the architecture naturally scalable.

The UI relies on a ScrollView-based dynamic layout, which adapts to data size and maintains smooth performance even with large datasets. This approach offers clean architecture, flexibility, and maintainability suitable for progression-tracking systems.


        private class SavePokedex
        {
            public List savePokedex = new List();
        }
        [System.Serializable]
        public class AllieIndividual : Allie
        {
            public bool isShiny;
            public Stats stats;
            public int exp;

            public AllieIndividual(Allie allie)
            {
                id = allie.id;
                rarity = allie.rarity;
                sprite = allie.sprite;
                spriteShiny = allie.spriteShiny;
                name = allie.name;
                canEvolve = allie.canEvolve;
                idEvolved = allie.idEvolved;
            }

            public AllieIndividual() {;}
        }

        [System.Serializable]
        public class PokedexInfo
        {
            public bool isPokemonCatched;
            public bool isShinyObtained;
            public int captureLevel;

            public float weight;
            public float height;
            public CaptureManager.ZoneName zoneName;

        }

  

The differents classes used to save the datas


public void SaveInfoToJson()
{
    if (!File.Exists(Application.persistentDataPath + "/InfoSaveFile.json"))
    {
        Directory.CreateDirectory(Path.GetDirectoryName(Application.persistentDataPath + "/InfoSaveFile.json"));
    }

    Debug.Log("Save");
    SaveInfoClass temp = new SaveInfoClass() { infoplayer = GameData.Instance.infoplayer };
    string json = JsonUtility.ToJson(temp, true);
    Debug.Log(json);
    System.IO.File.WriteAllText(Application.persistentDataPath + "/InfoSaveFile.json", json);
}

public void LoadInfoToJson()
{
    if (File.Exists(Application.persistentDataPath + "/InfoSaveFile.json"))
    {
        Debug.Log("Load");
        string json = System.IO.File.ReadAllText(Application.persistentDataPath + "/InfoSaveFile.json");
        Debug.Log(json);
        GameData.Instance.infoplayer = JsonUtility.FromJson(json).infoplayer;
        Debug.Log(GameData.Instance.infoplayer);
    }
}

public void SavePokedexIntoFile()
{
    if (!File.Exists(Application.persistentDataPath + "/PokedexSaveFile.json"))
    {
        Directory.CreateDirectory(Path.GetDirectoryName(Application.persistentDataPath + "/PokedexSaveFile.json"));
    }

    SavePokedex temp = new SavePokedex() { savePokedex = GameData.Instance.Pokedex };
    string json = JsonUtility.ToJson(temp, true);
    Debug.Log(json);
    System.IO.File.WriteAllText(Application.persistentDataPath + "/PokedexSaveFile.json", json);
}

public void LoadPokedexFromFile()
{
    if (File.Exists(Application.persistentDataPath + "/PokedexSaveFile.json"))
    {
        string json = System.IO.File.ReadAllText(Application.persistentDataPath + "/PokedexSaveFile.json");
        Debug.Log(json);
        GameData.Instance.Pokedex = JsonUtility.FromJson(json).savePokedex;
        Debug.Log(GameData.Instance.PlayerBox);
    }
}

  

Save functions that serialize data into JSON files

3 : Custom Sound Manager

To manage the entire audio layer of the game, I developed a custom SoundManager in Unity.
It centralizes all music and sound-effect logic, using AudioMixers to dynamically adjust master, music, and FX volumes with persistent PlayerPrefs saving.

The system supports both one-shot sounds (UI clicks, attacks…) and looped background tracks, and relies on a singleton pattern to ensure consistent audio behavior across all scenes.

By leveraging Unity’s AudioMixerGroups, I was able to fine-tune the mix in real time and optimize audio performance, especially for mobile platforms.

Additionally, I added an editor-only feature that automatically updates and displays all sound categories in the Inspector through OnEnable, making audio organization much more efficient during development.


public enum SoundType
{
    BUTTON_PRESSED,
    ALLIE_HIT,
    ALLIE_ATTACK,
    CLICK_BAR,
    QTE_CIRCLE,
    SHINY_POP,
    CATCH,
    VICTORY,

    BATTLE_MUSIC,
    MAIN_MENU_MUSIC,
    TOTAL_SOUNDS_TYPE
}

public enum MusicType
{
    MAIN,
    BATTLE,
}

public class SoundManager : MonoBehaviour
{
    public enum SoundInfo
    {
        MASTER,
        SOUNDS,
        FX,
        TOTAL_SOUNDS_MIXER
    }

    private class SaveSound
    {
        public List<float> saveSounds = new List<float>();
    }

    [SerializeField] private SoundList[] soundList;
    private static SoundManager instance;

    private AudioSource audioSource;
    private AudioSource[] audioSources;

    [SerializeField] private AudioSource FXSource;
    [SerializeField] private AudioMixerGroup[] audioMixers;

    public List<float> volumesPrefs;

    private void Awake()
    {
        if (instance == null)
        {
            instance = this;
            DontDestroyOnLoad(gameObject);
            DontDestroyOnLoad(gameObject.GetComponent<AudioResource>());
        }
        else
        {
            Destroy(gameObject);
        }
    }

    private void Start()
    {
        audioSource = GetComponent<AudioSource>();
        volumesPrefs = new List<float>(audioMixers.Length);
        audioSources = new AudioSource[(int)SoundType.TOTAL_SOUNDS_TYPE];

        if (PlayerPrefs.HasKey("MasterVolume"))
        {
            instance.audioMixers[0].audioMixer.SetFloat("MasterVolume", PlayerPrefs.GetFloat("MasterVolume"));
            instance.audioMixers[1].audioMixer.SetFloat("MusicVolume", PlayerPrefs.GetFloat("MusicVolume"));
            instance.audioMixers[2].audioMixer.SetFloat("FXVolume", PlayerPrefs.GetFloat("FXVolume"));
        }

        FXSource.volume = 0.2f;
        audioSource.volume = 0.2f;
    }

    public static void PlaySound(SoundType sound, float volume = 0.2f)
    {
        instance.audioSource.PlayOneShot(instance.soundList[(int)sound].Sounds, volume);
        instance.audioSource.clip = null;
    }

    public static void PlayMusic(SoundType music, float volume = 0.2f)
    {
        instance.FXSource.clip = instance.soundList[(int)music].Sounds;
        instance.FXSource.Play();
    }

    public static void PauseAllSound()
    {
        instance.audioSource.Pause();
    }

    public static void UnPauseAllSound()
    {
        instance.audioSource.UnPause();
    }

    public static void PlaySoundSpecificTime(float timeWanted)
    {
        instance.audioSource.time = timeWanted;
        instance.audioSource.Play();
    }

    public static void UnPauseSoundSpecificTime(float timeWanted)
    {
        instance.audioSource.time = timeWanted;
        instance.audioSource.UnPause();
    }

    public static void SetSoundVolume(int _i, float _volume)
    {
        if (_i == (int)SoundInfo.MASTER)
        {
            instance.audioMixers[_i].audioMixer.SetFloat("MasterVolume", _volume);
            PlayerPrefs.SetFloat("MasterVolume", _volume);
        }
        else if (_i == (int)SoundInfo.SOUNDS)
        {
            instance.audioMixers[_i].audioMixer.SetFloat("MusicVolume", _volume);
            PlayerPrefs.SetFloat("MusicVolume", _volume);
        }
        else if (_i == (int)SoundInfo.FX)
        {
            instance.audioMixers[_i].audioMixer.SetFloat("FXVolume", _volume);
            PlayerPrefs.SetFloat("FXVolume", _volume);
        }

        PlayerPrefs.Save();
    }

#if UNITY_EDITOR
    private void OnEnable()
    {
        string[] names = Enum.GetNames(typeof(SoundType));
        Array.Resize(ref soundList, names.Length);

        for (int i = 0; i < soundList.Length; i++)
        {
            soundList[i].name = names[i];
        }
    }
#endif
}

[Serializable]
public struct SoundList
{
    public string name;
    public AudioClip Sounds { get => sounds; }
    public AudioMixerGroup mixer;

    [SerializeField] private AudioClip sounds;
}
    
Show full script ↓

The differents classes used to save the datas


public void SaveInfoToJson()
{
    if (!File.Exists(Application.persistentDataPath + "/InfoSaveFile.json"))
    {
        Directory.CreateDirectory(Path.GetDirectoryName(Application.persistentDataPath + "/InfoSaveFile.json"));
    }

    Debug.Log("Save");
    SaveInfoClass temp = new SaveInfoClass() { infoplayer = GameData.Instance.infoplayer };
    string json = JsonUtility.ToJson(temp, true);
    Debug.Log(json);
    System.IO.File.WriteAllText(Application.persistentDataPath + "/InfoSaveFile.json", json);
}

public void LoadInfoToJson()
{
    if (File.Exists(Application.persistentDataPath + "/InfoSaveFile.json"))
    {
        Debug.Log("Load");
        string json = System.IO.File.ReadAllText(Application.persistentDataPath + "/InfoSaveFile.json");
        Debug.Log(json);
        GameData.Instance.infoplayer = JsonUtility.FromJson(json).infoplayer;
        Debug.Log(GameData.Instance.infoplayer);
    }
}

public void SavePokedexIntoFile()
{
    if (!File.Exists(Application.persistentDataPath + "/PokedexSaveFile.json"))
    {
        Directory.CreateDirectory(Path.GetDirectoryName(Application.persistentDataPath + "/PokedexSaveFile.json"));
    }

    SavePokedex temp = new SavePokedex() { savePokedex = GameData.Instance.Pokedex };
    string json = JsonUtility.ToJson(temp, true);
    Debug.Log(json);
    System.IO.File.WriteAllText(Application.persistentDataPath + "/PokedexSaveFile.json", json);
}

public void LoadPokedexFromFile()
{
    if (File.Exists(Application.persistentDataPath + "/PokedexSaveFile.json"))
    {
        string json = System.IO.File.ReadAllText(Application.persistentDataPath + "/PokedexSaveFile.json");
        Debug.Log(json);
        GameData.Instance.Pokedex = JsonUtility.FromJson(json).savePokedex;
        Debug.Log(GameData.Instance.PlayerBox);
    }
}

  

Save functions that serialize data into JSON files

Conclusion :

Since this game is the first I did on Unity I learned a lot through it. The fact that it was a mobile game was a great opportunity to do something different than what I was used to.
It helped me on my team-leading skills and how to sometimes , think more about the groups than the individual and  how managing problem could be easier with teamworks.