Part 46: Monsters Part 6: wandering

Downloads

Source code
Executable for the map editor - exactly the same as in part 40 (Windows 32bits)
Executable for the game (Windows 32bits)

Before you try to compile the code go to the "Projects" tab on the left menu, select the "Run" settings for your kit,
and set the "Working directory" to the path of "editor\data" for the editor or "game\data" for the game.

Monsters parameters

In this part we will make the monsters' groups move randomly when they are wandering.
First let's see what parameters we will need.

		<!-- 01 Giant Scorpion -->
		<monster name = "MONSTER01">
            [...]
			<movement_time>80</movement_time>
			<move_sound>Move_Screamer.wav</move_sound>
            [...]
		</monster>
				
"move_sound" is the sound of the steps that will be played every time the group moves.
Some monsters like the ghosts won't play sounds.

"movement_time" is the base time between 2 steps. It will be a little bit randomized

		void CMonsterGroup2::initMoveTimer(int type)
		{
			CMonsters::SMonsterData&    data = monsters.monstersDatas[type];
			int t = MAX(10, RANDOM(40) + data.movementTime - 10);
			moveTimer.set(t, true);
		}
				
This timer is set when the group is initialized by calling an enterWanderState() function.
And the same function will be called when we transition from the fight state to the wander one.

		void CMonsterGroup2::enterWanderState(int type)
		{
			state = eMonsterState_Wandering;
			initMoveTimer(type);
		}
				
Now, in the wander state, everything appears only when the timer fires - even the transition to "fight" to avoid
that the group turns immediately towards the player when it is near him.

		void CMonsterGroup2::wander(CVec2 mapPos, uint8_t type)
		{
			if (moveTimer.update() == true)
			{
				initMoveTimer(type);

				// transition to fight state ?
				CVec2   dist = player.pos - mapPos;
				bool    isNearPlayer = (dist.x == 0 && ABS(dist.y) == 1) || (dist.y == 0 && ABS(dist.x) == 1);

				if (isNearPlayer == true)
				{
					[...]
				}
				else
				{
					// wander
					[...]
				}
			}
		}
				

Moving

In the "wander" part of the test, we will first choose where we will go.
So we will make a list of all the available directions and choose one randomly.
The list is created like that:

		// list the possible directions
		EWallSide   possibleDirs[4];
		int nbPossibleDirs = 0;

		for (int dir = eWallSideUp; dir < eWallSideMax; ++dir)
			if (canMove(mapPos, (EWallSide)dir) == true)
			{
				possibleDirs[nbPossibleDirs++] = (EWallSide)dir;
			}
				
The canMove() function tells if we can walk on a given tile.
We will not walk into an open pit, a teleporter or a closed door - although in the original game some monsters
could take teleporters and fall in the pits, but actually it was extremely rare, so there is no need to bother with
these cases at the moment.
Note the getNewPos() function only compute the position of the destination tile given a direction.

		CVec2 CMonsterGroup2::getNewPos(CVec2 pos, EWallSide side)
		{
			CVec2   newPos = pos;

			switch (side)
			{
			case eWallSideUp:
				newPos.y--;
				break;

			case eWallSideDown:
				newPos.y++;
				break;

			case eWallSideLeft:
				newPos.x--;
				break;

			case eWallSideRight:
				newPos.x++;
				break;

			default:
				break;
			}

			return newPos;
		}

		//---------------------------------------------------------------------------------------------
		bool CMonsterGroup2::canMove(CVec2 pos, EWallSide side)
		{
			CVec2   newPos = getNewPos(pos, side);

			CTile*  tile = map.getTile(pos);
			CTile*  destTile = map.getTile(newPos);

			// hit a wall
			if (tile->mWalls[side].getType() != 0)
				return false;

			// out of the map
			if (destTile == NULL)
				return false;

			switch (destTile->getType())
			{
				case eTileDoor:
					if (doors.isOpened(destTile) == false)
						return false;
					break;

				case eTileStairs:
				case eTileTeleporter:
					return false;

				case eTilePit:
					if (destTile->getBoolParam("isClosed") == false)
						return false;
					break;

				default:
					break;
			};

			// monster group
			if (map.findMonsterGroupIndex(newPos) != -1)
				return false;

			// player
			if (newPos == player.pos)
				return false;

			return true;
		}
				
Back in our main routine, now that we have the directions list, we choose one randomly, move the group and play the
"move" sound.

		if (nbPossibleDirs > 0)
		{
			// chose a random direction and move
			EWallSide   chosenDir = possibleDirs[RANDOM(nbPossibleDirs)];

			CMonsterGroup*  group = map.findMonsterGroup(mapPos);
			group->mPos = getNewPos(mapPos, chosenDir);

			if (data.moveSound.isEmpty() == false)
				sound->play(group->mPos, data.moveSound.toLocal8Bit().constData());
		}
				
And we rotate each monster in a random direction - even in the case that we did not move.

		// rotate monsters randomly
		for (int i = 0; i < 4; ++i)
			mMonsters[i].dir = (EMonsterDir)RANDOM(4);
				

Last words

This part was a little bit short, but in the next one we will see the monsters seek mode.
And there will be a lot to talk about: how the monsters see the player, how they walk towards him...
At first I thought I would talk about the 2 modes in this part, but I think it would have been definitely too long,
and I think it's better to keep all the things about the seek mode as a whole.
So see you next time.