[WIP] Pocket Dungeon

(Andrew) #1

Your the monsters, and you try and kill the invading “heroes”

https://github.com/bridge2nowhere/pocketDungeon

3 Likes

(Pharap) #2

A good start overall.


A few bits of advice/suggestions:

You can save memory by wrapping your strings in the F macro. E.g.

arduboy.print(F("A to switch between Jellies"));

(Note that this only works for string literals being passed to print or println.)

And you don’t need to call arduboy.clear after arduboy.begin because arduboy.begin already clears the screen.


I can think of a few other suggestions but it depends how many suggestions you’re open to receiving at the moment.

0 Likes

(Andrew) #3

I wrapped the strings as suggested. I am open to more feedback. I would like to get better at programming along the way.

1 Like

(Pharap) #4

In that case I have two suggestions.

I apologise in advance for the big info dump,
and I apologise if you already knew some/any of this.

Hopefully some or all of it will be useful.


Scoped enumerations

Firstly, you could replace screenMode with what’s called a ‘scoped enumeration’ (also informally referred to as an enum class).

Theres a good explanation of those here.
(Though you might need to read this about unscoped enumerations first.)

Basically instead of relying on remembering that 0 means splash screen, 1 means map et cetera,
you can define a custom type (e.g. GameState) and define what named values it can hold, like so:

// Specifying uint8_t means each object will occupy 8 bits
enum class GameState : uint8_t
{
	SplashScreen,
	Overworld,
	Combat,
};

Then you can create a variable like so:

GameState gameState = GameState::SplashScreen;

And test it like this:

if(gameState == GameState::SplashScreen)

And overwrite it like this:

gameState = GameState::MapScreen;

And you’re protected from accidentally overwriting it with an invalid value:

// Compiler error due to incompatible types
gameState = 10;

All the special names and bug prevention are just compiler magic.

When the code is compiled the resulting code will be the same as if you had just used a plain uint8_t/byte and unnamed numbers, it won’t be any bigger or any smaller.

But by using scoped enumerations instead of plain integers the source code becomes easier to read (because you’re using actual words, which helps to make the code more self-descriptive) and it’s harder to make mistakes.


Finite State Machines

My other suggestion is to just reorganise your code slightly, to something more like this:

void loop()
{
	if (!(arduboy.nextFrame()))
		return;

	arduboy.pollButtons();
		
	switch(screenMode)
	{
		case 0:
			updateSplashScreen();
			break;
		case 1:
			updateMap();
			break;
	}

	arduboy.clear();
		
	switch(screenMode)
	{
		case 0:
			drawSplashScreen();
			break;
		case 1:
			drawMap();
			break;
	}
	
	arduboy.display();
}

void updateMap()
{
	if(arduboy.justPressed(A_BUTTON))
	{
		switchActiveMonster();
	}
	
	if(arduboy.justPressed(LEFT_BUTTON))
	{
		activeMonster->monsterMove('l');
	}
	else if(arduboy.justPressed(RIGHT_BUTTON))
	{
		activeMonster->monsterMove('r');
	}
	else if(arduboy.justPressed(UP_BUTTON))
	{
		activeMonster->monsterMove('u');
	}
	else if(arduboy.justPressed(DOWN_BUTTON))
	{
		activeMonster->monsterMove('d');
	}
}

void drawMap()
{
	// Draws the cave walls, checks for movement, then draws the sprites
	drawCave();
	arduboy.drawBitmap(jelly1.posnX, jelly1.posnY, jelly1.pointerToMySprite, 8, 8, WHITE);
	arduboy.drawBitmap(jelly2.posnX, jelly2.posnY, jelly2.pointerToMySprite, 8, 8, WHITE);
	arduboy.drawBitmap(96, 32, jeff.pointerToMySprite, 8, 8, WHITE);
	arduboy.setCursor(0, 56);
	arduboy.print(F("A to switch between Jellies"));
	//arduboy.print(F("j1 hp="));
	//arduboy.print(jelly1.hp);
	//arduboy.print(F("  j2 hp="));
	//arduboy.print(jelly2.hp);
}

void updateSplashScreen()
{
   if(arduboy.justPressed(A_BUTTON))
   {
      screenMode = 1; 
   }
}

void drawSplashScreen()
{
	arduboy.setCursor(0, 0);
	arduboy.setTextSize(2);
	arduboy.println(F("Pocket"));
	arduboy.println(F("Dungeon"));
	arduboy.setTextSize(1);
	arduboy.println(F("by Andrew Woodbridge"));
	arduboy.println();
	arduboy.println();
	arduboy.println(F("Press A to Continue"));
}

Or, if you’ve already adopted the ‘scoped enumeration’ suggestion, like this:

void loop()
{
	if (!(arduboy.nextFrame()))
		return;

	arduboy.pollButtons();
		
	switch(gameState)
	{
		case GameState::SplashScreen:
			updateSplashScreen();
			break;
		case GameState::Overworld:
			updateMap();
			break;
	}

	arduboy.clear();
		
	switch(gameState)
	{
		case GameState::SplashScreen:
			drawSplashScreen();
			break;
		case GameState::Overworld:
			draw;
			break;
	}
	
	arduboy.display();
}

void updateMap()
{
	if(arduboy.justPressed(A_BUTTON))
	{
		switchActiveMonster();
	}
	
	if(arduboy.justPressed(LEFT_BUTTON))
	{
		activeMonster->monsterMove('l');
	}
	else if(arduboy.justPressed(RIGHT_BUTTON))
	{
		activeMonster->monsterMove('r');
	}
	else if(arduboy.justPressed(UP_BUTTON))
	{
		activeMonster->monsterMove('u');
	}
	else if(arduboy.justPressed(DOWN_BUTTON))
	{
		activeMonster->monsterMove('d');
	}
}

void drawMap()
{
	// Draws the cave walls, checks for movement, then draws the sprites
	drawCave();
	arduboy.drawBitmap(jelly1.posnX, jelly1.posnY, jelly1.pointerToMySprite, 8, 8, WHITE);
	arduboy.drawBitmap(jelly2.posnX, jelly2.posnY, jelly2.pointerToMySprite, 8, 8, WHITE);
	arduboy.drawBitmap(96, 32, jeff.pointerToMySprite, 8, 8, WHITE);
	arduboy.setCursor(0, 56);
	arduboy.print(F("A to switch between Jellies"));
	//arduboy.print(F("j1 hp="));
	//arduboy.print(jelly1.hp);
	//arduboy.print(F("  j2 hp="));
	//arduboy.print(jelly2.hp);
}

void updateSplashScreen()
{
	if(arduboy.justPressed(A_BUTTON))
	{
		gameState = GameState::Overworld; 
	}
}

void drawSplashScreen()
{
	arduboy.setCursor(0, 0);
	arduboy.setTextSize(2);
	arduboy.println(F("Pocket"));
	arduboy.println(F("Dungeon"));
	arduboy.setTextSize(1);
	arduboy.println(F("by Andrew Woodbridge"));
	arduboy.println();
	arduboy.println();
	arduboy.println(F("Press A to Continue"));
}

(Don’t worry about the braces and the spacing, I change those out of habbit because I find ‘Allman style’ easier to read.)

Reorganising your code like this should make it easier to introduce new states because you just have to:

  • Write a new update and draw function
  • Add a new entry into the two switch statements in the loop function

This is what’s technically called a “finite state machine”.
It’s nowhere near as scary as it sounds, it just means that there are several different ‘states’ (or ‘screens’ if you want to think of it that way) that your game can be in, and there’s a finite number of them (as opposed to an infinite number - mathematicians love thinking about ‘inifinity’ for some strange reason).

State machines can be drawn as a state diagram, like so:

Each box is a ‘state’, and each arrow is a ‘transition’.
(Usually they’re circles rather than boxes, but I got lazy. :P)
And that’s basically what the code implements.
It’s a very common tool in game programming.

(Your code was already sort of implementing a finite state machine anyway, but not in the typical way.)


If you want to implement either of these two suggestions and you’re having problems I’m happy to lend a hand and/or answer any questions you might have.

0 Likes

(Andrew) #5

This was really easy to implement, and I see the value.

Getting the map update stuff out of the main loop was on my list, thank you for the suggestion on how to do it.

I’m currently pondering how to do combat, either as Rogue bump into the enemy or a Pokemon type turn based system.

1 Like

(Pharap) #6

Bumping into an enemy would be easiest and use less memory.

Though you could take a hybrid approach like the mystery dungeon series where you do a normal attack with A and have special abilities that you can use.

If you’ve never played one, here’s a video featuring the tutorial stage for Chocobo’s Mystery Dungeon:

There’s also various Pokemon Mystery Dungeon games,
but I struggled to find one without obnoxious commentary.

1 Like