Historique des modifications - Message

Message #449

Sujet: Petit moteur "tilemap" en C# avec Irrlicht.


Type Date Auteur Contenu
Dernière modification 02-11-2006 17:32:59 Agar
Ce tutorial est un peu gros (500 Ko), avec des fichiers de texture et des modèles. Vous pouvez le télécharger ici: http://irrlichtfr.free.fr/Tutoriaux/tiletutorial.zip

ATTENTION: Irrlicht.DLL et Irrlicht.Net.DLL ne sont PAS présents dans l'archive. Vous devrez les copier dans le répertoire du programme pour qu'il se lance.

  • -----------------------------------------------------------------------
  • -----------------------------------------------------------------------
  • -----------------------------------------------------------------------

Voici le code source de l'unique fichier .CS

Notez bien que vous aurez besoin des fichiers présents dans l'archive tiletutorial.zip pour le faire tourner.
  • -----------------------------------------------------------------------
  • -----------------------------------------------------------------------
  • -----------------------------------------------------------------------


using System;
using System.Collections.Generic;
using System.Windows.Forms;
using Irrlicht;
using Irrlicht.Core;
using Irrlicht.Scene;
using Irrlicht.Video;

/*
*
* TUTORIAL - Moteur "tile engine" très très basique avec Irrlicht.
* Ecrit par Ambroise Garel (agar@cafedefaune.com) pour www.irrlicht.fr
*
* Le but de ce tutorial est de démonter au lecteur qu'il est extrêmement simple de réaliser un petit
* moteur utlisant des "tiles" avec Irrlicht.
*
* Cette version est très basique, mais donne les outils principaux (repérage de l'endroit sur lequel le joueur
* clique afin de déplacer son perso, etc...)
*
* Ce moteur peut servir de base à la réalisation d'un Diablo-like, d'un "Zelda", d'un jeu de stratégie, etc...
*
* Merci de bien vouloir me tenir au courant si vous avez de ce tutorial autre chose qu'un usage privé
* (upload sur votre site, traduction, etc...). Pas pour vous crier dessus, juste pour être au courant. Merci!
*
* NB: Je sais que tous ces objets statiques ne sont pas beaux du tout, mais c'est un tutorial, pas un vrai
* programme. J'ai cherché à faire au plus lisible et au plus simple.
*
* */

namespace TileTutorial
{
public class TileEngine
{
// La taille de la carte
const int MAPSIZE_X = 32;
const int MAPSIZE_Y = 32;

// La taille de la fenêtre d'affichage
const int WINDOWSIZE_X = 800;
const int WINDOWSIZE_Y = 600;

// Le device Irrlicht et ses petits.
public static IrrlichtDevice dev;
public static ISceneManager scm;
public static IVideoDriver drv;

// Le receveur d'évènement, une classe nommée MyEventReceiver (voir plus bas)
public static MyEventReciever evnt;

// La camera
public static ICameraSceneNode cam;

// Les tiles
public static ISceneNode[,] FloorTile = new ISceneNode[MAPSIZE_X, MAPSIZE_Y];

// Le joueur
public static IAnimatedMeshSceneNode Player;

// La position où le joueur doit aller.
public static Vector3D playertargetpos;

// Une source de lumière
public static ILightSceneNode light;

// Tant que Running est "true", le programme tourne.
public static bool Running;

// Fonction Main()
// Tout commence ici.
[STAThread]
static void Main()
{
// On demande à l'utilisateur s'il souhaite utiliser OpenGL ou DirectX
DialogResult DR;
DR = MessageBox.Show("Souhaitez-vous utiliser OpenGL ? Si vous répondez non, Direct3D 9.0c sera utilisé.", "OpenGL ou non ?", MessageBoxButtons.YesNo, MessageBoxIcon.Question);

DriverType DT;
if (DR == DialogResult.Yes)
DT = DriverType.OPENGL;
else
DT = DriverType.DIRECT3D9;

// On demande à l'utilisateur s'il souhaite utiliser du normal mapping
bool usenormals = false;
DR = MessageBox.Show("Souhaitez-vous utiliser des normal maps ?", "Normal maps ou vieilles textures à l'ancienne ?", MessageBoxButtons.YesNo, MessageBoxIcon.Question);

if (DR == DialogResult.Yes)
usenormals = true;

// Création du device Irrlicht
dev = new IrrlichtDevice(DT, new Dimension2D(WINDOWSIZE_X, WINDOWSIZE_Y), 32, false, false, false, false, new IntPtr());
scm = dev.SceneManager;
drv = dev.VideoDriver;

dev.WindowCaption = "Pressez échap pour quitter";

// Création du receveur d'évènements
evnt = new MyEventReciever();
dev.EventReceiver = evnt;

// On charge le mesh utilisé pour le sol.
IMesh TileMesh = scm.GetMesh(@"..\\..\\media\\floor.x").GetMesh(0);
// Il faut créer les tangeantes pour utiliser le normal mapping.
if (usenormals) TileMesh = scm.MeshManipulator.CreateMeshWithTangents(TileMesh);

// Et maintenant la petite texture qui va avec.
ITexture TileTex = drv.GetTexture(@"..\\..\\media\\floor.jpg");

// Et la normal map.
ITexture TileTexNormal = drv.GetTexture(@"..\\..\\media\\floor_normal.jpg");

// Création du monde...

// 1- D'abord, on ajoute les "tiles" du sol
for (int i = 0; i < MAPSIZE_X; i++)
for (int j = 0; j < MAPSIZE_Y; j++)
{
// Très important : les nodes des tiles ont une ID de 1! Cela servira lors de la détection
// des clics.
FloorTile[i, j] = scm.AddMeshSceneNode(TileMesh, null, 1);
// FloorTile[i, j].SetMaterialFlag(MaterialFlag.LIGHTING, false);
FloorTile[i, j].SetMaterialTexture(0, TileTex);
FloorTile[i, j].Position = new Vector3D(i * 5, 0, j * 5);

if (usenormals)
{
FloorTile[i, j].SetMaterialTexture(1, TileTexNormal);
FloorTile[i, j].SetMaterialType(MaterialType.NORMAL_MAP_SOLID);
}
}

// Et voilà votre incarnation : syndney.md2
Player = scm.AddAnimatedMeshSceneNode(scm.GetMesh(@"..\\..\\media\\sydney.md2"), null, 0);
Player.SetMaterialTexture(0, drv.GetTexture(@"..\\..\\media\\sydney.bmp"));
Player.Position = new Vector3D(2.5f, 2.5f, 2.5f);
// Syndey est un peu grosse, alors on va la réduire.
Player.Scale = new Vector3D(0.15f, 0.15f, 0.15f);
Player.SetMD2Animation(MD2AnimationType.STAND);

// Pour le moment, le joueur ne va nulle part, donc sa position cible sera sa position actuelle.
playertargetpos = Player.Position;

// On ajoute la caméra...
cam = scm.AddCameraSceneNode(null, new Vector3D(0, 25, 25), new Vector3D(0, 0, 0), 0);

// Et une petite source de lumière, histoire qu'on y voit quelque chose.
light = scm.AddLightSceneNode(null, new Vector3D(25, 25, 25), new Colorf(1.0f, 0.95f, 0.9f), 100, 0);

// C'est parti! Tant que running est "true", la boucle principale tourne.
Running = true;

// Et voici donc cette boucle de dessin principale.
DrawLoop();

// On est sorti de la boucle principale car la fenêtre n'est plus active.
// Il ne reste plus...

// 1- Qu'à détruire le device
dev.CloseDevice();

// 2- Et à faire un peu le ménage dans la mémoire.
GC.Collect();
}

static void DrawLoop()
{
// Boucle tant que la fenêtre est active.
while (Running)
{
// On dessine toutes ces petites choses.
drv.BeginScene(true, true, new Color(0, 0, 0, 0));
scm.DrawAll();
drv.EndScene();

// On laisse un peu respirer l'application.
Application.DoEvents();

// Si le joueur ne se trouve pas à sa position cible, il faut qu'il bouge...
if (
(playertargetpos.X != Player.Position.X) ||
(playertargetpos.Z != Player.Position.Z)
)
{
float speedx = 0.0f;
float speedz = 0.0f;

// On vérifie l'axe X
if (Player.Position.X < playertargetpos.X) speedx = 0.1f;
else if (Player.Position.X > playertargetpos.X) speedx = -0.1f;

// On vérifie l'axe Z
if (Player.Position.Z < playertargetpos.Z) speedz = 0.1f;
else if (Player.Position.Z > playertargetpos.Z) speedz = -0.1f;

// On fait bouger tout ça...
Player.Position = new Vector3D(Player.Position.X + speedx, 2.5f, Player.Position.Z + speedz);

//Pour éviter un effet de "sautillement", si le joueur est suffisemment près de sa position
//cible, on l'y place directement.
if (Math.Abs(Player.Position.X - playertargetpos.X) < 0.15f)
Player.Position = new Vector3D(playertargetpos.X, 2.5f, Player.Position.Z);

if (Math.Abs(Player.Position.Z - playertargetpos.Z) < 0.15f)
Player.Position = new Vector3D(Player.Position.X, 2.5f, playertargetpos.Z);
}

// La caméra suit le perso
cam.Position = new Vector3D(Player.Position.X + 10, 15, Player.Position.Z + 10);
cam.Target = Player.Position;

// La lumière suit le joueur, puisqu'il a un "ring of light +1"
// Je ne vous l'avait pas dit? Maintenant, c'est fait.
light.Position = new Vector3D(Player.Position.X +10, 5, Player.Position.Z +10);
}
}
}

public class MyEventReciever : IEventReceiver
{
// Lecture des évènements Irrlicht (en l'occurence, les clics de souris)
public bool OnEvent(Event e)
{
if (e.Key == KeyCode.KEY_ESCAPE)
TileEngine.Running = false;

// Si c'est pas la souris, poubelle. Il n'y a que la souris qui nous intéresse.
if (e.Type != EventType.MouseInput) return false;

// Un clic gauche... Ah, ah! Ca veut dire que le joueur veut bouger.
if (e.MouseInputType == MouseInputEvent.PressedDownLeft)
{
// On doit trouver sur quel tile le joueur a cliqué.
// Ca se fait avec le SceneCollisionManager.
// Notez que le paramètre "idbitmask" permet de ne prendre en compte que certains nodes.
// (Ici, ceux avec l'ID 1, donc les tiles. Cliquer sur Sydney ne fait donc rien).
ISceneNode ClickedNode =
TileEngine.scm.SceneCollisionManager.GetSceneNodeFromScreenCoordinatesBB(e.MousePos, 1, true);

// Si le joueur n'a pas cliqué sur une tile, on oublie tout ça...
if (ClickedNode == null) return false;

// Sinon, le joueur doit maintenant aller sur cette tile.
// On ajoute 2,5 aux coordonnées X et Z pour que le joueur aille au centre de la tile, pas
// dans un coin.
TileEngine.playertargetpos =
new Vector3D(ClickedNode.Position.X + 2.5f, 0, ClickedNode.Position.Z + 2.5f);
}

return false;
}
}
}

/*
*
* Merci d'avoir lu mon tutorial.
* Si vous avez des questions ou des suggestions, vous pouvez m'écrire à l'adresse agar@cafedefaune.com.
*
* QUELQUES SUGGESTIONS POUR ALLER PLUS LOIN:
* ==========================================
*
* 1- Déjà, il serait bien que le perso soit animé et se tourne dans la direction où il se déplace. Si le premier
* point est très simple à régler, le deuxième nécessite de sortir un peu de trigonométrie.
*
* 2- La carte est petite par défaut (32x32), donc ça tourne pas trop mal. Maintenant, avec une carte plus grande,
* les tiles trop nombreuses risquent de noyer Irrlicht (et la carte vidéo) sous une avalanche de "nodes". Il
* faudra donc écrire une petite fonction qui détermine si telle ou telle tile est visible à un moment précis et
* affiche dynamiquement uniquement les tiles nécessaires.
*
* 3- En utilisant les "id" des nodes, pourquoi ne pas ajouter des ennemis? Ensuite, quand on clique, le programme
* devra vérifier si l'ID du node cliqué est 1 (le sol) ou 2, par exemple. Si c'est 2, alors lancer la séquence
* d'attaque... Avec cette technique, il est très très rapide de créer un "petit diablo".
*
* 4- Ajouter des murs n'est pas très compliqué non plus. Il suffit d'utiliser le modèle "floor.x" et de le faire
* pivoter de 90 degrés.
*
* 5- Qui dit mur dit obstacle. Et qui dit obstacle dit pathfinding. Ca peut être intéressant à ajouter, mais
* c'est un peu plus de travail.
*
* 6- Et allez, si vous avez du sommeil à perdre... Vous nous écrirez bien un petit générateur aléatoire de
* donjons, non ?
*
*/
Création du message 02-11-2006 16:07:18 Agar
Ce tutorial est un peu gros (500 Ko), avec des fichiers de texture et des modèles. Vous pouvez le télécharger ici: http://irrlichtfr.free.fr/Tutoriaux/tiletutorial.zip

ATTENTION: Irrlicht.DLL et Irrlicht.Net.DLL ne sont PAS présents dans l'archive. Vous devrez les copier dans le répertoire du programme pour qu'il se lance.

  • -----------------------------------------------------------------------
  • -----------------------------------------------------------------------
  • -----------------------------------------------------------------------

Voici le code source de l'unique fichier .CS

Notez bien que vous aurez besoin des fichiers présents dans l'archive tiletutorial.zip pour le faire tourner.
  • -----------------------------------------------------------------------
  • -----------------------------------------------------------------------
  • -----------------------------------------------------------------------


using System;
using System.Collections.Generic;
using System.Windows.Forms;
using Irrlicht;
using Irrlicht.Core;
using Irrlicht.Scene;
using Irrlicht.Video;

/*
*
* TUTORIAL - Moteur "tile engine" très très basique avec Irrlicht.
* Ecrit par Ambroise Garel (agar@cafedefaune.com) pour www.irrlicht.fr
*
* Le but de ce tutorial est de démonter au lecteur qu'il est extrêmement simple de réaliser un petit
* moteur utlisant des "tiles" avec Irrlicht.
*
* Cette version est très basique, mais donne les outils principaux (repérage de l'endroit sur lequel le joueur
* clique afin de déplacer son perso, etc...)
*
* Ce moteur peut servir de base à la réalisation d'un Diablo-like, d'un "Zelda", d'un jeu de stratégie, etc...
*
* Merci de bien vouloir me tenir au courant si vous avez de ce tutorial autre chose qu'un usage privé
* (upload sur votre site, traduction, etc...). Pas pour vous crier dessus, juste pour être au courant. Merci!
*
* NB: Je sais que tous ces objets statiques ne sont pas beaux du tout, mais c'est un tutorial, pas un vrai
* programme. J'ai cherché à faire au plus lisible et au plus simple.
*
* */

namespace TileTutorial
{
public class TileEngine
{
// La taille de la carte
const int MAPSIZE_X = 32;
const int MAPSIZE_Y = 32;

// La taille de la fenêtre d'affichage
const int WINDOWSIZE_X = 800;
const int WINDOWSIZE_Y = 600;

// Le device Irrlicht et ses petits.
public static IrrlichtDevice dev;
public static ISceneManager scm;
public static IVideoDriver drv;

// Le receveur d'évènement, une classe nommée MyEventReceiver (voir plus bas)
public static MyEventReciever evnt;

// La camera
public static ICameraSceneNode cam;

// Les tiles
public static ISceneNode[,] FloorTile = new ISceneNode[MAPSIZE_X, MAPSIZE_Y];

// Le joueur
public static IAnimatedMeshSceneNode Player;

// La position où le joueur doit aller.
public static Vector3D playertargetpos;

// Une source de lumière
public static ILightSceneNode light;

// Tant que Running est "true", le programme tourne.
public static bool Running;

// Fonction Main()
// Tout commence ici.
[STAThread]
static void Main()
{
// On demande à l'utilisateur s'il souhaite utiliser OpenGL ou DirectX
DialogResult DR;
DR = MessageBox.Show("Souhaitez-vous utiliser OpenGL ? Si vous répondez non, Direct3D 9.0c sera utilisé.", "OpenGL ou non ?", MessageBoxButtons.YesNo, MessageBoxIcon.Question);

DriverType DT;
if (DR == DialogResult.Yes)
DT = DriverType.OPENGL;
else
DT = DriverType.DIRECT3D9;

// On demande à l'utilisateur s'il souhaite utiliser du normal mapping
bool usenormals = false;
DR = MessageBox.Show("Souhaitez-vous utiliser des normal maps ?", "Normal maps ou vieilles textures à l'ancienne ?", MessageBoxButtons.YesNo, MessageBoxIcon.Question);

if (DR == DialogResult.Yes)
usenormals = true;

// Création du device Irrlicht
dev = new IrrlichtDevice(DT, new Dimension2D(WINDOWSIZE_X, WINDOWSIZE_Y), 32, false, false, false, false, new IntPtr());
scm = dev.SceneManager;
drv = dev.VideoDriver;

dev.WindowCaption = "Pressez échap pour quitter";

// Création du receveur d'évènements
evnt = new MyEventReciever();
dev.EventReceiver = evnt;

// On charge le mesh utilisé pour le sol.
IMesh TileMesh = scm.GetMesh(@"..\\..\\media\\floor.x").GetMesh(0);
// Il faut créer les tangeantes pour utiliser le normal mapping.
if (usenormals) TileMesh = scm.MeshManipulator.CreateMeshWithTangents(TileMesh);

// Et maintenant la petite texture qui va avec.
ITexture TileTex = drv.GetTexture(@"..\\..\\media\\floor.jpg");

// Et la normal map.
ITexture TileTexNormal = drv.GetTexture(@"..\\..\\media\\floor_normal.jpg");

// Création du monde...

// 1- D'abord, on ajoute les "tiles" du sol
for (int i = 0; i < MAPSIZE_X; i++)
for (int j = 0; j < MAPSIZE_Y; j++)
{
// Très important : les nodes des tiles ont une ID de 1! Cela servira lors de la détection
// des clics.
FloorTile[i, j] = scm.AddMeshSceneNode(TileMesh, null, 1);
// FloorTile[i, j].SetMaterialFlag(MaterialFlag.LIGHTING, false);
FloorTile[i, j].SetMaterialTexture(0, TileTex);
FloorTile[i, j].Position = new Vector3D(i * 5, 0, j * 5);

if (usenormals)
{
FloorTile[i, j].SetMaterialTexture(1, TileTexNormal);
FloorTile[i, j].SetMaterialType(MaterialType.NORMAL_MAP_SOLID);
}
}

// Et voilà votre incarnation : syndney.md2
Player = scm.AddAnimatedMeshSceneNode(scm.GetMesh(@"..\\..\\media\\sydney.md2"), null, 0);
Player.SetMaterialTexture(0, drv.GetTexture(@"..\\..\\media\\sydney.bmp"));
Player.Position = new Vector3D(2.5f, 2.5f, 2.5f);
// Syndey est un peu grosse, alors on va la réduire.
Player.Scale = new Vector3D(0.15f, 0.15f, 0.15f);
Player.SetMD2Animation(MD2AnimationType.STAND);

// Pour le moment, le joueur ne va nulle part, donc sa position cible sera sa position actuelle.
playertargetpos = Player.Position;

// On ajoute la caméra...
cam = scm.AddCameraSceneNode(null, new Vector3D(0, 25, 25), new Vector3D(0, 0, 0), 0);

// Et une petite source de lumière, histoire qu'on y voit quelque chose.
light = scm.AddLightSceneNode(null, new Vector3D(25, 25, 25), new Colorf(1.0f, 0.95f, 0.9f), 100, 0);

// C'est parti! Tant que running est "true", la boucle principale tourne.
Running = true;

// Et voici donc cette boucle de dessin principale.
DrawLoop();

// On est sorti de la boucle principale car la fenêtre n'est plus active.
// Il ne reste plus...

// 1- Qu'à détruire le device
dev.CloseDevice();

// 2- Et à faire un peu le ménage dans la mémoire.
GC.Collect();
}

static void DrawLoop()
{
// Boucle tant que la fenêtre est active.
while (Running)
{
// On dessine toutes ces petites choses.
drv.BeginScene(true, true, new Color(0, 0, 0, 0));
scm.DrawAll();
drv.EndScene();

// On laisse un peu respirer l'application.
Application.DoEvents();

// Si le joueur ne se trouve pas à sa position cible, il faut qu'il bouge...
if (
(playertargetpos.X != Player.Position.X) ||
(playertargetpos.Z != Player.Position.Z)
)
{
float speedx = 0.0f;
float speedz = 0.0f;

// On vérifie l'axe X
if (Player.Position.X < playertargetpos.X) speedx = 0.1f;
else if (Player.Position.X > playertargetpos.X) speedx = -0.1f;

// On vérifie l'axe Z
if (Player.Position.Z < playertargetpos.Z) speedz = 0.1f;
else if (Player.Position.Z > playertargetpos.Z) speedz = -0.1f;

// On fait bouger tout ça...
Player.Position = new Vector3D(Player.Position.X + speedx, 2.5f, Player.Position.Z + speedz);

//Pour éviter un effet de "sautillement", si le joueur est suffisemment près de sa position
//cible, on l'y place directement.
if (Math.Abs(Player.Position.X - playertargetpos.X) < 0.15f)
Player.Position = new Vector3D(playertargetpos.X, 2.5f, Player.Position.Z);

if (Math.Abs(Player.Position.Z - playertargetpos.Z) < 0.15f)
Player.Position = new Vector3D(Player.Position.X, 2.5f, playertargetpos.Z);
}

// La caméra suit le perso
cam.Position = new Vector3D(Player.Position.X + 10, 15, Player.Position.Z + 10);
cam.Target = Player.Position;

// La lumière suit le joueur, puisqu'il a un "ring of light +1"
// Je ne vous l'avait pas dit? Maintenant, c'est fait.
light.Position = new Vector3D(Player.Position.X +10, 5, Player.Position.Z +10);
}
}
}

public class MyEventReciever : IEventReceiver
{
// Lecture des évènements Irrlicht (en l'occurence, les clics de souris)
public bool OnEvent(Event e)
{
if (e.Key == KeyCode.KEY_ESCAPE)
TileEngine.Running = false;

// Si c'est pas la souris, poubelle. Il n'y a que la souris qui nous intéresse.
if (e.Type != EventType.MouseInput) return false;

// Un clic gauche... Ah, ah! Ca veut dire que le joueur veut bouger.
if (e.MouseInputType == MouseInputEvent.PressedDownLeft)
{
// On doit trouver sur quel tile le joueur a cliqué.
// Ca se fait avec le SceneCollisionManager.
// Notez que le paramètre "idbitmask" permet de ne prendre en compte que certains nodes.
// (Ici, ceux avec l'ID 1, donc les tiles. Cliquer sur Sydney ne fait donc rien).
ISceneNode ClickedNode =
TileEngine.scm.SceneCollisionManager.GetSceneNodeFromScreenCoordinatesBB(e.MousePos, 1, true);

// Si le joueur n'a pas cliqué sur une tile, on oublie tout ça...
if (ClickedNode == null) return false;

// Sinon, le joueur doit maintenant aller sur cette tile.
// On ajoute 2,5 aux coordonnées X et Z pour que le joueur aille au centre de la tile, pas
// dans un coin.
TileEngine.playertargetpos =
new Vector3D(ClickedNode.Position.X + 2.5f, 0, ClickedNode.Position.Z + 2.5f);
}

return false;
}
}
}

/*
*
* Merci d'avoir lu mon tutorial.
* Si vous avez des questions ou des suggestions, vous pouvez m'écrire à l'adresse agar@cafedefaune.com.
*
* QUELQUES SUGGESTIONS POUR ALLER PLUS LOIN:
* ==========================================
*
* 1- Déjà, il serait bien que le perso soit animé et se tourne dans la direction où il se déplace. Si le premier
* point est très simple à régler, le deuxième nécessite de sortir un peu de trigonométrie.
*
* 2- La carte est petite par défaut (32x32), donc ça tourne pas trop mal. Maintenant, avec une carte plus grande,
* les tiles trop nombreuses risquent de noyer Irrlicht (et la carte vidéo) sous une avalanche de "nodes". Il
* faudra donc écrire une petite fonction qui détermine si telle ou telle tile est visible à un moment précis et
* affiche dynamiquement uniquement les tiles nécessaires.
*
* 3- En utilisant les "id" des nodes, pourquoi ne pas ajouter des ennemis? Ensuite, quand on clique, le programme
* devra vérifier si l'ID du node cliqué est 1 (le sol) ou 2, par exemple. Si c'est 2, alors lancer la séquence
* d'attaque... Avec cette technique, il est très très rapide de créer un "petit diablo".
*
* 4- Ajouter des murs n'est pas très compliqué non plus. Il suffit d'utiliser le modèle "floor.x" et de le faire
* pivoter de 90 degrés.
*
* 5- Qui dit mur dit obstacle. Et qui dit obstacle dit pathfinding. Ca peut être intéressant à ajouter, mais
* c'est un peu plus de travail.
*
* 6- Et allez, si vous avez du sommeil à perdre... Vous nous écrirez bien un petit générateur aléatoire de
* donjons, non ?
*
*/

Retour

Options Liens officiels Caractéristiques Statistiques Communauté
Préférences cookies
Corrections
irrlicht
irrklang
irredit
irrxml
Propulsé par Django
xhtml 1.0
css 2.1
884 membres
1440 sujets
11337 messages
Dernier membre inscrit: Saidov17
195 invités en ligne
membre en ligne: -
RSS Feed