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 :

First I thought that working on the project's hierarchy since it's really important to have a "clean & understandable" hierarchy when you work with a team.
For the Artists :
They were discovering Unity with this project , they had no idea where to put their assets , materials or prefabs so making the hierarchy simple to use was mandatory.

For the Developers :
We were 5 developers on this project , it means that we are going to work a lot of time with other's scripts and that a good setup is really important.

What I worked on :

Next up , I worked on a scene that can be interpreted as a "Pokedex" , it's used to see all the monsters you encountered , for that I  implemented a data-driven Pokédex system in Unity designed to persist player progression across multiple scenes.

The project uses a singleton-based GameData manager to centralize all runtime data  including player info, captured creatures, and Pokédex entries  ensuring consistent access and synchronization between systems.

All data is serialized into JSON files, allowing for efficient save/load operations while keeping the structure human-readable and easy to debug.

Each creature’s information (stats, rarity, shiny state, and origin zone) is stored through custom serializable classes, providing flexibility for future expansion.

The UI leverages a ScrollView-driven layout for dynamic content rendering, maintaining performance and responsiveness even with large data sets.

This approach combines clean architecture, scalability, and maintainability suitable for complex gameplay systems or progression tracking features.


        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

What I worked on :

To handle all the audio aspects of the game, I developed a custom SoundManager in Unity.
It centralizes music and sound effects control using AudioMixers, allowing me to dynamically adjust volumes for master, music, and FX channels with persistent PlayerPrefs saving.
 The system supports both one-shot effects (like UI clicks or battle sounds) and looped background tracks, and uses a singleton pattern to ensure consistent audio behavior across scenes.
 By leveraging Unity’s AudioMixerGroups, I was able to fine-tune the mix in real time and optimize performance for mobile devices.

Additionally, an editor-only feature automatically updates and displays all sound types in the Inspector through the OnEnable method, streamlining audio organization 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 saveSounds = new List();  }

    [SerializeField] private SoundList[] soundList;
    private static SoundManager instance;
    private AudioSource audioSource;
    private AudioSource[] audioSources;
    [SerializeField] private AudioSource FXSource;
    [SerializeField] private AudioMixerGroup[] audioMixers;

    public List volumesPrefs;

    private void Awake()
    {
        if (instance == null)
        {
            instance = this;
            DontDestroyOnLoad(gameObject);
            DontDestroyOnLoad(gameObject.GetComponent());
        }
        else
        {
            Destroy(gameObject);
        }
    }
    private void Start()
    {
        audioSource = GetComponent();
        volumesPrefs = new List(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;
}


  

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.