Sofiane Kaci
LogoUnity Anazir - Threevision Anazir Website
https://www.anazirgame.com/


Type
Multiplayer
Genre
Tower Defense
Moteur
Unity
Langage
C#
SQL
Rôles
Gameplay Programmer
UI
Intégration
Incarnez un héros élémentaire épique et construisez votre armée de golems pour affronter d'autres joueurs dans un jeu de stratégie mêlant défense de tours et gameplay d'arène. Partez à l'aventure et explorez le monde d'Anazir pour découvrir les Golems qui y habitent et utilisez leurs incroyables pouvoirs.

Faites preuve de stratégie en construisant votre deck de cartes afin de guider votre armée de golems à la victoire dans ce jeu de survie du plus fort.
down-arrow Gameplay Programmation down-arrow Golems Sur Anazir, les golems sont composés chacun d'une tour et d'une invocation, les tours permettent de défendre la base du joueur face aux invocations ennemies. Les invocations permettent elles d'attaquer la base de l'adversaire.

Chaque tour et invocation possède des capacités uniques permettant une approche variée de l'attaque et de la défense selon les joueurs.

J'ai eu l'occasion de travailler sur la création de différents golems, comprenant la création des différents patterns des tours et invocations, ainsi que l'intégration dans le moteur des différents visuels, animations et vfx des golems, adaptés aux patterns créés auparavant.

Sur les différents golems suivants, les tours et invocations ont été créés from scratch, vous pouvez voir sur le terrain de droite, la tourelle travaillée ainsi que ses différents effets, de l'autre coté du terrain les invocations et leurs effets respectifs.
Octnide octnide Golem Octnide Tourelle A chaque vague, les dégâts des premiers tirs de la tourelle sont fortement augmentés, infligeant de lourds dégâts unicibles. En contre-partie, une fois ses charges épuisées, la tourelle n'inflige plus que des dégâts mineurs, elle doit alors attendre le début d'une prochaine vague pour se recharger. Invocation L'invocation possède une vitesse de déplacement très élevée, en revanche, sa vitesse diminue à chaque fois qu'elle subit des dégâts, et devient minime lorsque ses points de vie sont au plus bas. Iks iks Golem Iks Tourelle La tourelle possède une vitesse d'attaque élevée, et cible aléatoirement une invocation ennemie parmi celles présentes dans sa portée. Invocation L'invocation ne possède pas de caractéristique particulière.
C# script Voici un code snippet représentant une partie du code que j'ai pu effectuer sur le projet, on peut y voir l'utilisation de custom class, d'héritage de classe, d'events "Action", ainsi que de variables fonctions et coroutines permettant une simplification et clarté du code, certaines fonctions utilisées font référence aux services UnityCloud permettant de syncroniser des données chez le joueur, afin de conserver des informations pour les prochaines connections du joueur.

Ce script montre le nouveau fonctionnement des UI de collection du joueur, lui permettant de simplement changer les golems et héros de son deck, tout en affichant les détails sur ceux-ci avant de pouvoir les sélectionner.

Référence à la section UI > Collection pour plus d'informations
using Anazir.UI.MainMenu;
using UnityEngine;
using UnityEngine.Assertions;
using UnityEngine.UI;
using TMPro;
using DG.Tweening;
using System.Collections;
using System;
using Anazir.UI.HeroInventory;
using System.Collections.Generic;
using BrainFailProductions.PolyFew;

namespace Anazir.UI
{
    public class DeckEditorSection : UISection
    {
        public static CardController SelectedCard { private set; get; }

        public readonly Deck EditorDeck = new();


        [SerializeField] GameObject m_leaveWithoutSavingPopup;
        [SerializeField] GameObject m_golemsSection, m_heroesSection;

        [SerializeField] private Transform m_deck;
        [SerializeField] private Transform m_ownedHeroesParent;

        [SerializeField] Button m_save;
        [SerializeField] Button m_golems, m_heros;
        [SerializeField] Button m_modifyGolems;
        [SerializeField] Button m_modifyHeroes;

        [SerializeField] private RectTransform m_headerMenu;
        [SerializeField] private RectTransform m_headerBackground;

        [SerializeField] private GameObject[] m_hideWhenModifyGolems;
        [SerializeField] private GameObject[] m_showWhenModifyGolems;

        [SerializeField] private GameObject[] m_hideWhenModifyHeroes;
        [SerializeField] private GameObject[] m_showWhenModifyHeroes;

        [SerializeField] private UIGolemDesc m_golemDesc;

        [SerializeField] private HeroItemController m_selectedHero;
        [SerializeField] private HeroItemController heroItem;

        [SerializeField] private HeroViewerController m_heroViewerController;

        [SerializeField] private TextMeshProUGUI m_golemsCount, m_heroesCount;

        [SerializeField] private VerticalLayoutGroup m_golemContent;

        private bool isModifyingGolems;
        private bool isModifyingHeroes;

        private Hero.ID m_heroID;
        private List<HeroItemController> m_listHeroes = new List<HeroItemController>();

        private int m_deckParentIndex;
        private Transform m_deckParentTransform;

        private Button lastButtonClicked;

        private CardController cardSelectedToSwap;


        private Button m_golemDescButton => m_golemDesc.gameObject.GetComponent<Button>();
        private CanvasGroup m_golemDescCanvasGroup => m_golemDesc.gameObject.GetComponent<CanvasGroup>();
        private VerticalLayoutGroup m_heroesSectionLayout => m_heroesSection.GetComponent<VerticalLayoutGroup>();
        private VerticalLayoutGroup m_headerMenuLayout => m_headerMenu.GetComponent<VerticalLayoutGroup>();
        private RectTransform m_golemsSectionRectTransform => m_golemsSection.GetComponent<RectTransform>();
        private RectTransform m_heroesSectionRectTransform => m_heroesSection.GetComponent<RectTransform>();



        void OnEnable()
        {
            EditorDeck.Changed += OnEditorDeckChanged;
            CardController.OnClick += OnCardClick;
            CardController.OnSelfHoldRelease += ShowCardDescription;

            m_save.onClick.AddListener(Save);

            m_golems.onClick.AddListener(LoadGolemSection);
            m_heros.onClick.AddListener(LoadHerosSection);

            m_modifyGolems.onClick.AddListener(ModifyGolems);
            m_modifyHeroes.onClick.AddListener(ModifyHeroes);

            m_golemDescButton.onClick.AddListener(HideCardDescription);

            LoadGolemSection();

            m_golemsCount.text = "(" + (PlayerData.Singleton.Golems.Count - 8) + ")";
            m_heroesCount.text = "(" + (PlayerData.Singleton.Heroes.Count - 1) + ")";
        }

        void OnDisable()
        {
            EditorDeck.Changed -= OnEditorDeckChanged;
            CardController.OnClick -= OnCardClick;
            CardController.OnSelfHoldRelease -= ShowCardDescription;

            m_save.onClick.RemoveListener(Save);

            m_golems.onClick.RemoveListener(LoadGolemSection);
            m_heros.onClick.RemoveListener(LoadHerosSection);

            m_modifyGolems.onClick.RemoveListener(ModifyGolems);
            m_modifyHeroes.onClick.RemoveListener(ModifyHeroes);

            m_golemDescButton.onClick.RemoveListener(HideCardDescription);
        }

        void OnEditorDeckChanged()
        {
            m_save.interactable = EditorDeck.IsValid;
        }


        void OnValidate() => CanHide = true;

        public override void Hide()
        {
            if (EditorDeck.IsValid)
            {
                EditorDeck.CopyTo(PlayerData.Singleton.Deck);
                Leave();
            }
            else
            {
                m_leaveWithoutSavingPopup.SetActive(true);
            }
        }

        public void LeaveWithoutSaving()
        {
            m_leaveWithoutSavingPopup.SetActive(false);
            Leave();
        }

        private void Save()
        {
            if (isModifyingGolems)
            {
                Assert.IsTrue(EditorDeck.IsValid,"Save button shouldn't be interactible if deck is not valid");
                EditorDeck.CopyTo(PlayerData.Singleton.Deck);
                UnityCloudManager.SetDeck();

                if (cardSelectedToSwap is not null)
                {
                    cardSelectedToSwap.ToggleHighlight(false);
                    cardSelectedToSwap = null;
                }

                RemoveModifyGolems();
            }
            else if (isModifyingHeroes)
            {
                m_selectedHero.ChangePlayerHero();
                RemoveModifyHeroes();
                UpdateHeroesSection();
            }
        }
        
        void Leave()
        {
            gameObject.SetActive(false);

            if(ParentSection != null)
            {
                CurrentSection = ParentSection;
                ParentSection.Show();
            }

            if (SelectedCard is not null) HideCardDescription();

            ExitCallback?.Invoke();
        }


        private void ModifyGolems()
        {
            isModifyingGolems = true;

            HideElements(m_hideWhenModifyGolems);
            ShowElements(m_showWhenModifyGolems);

            m_headerMenuLayout.spacing += 80;

            m_headerBackground.sizeDelta += new Vector2(0, 700);
            m_headerBackground.localPosition -= new Vector3(0, 350, 0);


            m_golemContent.padding.top += 460;
            RebuildLayout(m_golemsSectionRectTransform);

            m_deckParentTransform = m_deck.parent;
            m_deckParentIndex = m_deck.GetSiblingIndex();
            m_deck.parent = m_headerMenu;
        }

        private void RemoveModifyGolems()
        {
            isModifyingGolems = false;

            HideElements(m_showWhenModifyGolems);
            ShowElements(m_hideWhenModifyGolems);

            m_headerMenuLayout.spacing -= 80;

            m_headerBackground.sizeDelta -= new Vector2(0, 700);
            m_headerBackground.localPosition += new Vector3(0, 350, 0);

            m_golemContent.padding.top -= 460;
            RebuildLayout(m_golemsSectionRectTransform);

            m_deck.parent = m_deckParentTransform;
            m_deck.SetSiblingIndex(m_deckParentIndex);
        }


        private void ModifyHeroes()
        {
            isModifyingHeroes = true;
            m_selectedHero.IsModifying(true);

            HideElements(m_hideWhenModifyHeroes);
            ShowElements(m_showWhenModifyHeroes);

            m_headerMenu.sizeDelta -= new Vector2(0, 200);
            m_headerMenu.localPosition += new Vector3(0, 100, 0);

            m_heroesSectionLayout.padding.top -= 160;
            RebuildLayout(m_heroesSectionRectTransform);
        }

        private void RemoveModifyHeroes()
        {
            isModifyingHeroes = false;
            m_selectedHero.IsModifying(false);

            HideElements(m_showWhenModifyHeroes);
            ShowElements(m_hideWhenModifyHeroes);

            m_headerMenu.sizeDelta += new Vector2(0, 200);
            m_headerMenu.localPosition -= new Vector3(0, 100, 0);

            m_heroesSectionLayout.padding.top += 160;
            RebuildLayout(m_heroesSectionRectTransform);
        }


        private void HideElements(GameObject[] _gos)
        {
            foreach (GameObject go in _gos)
            {
                go.SetActive(false);
            }
        }
        private void ShowElements(GameObject[] _gos)
        {
            foreach (GameObject go in _gos)
            {
                go.SetActive(true);
            }
        }


        public void RebuildLayout(RectTransform _rectTransform)
        {
            StartCoroutine(DelayedRebuildLayout(_rectTransform));
        }
        private IEnumerator DelayedRebuildLayout(RectTransform _rectTransform)
        {
            yield return null;
            LayoutRebuilder.MarkLayoutForRebuild(_rectTransform);
        }


        private void LoadGolemSection()
        {
            if (lastButtonClicked == m_golems) return;
            lastButtonClicked = m_golems;

            UnloadAllSections();

            m_golems.targetGraphic.DOFade(1f, 0.5f);
            m_golemsSection.SetActive(true);

            PlayerData.Singleton.Deck.CopyTo(EditorDeck);
        }

        public void LoadHerosSection()
        {
            if (lastButtonClicked == m_heros) return;
            lastButtonClicked = m_heros;

            UnloadAllSections();

            m_heros.targetGraphic.DOFade(1f, 0.5f);
            m_heroesSection.SetActive(true);

            UpdateHeroesSection();
        }

        private void UnloadAllSections()
        {
            m_golems.targetGraphic.DOFade(0.2f, 0.5f);
            m_heros.targetGraphic.DOFade(0.2f, 0.5f);

            m_golemsSection.SetActive(false);
            m_heroesSection.SetActive(false);
        }


        private void OnCardClick(CardController _card)
        {
            if (isModifyingGolems)
            {
                SwapCard(_card);
            }
            else
            {
                GolemViewer.View(_card.GolemID);
            }
        }

        private void SwapCard(CardController _card)
        {
            if (!cardSelectedToSwap)
            {
                cardSelectedToSwap = _card;
                cardSelectedToSwap.ToggleHighlight(true);
            }
            else
            {
                if (cardSelectedToSwap.Container != _card.Container)
                {
                    SwapCardAnimation(_card, cardSelectedToSwap);
                }
                cardSelectedToSwap.ToggleHighlight(false);
                cardSelectedToSwap = null;
            }
        }

        private void SwapCardAnimation(CardController _card1, CardController _card2)
        {
            Vector3 position1 = _card1.transform.position;
            Vector3 position2 = _card2.transform.position;

            float arcHeight = 0.5f;
            float swapDuration = 0.5f;

            Vector3 arcDirection = Vector3.Cross((position1 - position2).normalized, Vector3.forward);

            Vector3 midpoint = (position1 + position2) / 2;
            Vector3 arcOffset = arcDirection * arcHeight;

            // Create the paths for the arc
            Vector3[] pathA = new Vector3[] { position1, midpoint - arcOffset, position2 };
            Vector3[] pathB = new Vector3[] { position2, midpoint + arcOffset, position1 };

            _card1.transform.DOPath(pathA, swapDuration, PathType.CatmullRom)
                  .OnComplete(() => ResetCardPosition(_card1, position1, _card2));

            _card2.transform.DOPath(pathB, swapDuration, PathType.CatmullRom)
                  .OnComplete(() => ResetCardPosition(_card2, position2, _card1));
        }

        private void ResetCardPosition(CardController _card, Vector3 _position, CardController _otherCard)
        {
            _card.transform.position = _position;

            if (_card.Container == CardController.ContainerType.Deck)
            {
                EditorDeck.ReplaceOne(_card.GolemID, _otherCard.GolemID);
            }
        }

        private void ShowCardDescription(CardController _card)
        {
            if (!SettingsManager.InGameCardDescription)
                return;

            SelectedCard = _card;

            m_golemDesc.Init(_card.GolemID);
            m_golemDesc.Show();
            m_golemDescCanvasGroup.blocksRaycasts = true;
        }

        public void HideCardDescription()
        {
            m_golemDesc.Hide();
            SelectedCard = null;

            m_golemDescCanvasGroup.blocksRaycasts = false;
        }
        
        private void UpdateHeroesSection()
        {
            m_heroID = (Hero.ID)PlayerData.Singleton.Hero;
            m_selectedHero.HeroID = m_heroID;
            m_selectedHero.SetHeroViewer(m_heroViewerController);

            m_listHeroes.Clear();
            m_listHeroes.Add(m_selectedHero);

            //Replace owned heroes
            foreach (Transform _child in m_ownedHeroesParent)
            {
                Destroy(_child.gameObject);
            }

            foreach (Hero.ID _heroID in PlayerData.Singleton.Heroes)
            {
                // Ignore selected hero
                if (_heroID == m_heroID) continue;

                var item = Instantiate(heroItem, m_ownedHeroesParent);
                item.HeroID = _heroID;
                m_listHeroes.Add(item);
            }
            m_selectedHero.UpdateList(m_listHeroes);
        }
    }
}
                            
C# code snippet
Langage adaptatif Anazir est un jeu disponible dans différentes langues, sont actuellement disponibles le français, l'anglais, le japonais et l'espagnol.

Les textes et éléments d'UI doivent donc s'adapter selon la taille du texte, variable selon la langue sélectionnée.
adaptative_language Adaptation du texte et des UI selon la langue sélectionnée
down-arrow UI down-arrow J'ai eu l'occasion de pouvoir retravailler différents éléments d'UI dans le jeu, permettant une meilleure lisibilité pour le joueur, ainsi qu'une fluidité de ses actions.

Les joueurs pouvant utiliser différents téléphones, il est important de faire en sorte que l'UI s'adapte aux différents formats existants.
Collection Dans l'ancienne version, la sélection des golems et des héros se faisait indépendemment, l'UI a été retravaillée afin de pouvoir recentrer ces éléments dans le même onglet, et rendre le tout plus esthétique.

Golem
Cliquer sur un golem permettait de voir ses informations, et il fallait glisser-déposer un golem présent dans notre deck afin de le retirer, puis en glisser-déposer un autre manuellement par la suite pour le rajouter au deck, cette pratique n'était pas évidente, et peu confortable pour l'utilisateur.

Pour simplifier, le joueur peut maintenant maintenir pour voir rapidement les statistiques des golems, cliquer simplement permet de voir plus en détail les informations à son sujet.

Cliquer sur le bouton "Modifier" permet au joueur d'éditer son deck, par la suite, cliquer sur un golem l'highlight, en sélectionner un deuxième permet d'intervertir la position des deux golems de manière fluide et bien plus confortable.

Héros
Cliquer sur un héros permettait de voir ses informations, un bouton "Sélectionner le héros" permettait au joueur de le sélectionner.

Maintenant, comme pour la sélection des golems, le joueur voit les différents héros qu'il a débloqués.

Cliquer sur un héros permet d'afficher des informations à son sujet, son artwork ainsi qu'une version 3D de celui-ci.

Une fois décidé, le joueur peut cliquer sur le bouton "Modifier" afin de sélectionner le héros de son choix.

Nouvelle version collection_new Version retravaillée de l'écran collection
Ancienne version collection_old_golem Ancienne version de l'écran collection des golems collection_old_hero Ancienne version de l'écran collection des héros
Home Le rework de l'écran home du jeu est purement esthétique, permettant une meilleure visibilité pour le joueur.

Le principe étant de rendre l'écran plus lisible, normaliser les différents éléments d'UI, et adapter la taille des différents éléments et textes afin d'éviter tout dépassement sur les différents éléments ou l'écran du joueur.

Une feature supplémentaire étant l'ajout de la présence du modèle 3D d'un des golems, sélectionné aléatoirement parmis ceux présent dans le deck du joueur.
Le golem s'actualise à chaque fois que le joueur retourne sur cet écran.

Nouvelle version home_new_gif Version retravaillée de l'écran home
Ancienne version home_old Ancienne version de l'écran home
Card trail and highlight Cette feature a été mise en place dans le but de simplifier la lisibilité du joueur en jeu quant aux différentes actions qu'il peut effectuer, lorsqu'un golem est déjà présent sur le terrain, il peut fusionner la carte présente dans sa main avec la tour correspondante présente sur le terrain afin de l'améliorer.

Afin de l'aider à identifier les golems déjà présents, à chaque fois que le joueur peut effectuer une fusion, la carte fusionnable présente dans sa main s'illumine, et crée une trail partant de la carte jusqu'aux tours correspondantes présentes sur le terrain.

Un halo et une flèche font alors leur apparition autour des tours afin d'indiquer la destination finale pour la fusion.

De ce fait, le joueur peut rapidement identifier quelle carte il peut fusionner, et où celle-ci doit être placée.

L'effet apparait à chaque fois qu'une nouvelle carte rejoint la main du joueur.

Nouvelle version card_trail_new Version retravaillée des trails et highlights
Ancienne version card_trail_old Ancienne version des trails et highlights
down-arrow Analytics down-arrow Les analytics utilisent le système d'Unity Cloud, le système permet d'obtenir des informations quant à l'utilisation des joueurs, il permet par exemple d'obtenir des informations sur le winrate et pickrate des différents golems, la durée des parties etc...

Ces datas pourront être utilisées par la suite afin d'équilibrer le jeu.

Les analytics suivants sont séparés en différentes catégories pour une meilleure lisibilité.

Les informations affichées sont des données de développement, elles ne correspondent pas aux données des joueurs.
SQL Les différents tableaux sont régis par des script SQL permettant un affichage plus personnalisé des données.

Le script ci-dessous permet par exemple de voir le nombre de parties lancées chaque jour au cours du dernier mois.
WITH DateSeries AS (
    SELECT 
        CURRENT_DATE() - (ROW_NUMBER() OVER (ORDER BY SEQ4()) - 1) AS EVENT_DATE
    FROM TABLE(GENERATOR(ROWCOUNT => 30)) -- Generates a series of 30 rows
)
SELECT
    ds.EVENT_DATE,
    COUNT(EVENT_JSON:GamePlayed) AS game_played_count
FROM 
    DateSeries ds
LEFT JOIN 
    events e
ON 
    DATE(e.EVENT_DATE) = ds.EVENT_DATE
    AND e.event_name = 'GameInfo'
GROUP BY 
    ds.EVENT_DATE
ORDER BY 
    ds.EVENT_DATE;
                            
SQL code snippet
Golems informations Ces tableaux permettent l'analyse des golems, affichant différentes informations utiles comme les winrates et pickrates des différents golems et héros, les dégâts infligés par les différentes tours et invocations des golems, le nombre de golems fusionnés au niveau maximum et le nombre de sorts lancés par le joueur. analytics_golems_informations_1 Analytics golems informations 1 analytics_golems_informations_2 Analytics golems informations 2 Games informations Ces tableaux permettent l'analyse des parties, affichant le nombre de parties jouées ou annulées au cours du dernier mois, ainsi que la durée moyenne de celles-ci. analytics_games_informations Analytics games informations Players informations Ces tableaux permettent l'analyse des joueurs, affichant le temps de matchmaking et de chargement moyen chez les joueurs ainsi que le nombre de comptes supprimées au cours du dernier mois. analytics_players_informations Analytics players informations
Get_It_On_Google_Play Download_on_the_App_Store_Badge