'Cubic Survival', conception et développement
Créer un jeu
Au Nouvel An 2023, je devais trouver quelque chose Ă faire. Je voulais crĂ©er quelque chose qui mâaiderait Ă amĂ©liorer mes compĂ©tences en programmation tout en restant amusant. AprĂšs le carillon de minuit, le lendemain, jâai Ă©tabli quelques plans.
En rĂ©flĂ©chissant Ă quelques idĂ©es, jâavais une bibliothĂšque Python basĂ©e sur les 5W1H, une application photo de type mirrorless, et un jeu mobile 2D. Chacune dĂ©coulait dâun programme Python, dâune simple application Android Studio, ou de mon prĂ©cĂ©dent projet Unity.
Mais le dĂ©veloppement de jeu avait lâair trop amusant. LâexpĂ©rience passĂ©e avec Unity mâavait marquĂ©, et lâidĂ©e de pouvoir utiliser mes propres assets maison me semblait passionnante. MĂȘme si câĂ©tait difficile, le thĂšme de crĂ©er un programme avec mon propre matĂ©riau quâon ne trouve nulle part ailleurs me semblait trĂšs attrayant. Comme je commençais Ă apprĂ©cier lâorientĂ© objet, jâai voulu utiliser un langage orientĂ© objet correctement, et jâai donc commencĂ© Ă crĂ©er un jeu mobile 2D.
Aperçu du projet
gantt
title Cubic Survival â Temps investi
Développement initial: crit, 2023-02-07, 60d
Développement avancé: a1, 2023-06-19, 150d
Publication test sur le store: crit, milestone, 2023-03-28, 0d
Republication sur le store: a3, 2023-10-29, 18d
La période de développement étant divisée en phases initiale et avancée, je vais les passer en revue briÚvement dans deux articles distincts. Ce post couvre donc le développement initial, surligné en rouge dans le diagramme ci-dessus.
Au dĂ©part, je voulais simplement survoler rapidement les caractĂ©ristiques de lâorientĂ© objet, sans savoir que je mâattacherais autant Ă ce projet. Il nây avait donc pas de plan ou dâobjectif systĂ©matique ; tout au plus avais-je quelques souhaits assez abstraits comme ceux-ci.
- Créer un design visuellement minimaliste.
- Implémenter un mouvement de caméra fluide.
- Appliquer efficacement une conception orientée objet.
- Utiliser des coroutines.
La pĂ©riode de dĂ©veloppement Ă©tant longue, ces objectifs ont Ă©tĂ© atteints les uns aprĂšs les autres. Les dĂ©tails sur comment et oĂč chacun a Ă©tĂ© rĂ©alisĂ© Ă©tant longs Ă expliquer, je les aborderai en dĂ©tail dans cet article et le suivant.
Développement initial

Jâai commencĂ© par du clone coding. LâidĂ©e Ă©tait dâabord dâessayer de reproduire un jeu cĂ©lĂšbre que je pourrais imiter Ă petite Ă©chelle.
Jâai dâabord consultĂ© Brawl Stars, dont je gardais un bon souvenir dây avoir jouĂ© avec des amis au lycĂ©e. Cependant, plutĂŽt que de copier le systĂšme du jeu, il sâagissait surtout de comprendre ce quâest une plateforme mobile 2D.
Joystick

Je voulais implémenter un joystick typique des jeux mobiles 2D : un à gauche pour le déplacement du joueur, un à droite pour viser.
Pour le crĂ©er, jâai utilisĂ© la classe OnScreenStick du package UnityEngine.InputSystem.OnScreen, en crĂ©ant deux scripts basĂ©s sur cette classe qui Translate() le joueur et un objet transparent de visĂ©e selon le dĂ©phasage. Comme il y avait peu de ressources corĂ©ennes sur le package OnScreen, jâai beaucoup consultĂ© la documentation officielle.
En passant, jâavais beaucoup dâidĂ©es que jâaurais aimĂ© implĂ©menter : relier visuellement le stick et le point central via un LineRenderer, un retour Ă©lastique du stick vers le centre, ou des contrĂŽles de joystick diffĂ©rents selon les armes. Mais mes compĂ©tences Ă©taient insuffisantes Ă lâĂ©poque, et certaines idĂ©es entraient en conflit avec la structure du jeu. Je nâai donc implĂ©mentĂ© quâun retour vibratoire Ă chaque pression et relĂąchement du joystick.
Apparition et comportement des ennemis

void spawnEnemy(GameObject Enemy, float east, float west, float south, float north)
{
float spawnPointX = Random.Range(west, east);
float spawnPointY = Random.Range(south, north);
instantiatedEnemy = Instantiate(
enemy,
player.transform.position + new Vector3(spawnPointX, spawnPointY),
transform.rotation
);
}
IEnumerator spawnEnemies()
{
for (int i = 0; i < data.spawnCount; i++)
{
spawnEnemy(Enemy, east, west, south, north);
yield return new WaitForSeconds(spawnDelay);
}
}
Au dĂ©but, jâavais créé un objet portail pour instancier les ennemis Ă des points dĂ©finis, mais le rĂ©sultat aprĂšs apparition me semblait trop monotone. Jâai donc Ă©crit le code ci-dessus pour que les ennemis apparaissent autour du joueur.
Les quatre paramĂštres east, west, south, north gĂ©nĂšrent des coordonnĂ©es alĂ©atoires Ă une certaine distance du joueur. Pour que les ennemis nâapparaissent pas soudainement autour du joueur, ces coordonnĂ©es sont spĂ©cialement traitĂ©es pour ĂȘtre en dehors de la zone rendue par lâĂ©cran.
Dans Unity, il nâexiste pas de mĂ©thode simple comme Delay() pour introduire un dĂ©lai ; la plupart des recommandations suggĂšrent dâutiliser des coroutines, ce qui a Ă©tĂ© ma premiĂšre occasion dâen utiliser une. Jâai créé une coroutine qui spawn les ennemis Ă intervalles de spawnDelay.
void Move()
{
dirTowardsPlayer = (player.transform.position - gameObject.transform.position).normalized;
transform.Translate(dirTowardsPlayer * speed * Time.deltaTime);
}
void OnCollisionEnter2D(Collision2D collider)
{
if (collider.gameObject.tag == "player")
{
player.hp -= damage;
Vibration.Vibrate((long)20);
Destroy(gameObject);
}
}
Les ennemis se dĂ©placent vers le joueur par dĂ©faut, et lors dâune collision avec le joueur, ils rĂ©duisent ses PV de damage avec un retour vibratoire, puis sont dĂ©truits via Destroy().
Inventaire et objets
Alors que la structure du jeu prenait forme, jâai pensĂ© quâun inventaire pour stocker des objets et les utiliser plus tard serait utile. Câest une question sur laquelle jâai beaucoup rĂ©flĂ©chi : dans de nombreux jeux, lâinterface dâinventaire se prĂ©sente soit comme une fenĂȘtre sĂ©parĂ©e, soit comme un bouton Ă bascule sans vraie interface. Les deux ne me satisfaisaient pas.
Jâai plutĂŽt visĂ© un inventaire capable de contenir plusieurs objets sans nuire Ă lâexpĂ©rience de jeu. Jâai donc remplacĂ© la visĂ©e manuelle assignĂ©e au joystick droit par un auto-aim, et jây ai assignĂ© une nouvelle fonction dâaccĂšs Ă lâinventaire. Maintenir le joystick droit enfoncĂ© ouvre lâinventaire ; relĂącher le doigt le ferme.

public struct InventoryData
{
public string[] Code;
public GameObject[] UI;
public GameObject[] ItemUI;
public GameObject Weapon;
public int[] Rounds;
}
for (int i = 0; i < InventoryData.InventoryUI.Length; i++)
InventoryData.UI[i].transform.position = Vector3.Lerp(currentPos, targetPos[i], 2*t);
Lâinventaire utilise 8 objets qui se dĂ©ploient autour du joueur lors de lâaccĂšs. Pour centraliser les donnĂ©es nĂ©cessaires (identifiants dâobjets, objets dâinventaire, objets dâitems, donnĂ©es dâarmes, munitions, etc.), jâai créé la structure ci-dessus.
Les objets sont divisĂ©s en deux : un objet destinĂ© Ă apparaĂźtre sur le terrain et un autre servant dâinterface UI. Lorsque le joueur ramasse un objet sur le terrain, lâobjet UI correspondant est ajoutĂ© au tableau ItemUI.
| Objet | ID |
|---|---|
| Pistolet | WPPSTL |
| Fusil Ă pompe | WPPASG |
| Minigun | WPMING |
| Passif de vitesse de déplacement | PVMSPD |
| Passif de vitesse dâattaque | PVATKR |
| ⊠| ⊠|
Les identifiants dâobjets sont composĂ©s de 2 caractĂšres indiquant le type suivis de 4 caractĂšres pour le nom de lâobjet. Ce qui Ă©tait amusant, câest que sur le moment, je ne mâen rendais pas compte, mais Ă mesure que les objets sâaccumulaient, lâidĂ©e de crĂ©er des codes uniques pour les distinguer mâest venue naturellement â ce que jâai dĂ©couvert plus tard ĂȘtre le concept dââidentifiantâ. CâĂ©tait assez utile, et je compte continuer Ă lâutiliser.
Tir dâarmes

if (shotTimer > fireThreshold)
{
for (int i = 0; i < bulletCount; i++)
{
instantBullet = Instantiate(
bullet,
FirePosition.transform.position,
Quaternion.Euler(
0, 0, transform.rotation.eulerAngles.z + Random.Range(MOA * -1, MOA) + 180
)
);
Destroy(instantBullet, 1);
}
}
shotTimer += Time.deltaTime;
void hasHitEnemy()
{
hit = Physics2D.Raycast(transform.position, transform.right, 100);
if (hit.collider != null && hit.distance < 1)
{
if (hit.collider.gameObject.tag == "enemy")
{
if (hit.collider.GetComponent<Enemy>().HP > 0)
Destroy(gameObject);
/* ... */
}
}
}
Les balles sont gĂ©nĂ©rĂ©es depuis lâobjet enfant FirePosition de lâarme, avancent en ligne droite dans la direction visĂ©e par le joueur, puis disparaissent aprĂšs 1 seconde. Pour implĂ©menter la dispersion des tirs, la valeur Z de lâangle est lĂ©gĂšrement ajustĂ©e avec Random.Range() dans la limite de la variable MOA dĂ©finie par arme lors de la gĂ©nĂ©ration de la balle.
La dĂ©tection de collision utilisait un raycast. Cependant, la vitesse des balles Ă©tant trop Ă©levĂ©e, la dĂ©tection par raycast ne fonctionnait pas correctement et les balles traversaient les ennemis. Augmenter la longueur du Ray ou Ă©largir la plage du Collider nâa pas rĂ©solu le problĂšme, mais lâajout de la condition hit.distance < 1 a permis de le rĂ©soudre.
AprĂšs avoir tout implĂ©mentĂ©, jâai dĂ©couvert quâil existe une technique dâoptimisation appelĂ©e object pooling pour les cas dâinstanciation frĂ©quente comme le tir de balles. Je compte lâappliquer plus tard quand jâaurai le temps.
Conception de lâexpĂ©rience utilisateur
Alors que la section prĂ©cĂ©dente portait sur âcrĂ©er un jeu dâaction oĂč lâon se dĂ©place sur le terrain pour Ă©liminer des ennemisâ, celle-ci concerne âimplĂ©menter une expĂ©rience utilisateur fluide et originaleâ. La plupart des travaux visuels sĂ©rieux ayant eu lieu en phase de dĂ©veloppement avancĂ©, je les aborderai dans le prochain article.
Caméra

void Move()
{
transform.position = Vector3.Lerp(
transform.position,
player.transform.position,
Time.deltaTime * moveSpeed
);
}
void Vignette()
{
targetVignetteValue = inventoryIsOpen ? 0.35f : 0f;
vignette.intensity.value = Mathf.Lerp(
vignette.intensity.value,
targetVignetteValue,
Time.deltaTime * vignetteSpeed
);
}
Il y a une option appelĂ©e vignettage que jâobserve attentivement en retouchant des photos. Cette fonction assombrit les bords de lâĂ©cran pour concentrer le regard au centre. Le post-processing dâUnity ayant la mĂȘme option, jâai pensĂ© quâil serait parfait de lâappliquer Ă lâouverture de lâinventaire.
Jâai donc fait en sorte que la valeur du vignettage passe Ă environ 0,35 lors de lâaccĂšs Ă lâinventaire. Comme pour le mouvement de la camĂ©ra, le vignettage est gĂ©rĂ© en douceur avec Lerp. Pendant le dĂ©veloppement, trouver les bonnes valeurs pour vignetteSpeed, moveSpeed et autres paramĂštres Ă©tait difficile. Je jouais, ajustais, rejouais, rĂ©ajustais, et quand je nâĂ©tais pas satisfait en travaillant sur dâautres parties, je revenais modifier â jâai cherchĂ© la bonne valeur tout au long du dĂ©veloppement.
URP

Au dĂ©but, jâutilisais tant bien que mal les effets de lumiĂšre de base dâUnity 2D, mais il y avait des insatisfactions Ă plusieurs endroits. En appliquant URP (Universal Render Pipeline) comme alternative, le rendu visuel sâest nettement amĂ©liorĂ©. Il offre des effets lumineux agrĂ©ables et doux par dĂ©faut, tout en permettant de rĂ©gler lâoption Falloff Strength pour crĂ©er une lumiĂšre plus subtile ou Ă©clatante, ou dâutiliser lâoption Shadows pour des effets de lumiĂšre et dâombre comme ci-dessus â vraiment utile.
Cependant, en ajoutant Light2D Ă chaque balle ou ennemi, le tĂ©lĂ©phone chauffait rapidement. Cela semblait consommer pas mal de ressources GPU, donc je nâai pas pu lâutiliser activement, ne le gardant que pour enrichir lâeffet de flamme lors des tirs.
Conclusion
Jâai briĂšvement rĂ©sumĂ© les activitĂ©s de dĂ©veloppement initial. En Ă©crivant, je rĂ©alise que je ne me souviens pas trĂšs bien de ce que jâai ressenti pendant cette pĂ©riode. Nâayant pas pu capturer toutes mes pensĂ©es et mes efforts, jâessaierai de prendre des notes plus souvent au cours du dĂ©veloppement.
MalgrĂ© tout, en implĂ©mentant diverses choses moi-mĂȘme, jâai dĂ©couvert que crĂ©er un jeu est bien plus complexe que je ne le pensais. En particulier, les autres jeux qui ne suivent pas les tendances populaires mais cherchent de nouveaux paradigmes, et qui les implĂ©mentent avec succĂšs, sont vraiment impressionnants. Et personnellement, en tant que quelquâun qui trouve cela admirable, cela mâa un peu donnĂ© envie.