Partie 41: Monstres partie 3: Comportements et attaque

Téléchargements

Code source
Exécutable de l'éditeur de niveaux - exactement le même que la partie 40 (Windows 32bits)
Exécutable du jeu (Windows 32bits)

Avant d'essayer de compiler le code, allez dans l'onglet "Projets" dans le menu de gauche, séléctionnez l'onglet "Run" pour votre kit,
et mettez dans "Working directory" le chemin de "editor\data" pour l'éditeur ou "game\data" pour le jeu.

Comportements de groupe

Quand vous jouez au jeu d'origine, si vous rencontrez un groupe de monstres, vous verrez qu'il aura différents
comportements. Il peut soit: En fait, certains monstres peuvent aussi vous attaquer avec des sorts même s'ils ne sont pas juste à coté de vous,
mais on ne parlera pas de ça pour le moment.

Pour implémenter ce type d'I.A. dans un jeu on utilise généralement une "machine à états finis".
Mais n'ayez pas peur, ce n'est pas aussi compliqué que ça en a l'air.

Dans une machine à états finis, un système peut avoir plusieurs états. Ici, notre groupe de monstres peut être dans
4 états (Balade, Poursuite, Combat ou Fuite).
Et sous des conditions spécifiques, on peut faire une transition d'un état à l'autre.

Dans cette partie on va seulement utiliser 2 états: Balade et Combat.

Maintenant pour les transitions:

Voici une image qui devrait être plus claire:

Implémentation des états

Auparavant on avait défini les monstres et les groupes de monstres comme des structures à l'intérieur de la classe
CMonsters.
Maintenant, comme ils vont devenir plus complexes et avoir leurs propres méthodes, c'est plus simple de les définir
comme des classes séparées.

		//---------------------------------------------------------------------------------------------
		class CMonster
		{
		public:
			[...]

			EMonsterPos pos;
			EMonsterDir dir;
			int life;
			[...]
		};

		//---------------------------------------------------------------------------------------------
		class CMonsterGroup2
		{
		public:
			[...]

			CMonster        monsters[4];
			EMonsterDir     dir;
			[...]
		};
				
Notez que j'ai renommé SMonsterGroup en CMonsterGroup2 pour éviter la confusion avec la classe CMonsterGroup
déclarée dans "map.h".

Maintenant, comme on l'a dit, chaque groupe de monstres est dans un état donné (soit balade, soit combat).
Alors on va leur ajouter une variable "state":

		enum EMonsterState
		{
			eMonsterState_Wandering,
			eMonsterState_Fight
		};

		class CMonsterGroup2
		{
		public:
			[...]
			EMonsterState   state;
		};
				
Pour changer la valeur de cette variable state, on va avoir besoin d'une fonction update() qui sera appelée à
chaque frame.
Cette fonction va appeler soit une fonction wander() soit une fonction fight() en fonction de l'état courant
du groupe.

		void CMonsterGroup2::update(CVec2 mapPos, uint8_t type)
		{
			[...]

			// gère le comportement du groupe pour chaque état
			switch (state)
			{
				case eMonsterState_Wandering:
					wander(mapPos, type);
					break;

				case eMonsterState_Fight:
					fight(mapPos, type);
					break;
			}
		}
				
Au niveau du groupe ces fonctions ne font pas grand chose à part gérer les transitions entre états en fonction de
la position du joueur.

		//---------------------------------------------------------------------------------------------
		void CMonsterGroup2::wander(CVec2 mapPos, uint8_t type)
		{
			// transition en état de combat ?
			CVec2   dist = player.pos - mapPos;
			bool    isNearPlayer = (dist.x == 0 && ABS(dist.y) == 1) || (dist.y == 0 && ABS(dist.x) == 1);

			if (isNearPlayer == true)
			{
				state = eMonsterState_Fight;
				[...]
			}
		}

		//---------------------------------------------------------------------------------------------
		void CMonsterGroup2::fight(CVec2 mapPos, uint8_t type)
		{
			// transition en mode ballade ?
			CVec2   dist = player.pos - mapPos;
			bool    isNearPlayer = (dist.x == 0 && ABS(dist.y) == 1) || (dist.y == 0 && ABS(dist.x) == 1);

			if (isNearPlayer == false)
			{
				state = eMonsterState_Wandering;
			}
		}
				
Dans la fonction update de notre groupe on va aussi appeler une fonction update() pour chaque monstre à dans le
groupe.

		void CMonsterGroup2::update(CVec2 mapPos, uint8_t type)
		{
			// met à jour tous les monstres du groupe
			for (int i = 0; i < 4; ++i)
				if (monsters[i].pos != eMonsterPosNone)
					monsters[i].update(*this, mapPos, type);

			[...]
		}
				

Comportements des monstres

Chaque monstre va se comporter différemment en fonction de l'état du groupe.
Alors la fonction update() de chaque monstre va elle aussi appeler soit une fonction wander(), soit une fonction
fight().

		void CMonster::update(CMonsterGroup2& group, CVec2 mapPos, uint8_t type)
		{
			displayAttackTimer.update();

			switch (group.state)
			{
				case eMonsterState_Wandering:
					wander();
					break;

				case eMonsterState_Fight:
					fight(mapPos, type);
					break;
			}
		}
				
On a dit qu'on ne ferait rien dans l'état balade, donc la fonction wander() sera vide.
Mais dans l'état combat On veut lancer l'animation d'attaque du monstre aléatoirement.
Alors regardons les valeurs dont nous aurons besoin.

Paramètres d'attaque

On va ajouter 3 paramètres à chaque monstre dans "monsters.xml":

		<monsters>
			[...]

			<!-- 01 Scorpion Géant -->
			<monster name = "MONSTER01">
				[...]
				<time_between_atk>200</time_between_atk>
				<atk_display_time>40</atk_display_time>
				<atk_sound>Attack_Scorpion.wav</atk_sound>
				[...]
			</monster>
				

Pour gérer ces temps on va ajouter des timers à notre monstre:

		class CMonster
		{
		public:
			[...]
			CTimer  displayAttackTimer;
			CTimer  nextAttackTimer;
		};
				
Dans le jeu d'origine le temps entre 2 attaques était calculé avec une formule qui ressemblait à ça:

		void CMonster::initNextAttack(uint8_t type)
		{
			int minTime = monsters.monstersDatas[type].timeBetweenAttack;
			int nextTime = minTime + (rand() % 40) - 10;

			if (minTime > 150)
				nextTime += (rand() % 80) - 20;

			nextAttackTimer.set(nextTime, true);
		}
				
Cette fonction initialise un de nos timers alors on l'appellera au moment où on entre dans l'état "fight" du groupe.
Et en même temps on va tourner le monstre vers le joueur:

		void CMonsterGroup2::wander(CVec2 mapPos, uint8_t type)
		{
			// transition en état de combat ?
			[...]

			if (isNearPlayer == true)
			{
				state = eMonsterState_Fight;

				// direction vers le joueur
				EMonsterDir dir;

				if (dist.x > 0)
					dir = eMonsterDirRight;
				else if (dist.x < 0)
					dir = eMonsterDirLeft;
				else if (dist.y > 0)
					dir = eMonsterDirDown;
				else
					dir = eMonsterDirUp;

				// initialise chaque monstre du groupe
				for (int i = 0; i < 4; ++i)
					if (monsters[i].pos != eMonsterPosNone)
					{
						monsters[i].dir = dir;
						monsters[i].initNextAttack(type);
					}
			}
		}
				
Ensuite, dans la fonction fight du monstre, on update ce timer.
Quand il se termine, on joue le son d'attaque, on lance le timer d'affichage et on relance le timer "entre
attaques".

		void CMonster::fight(CVec2 mapPos, uint8_t type)
		{
			if (nextAttackTimer.update() == true)
			{
				QString soundName = monsters.monstersDatas[type].attackSound;

				if (soundName.isEmpty() == false)
					sound->play(mapPos, soundName.toLocal8Bit().constData());

				int attackTime = monsters.monstersDatas[type].attackDisplayTime;
				displayAttackTimer.set(attackTime, true);
				initNextAttack(type);
			}
		}
				

Afficher l'attaque


On a vu les sprites d'attaque dans la partie précédente.
Notez qu'ils remplacent seulement une image "de face" (on n'a pas de sprite d'attaque pour les cotés ou le dos).
Donc quand on récupère le sprite du monstre dans CMonsters::draw() on doit seulement tester si le timer d'affichage
tourne dans le cas d'un monstre de face:

		// sprite
		[...]

		if (tableDir == eMonsterDirDown)
		{
			// de face
			if (monster.displayAttackTimer.isRunning())
			{
				// attaque
				spriteName = monstersDatas[type].attackImage;
				if (spriteName.isEmpty() == true)
					spriteName = monstersDatas[type].frontImage;
			}
			else
			{
				// normal
				spriteName = monstersDatas[type].frontImage;
			}
		}
		[...]