Petit moteur "tilemap" en C# avec Irrlicht.

Proposé par Agar

le 02 November 2006 à 17h 07mn 18s

10668 visualisations



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 ([email protected]) 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 [email protected].
*
* 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 ?
*
*/


#1 

02-11-2006 18:11:11

izguit
Administrateur
Lieu: 127.0.0.1
Date d'inscription: 14-09-2006
Messages: 306
Site web

merci beaucoup de ta contribution smile
Je regarderai ça, ça m'a l'air bien fait
C'est hébergé smile


Athlon 64 3000+ // 1Go RAM // Geforce 6600GT 128Mo
Turion 64 X2 // 1Go RAM // ATI X1250

Hors ligne


#2 

02-11-2006 18:33:21

Agar
Membre
Date d'inscription: 01-11-2006
Messages: 16

izguit :

C'est hébergé smile


Merci beaucoup. J'ai modifié mon message en conséquence.

Dernière modification par Agar (02-11-2006 18:33:33)

Hors ligne


#3 

02-11-2006 20:25:23

Copland
Modérateur
Lieu: ZarbiLand
Date d'inscription: 22-09-2006
Messages: 657
Site web

Merci beaucoup pour ta contribution c'est vraiment sympa !


Config : I5 2400, ATI HD6870 1Go DDR5, 4Go DDR3.
Single Boot : Windows Seven.

Hors ligne


#4 

02-11-2006 20:31:19

kedu
Modérateur
Date d'inscription: 23-09-2006
Messages: 155

Merci pour ce tuto bien sympa ! De plus, j'ai l'impression qu'il y a une vague ces derniers temps d'amateurs du genre ; qui reprennent nos bons vieux jeux d'antan...

Hors ligne


#5 

10-09-2008 22:12:04

Krankmann
Petit nouveau
Date d'inscription: 07-09-2008
Messages: 2

J'ai essayé de compiler le tuto sous Mono (linux), et, une flopée d'erreurs apparait, parmi lesquelles certaines me semblent assez bizarres...

Enfin bref, j'ai "corrigé" (essayé de corriger serait plus juste) le code, et voilà ce que ça donne (étant donné que j'ai fait à moitié des copier-collers, et que j'ai repris une bonne partie afin de me familiariser, tous les commentaires ne sont pas là...)

Si ça peut intéresser quelqu'un...

Code:

using System;

using System.Collections.Generic;

using System.Windows.Forms;

using IrrlichtNETCP;
using IrrlichtNETCP.Inheritable;

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 device;

        public static SceneManager smgr;

        public static VideoDriver driver;



        // La camera

        public static CameraSceneNode camera;



        // Les tiles

        public static SceneNode[,] FloorTile = new SceneNode[Mapsize_X, Mapsize_Y];



        // Le joueur

        public static AnimatedMeshSceneNode Player;



        // La position où le joueur doit aller.

        public static vector3d playertargetpos;



        // Une source de lumière

        public static LightSceneNode light;
        
        public static bool Running;
        
        static double pi =3.1415926535;
        
        static void Main()

        {
            // Création du device Irrlicht
            device = new IrrlichtDevice(DriverType.OpenGL, new dimension2d(640, 480), 32, false, true, true, true);
            device.FileSystem.WorkingDirectory = "./media";
            device.OnEvent += new OnEventDelegate(device_OnEvent);
            smgr = device.SceneManager;
            driver = device.VideoDriver;
            //Initialisation du gestionnaire d'évènement
            

            // On charge le mesh utilisé pour le sol.
            Mesh TileMesh = smgr.GetMesh("floor.x").GetMesh(0);
            TileMesh = smgr.MeshManipulator.CreateMeshWithTangents(TileMesh);
            
            Texture TileTex = driver.GetTexture("floor.jpg");
            
            // Et la normal map.

            Texture TileTexNormal = driver.GetTexture("floor_normal.jpg");
            
            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] = smgr.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);





                        FloorTile[i, j].SetMaterialTexture(1, TileTexNormal);

                        FloorTile[i, j].SetMaterialType(MaterialType.NormalMapSolid);



                }
            
            Player = smgr.AddAnimatedMeshSceneNode(smgr.GetMesh("sydney.md2"));

            Player.SetMaterialTexture(0, driver.GetTexture("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.SetFrameLoop(0,159);
            
            playertargetpos = Player.Position;
            
            // On ajoute la caméra...

            camera = smgr.AddCameraSceneNode(null);
            camera.Position=new vector3d(0, 0, 0);                              
            camera.Target=new vector3d(0, 25, 25);                                 


            // Et une petite source de lumière, histoire qu'on y voit quelque chose.

            light = smgr.AddLightSceneNode(null, new vector3d(25, 25, 25), new Colorf(1.0f,1.0f, 0.95f, 0.9f), 100, 0);
            
                        // Et voici donc cette boucle de dessin principale.
            
            Running=true;

           // DrawLoop();
            int lastFPS = -1;
            while (device.Run())
            {
                if (device.WindowActive)
                {
                    device.VideoDriver.BeginScene(true, true, new Color(255, 100, 101, 140));

                    device.SceneManager.DrawAll();
                     driver.EndScene();
                    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 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);
                
                                // La caméra suit le perso

                camera.Position = new vector3d(Player.Position.X + 10, 15, Player.Position.Z + 10);

                camera.Target = Player.Position;

                    int fps = device.VideoDriver.FPS;

                }
            }



            // 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

            device.Dispose();



            // 2- Et à faire un peu le ménage dans la mémoire.

            GC.Collect();
            
        }
        static bool device_OnEvent(Event e)

        {

            if (e.KeyCode == KeyCode.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.MouseInputEvent) return false;



            // Un clic gauche... Ah, ah! Ca veut dire que le joueur veut bouger.

            if (e.MouseInputEvent == MouseInputEvent.LMousePressedDown)

            {

                // 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).

                SceneNode ClickedNode =

                    TileEngine.smgr.CollisionManager.GetSceneNodeFromScreenCoordinates(e.MousePosition, 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;
            }
                
                    
    }
}

PS : vu la date du dernier message, il se pourrait fortement que ces erreurs soient dues à un (voir plusieurs) changements de syntaxe dans irrlicht.

PPS : En fait, il s'agit d'une convertion vers Irrlicht.Net CP ^^

Dernière modification par Krankmann (10-09-2008 22:16:50)

Hors ligne


Options Liens officiels Caractéristiques Statistiques Communauté
Corrections
irrlicht
irrklang
irredit
irrxml
xhtml 1.0
css 2.1
Propulsé par FluxBB
Traduit par FluxBB.fr
882 membres
1429 sujets
11119 messages
Dernier membre inscrit: LiseBuisson96
38 invités en ligne
Aucun membre connecté
RSS Feed