Simon Says Game

The Partridge Family were neither partridges nor a family. Discuss.
User avatar
chili
Site Admin
Posts: 3948
Joined: December 31st, 2011, 4:53 pm
Location: Japan
Contact:

Re: Simon Says Game

Post by chili » July 22nd, 2017, 2:44 pm

Yeah, you can implement a state machine basically with an enumeration value to store current state. General idea is you have states and events. Events trigger actions and/or state transitions. Every event can have its own function. In each event function, you have a switch which branches based on state, and there you do the state-dependent behavior for the event.

A better way to do state machines is with inheritance and virtual functions, but that's a little advanced for now i think.
Chili

albinopapa
Posts: 4373
Joined: February 28th, 2013, 3:23 am
Location: Oklahoma, United States

Re: Simon Says Game

Post by albinopapa » July 22nd, 2017, 11:16 pm

Say you have a character that can stand, walk and run.

Code: Select all

class Keyboard 
{ 
public:
	enum class Key
	{
		LEFT = VK_LEFT, 
		UP = VK_UP, 
		DOWN = VK_DOWN, 
		RIGHT = VK_RIGHT,
		SHIFT = VK_SHIFT
	};
	bool IsPressed( Key K ) 
	{ 
		assert( K == Key::UP || K == Key::LEFT || K == Key::DOWN || K == Key::RIGHT ); 
		const size_t idx = static_cast<size_t>( K );
		return m_keys[ idx ] == K;
	}

private:
	std::array<Key, 4> m_keys;
};
class Vec2
{
public:
	Vec2 &operator+=( const Vec2 &V )
	{
		x += V.x;
		y += V.y;
		return *this;
	}
	Vec2 operator*( const float S )const
	{
		return{ x * S, y * S };
	}
	float x, y;
};
struct Color { int dword; };
class Graphics
{
public:
	void PutPixel( int X, int Y, Color C );
};
class Sprite
{
public:
	void Draw( int OffsetX, int OffsetY, Graphics &Gfx )const
	{
		for( int y = 0; y < m_height; ++y )
		{
			for( int x = 0; x < m_width; ++x )
			{
				const Color c = m_pPixels[ x + ( y * m_width ) ];
				Gfx.PutPixel( x + OffsetX, y + OffsetY, c );
			}
		}
	}

private:
	int m_width = 32, m_height = 32;
	std::unique_ptr<Color[]> m_pPixels;
};
class AnimatedSprite
{
public:
	void AdvanceFrame()
	{
		++m_curHoldFrame;
		if( m_curHoldFrame >= m_holdFrames )
		{
			++m_currentFrame;
			m_currentFrame %= m_maxFrames;
		}
	}
	void Draw( int OffsetX, int OffsetY, Graphics &Gfx )const
	{
		const Sprite &sprite = m_pFrames[ m_currentFrame ];

		sprite.Draw( OffsetX, OffsetY, Gfx );
	}
private:
	std::vector<Sprite> m_pFrames;
	unsigned int m_currentFrame = 0u;
	unsigned int m_curHoldFrame = 0;
	unsigned int m_maxFrames = 16u;
	unsigned int m_holdFrames = 3;
};

class Character
{
public:
	enum State
	{
		Stand, Walk, Run
	};
public:
	void DoInput( Keyboard &Kbd )
	{
		if( Kbd.IsPressed( Keyboard::Key::LEFT ) || Kbd.IsPressed( Keyboard::Key::RIGHT ) )
		{
			if( Kbd.IsPressed( Keyboard::Key::LEFT ) )
			{
				m_direction = { -1.f, 0.f };
			}
			else
			{
				m_direction = { 1.f, 0.f };
			}

			if( Kbd.IsPressed( Keyboard::Key::SHIFT ) )
			{
				
				m_state = State::Run;
			}
			else
			{
				m_state = State::Walk;
			}
		}
		else
		{
			m_state = State::Stand;
		}
	}
	void Update( float DeltaTime )
	{
		switch( m_state )
		{
			case State::Stand:
				m_speed = 0.f;
				m_pCurrentAnim = &m_standAnim;
				break;
			case State::Walk:
				m_speed = m_walkSpeed;
				m_pCurrentAnim = &m_walkAnim;
				break;
			case State::Run:
				m_speed = m_runSpeed;
				m_pCurrentAnim = &m_runAnim;
				break;
		}

		m_pCurrentAnim->AdvanceFrame();
		m_position += ( m_direction * ( m_speed * DeltaTime ) );
	}
	void Draw( Graphics &Gfx )const
	{
		m_pCurrentAnim->Draw( m_position.x, m_position.y, Gfx );
	}

private:
	static constexpr float m_walkSpeed = 100.f;
	static constexpr float m_runSpeed = 200.f;
	State m_state;
	Vec2 m_position, m_direction;
	AnimatedSprite* m_pCurrentAnim, m_standAnim, m_walkAnim, m_runAnim;
	float m_speed = 0.f;
};

int main()
{
	Keyboard kbd;
	Graphics gfx;
	Character character;
	character.DoInput( kbd );
	character.Update( .016f );
	character.Draw( gfx );

	//system( "pause" );
	return 0;
}
Obviously I made this in a console program so it won't run, but it was to give a better idea of what a state machine is using enums and how to use them. Once you learn how to load bitmap images, you could even use this code for sprites and animated sprites...

Next post, state machine using classes.
If you think paging some data from disk into RAM is slow, try paging it into a simian cerebrum over a pair of optical nerves. - gameprogrammingpatterns.com

albinopapa
Posts: 4373
Joined: February 28th, 2013, 3:23 am
Location: Oklahoma, United States

Re: Simon Says Game

Post by albinopapa » July 22nd, 2017, 11:37 pm

Ok, most of the code is the same as before ( all the boiler plate code ) so I'm only posting the differences.

Code: Select all

class Character;
class StateHandler;
class CharacterState
{
public:
	CharacterState( Character &Ch );

	// A virtual function can be overriden by children of this class
	// Making the function = 0 means that the children of this class must
	// override this function or you get compiler errors.
	virtual void DoState() = 0;
protected:
	Character &m_character;
};

class StandState : public CharacterState
{
public:
	// Instead of having to make a new constructor, I'm telling the 
	// compiler that I want to use the parent constructor
	using CharacterState::CharacterState;

	// Since the parent has a pure virtual function ( = 0 ) we must use the
	// override keyword.
	void DoState()override
	{
		m_character.m_speed = 0.f;
		m_character.m_pCurrentAnim = &m_character.m_standAnim;
	}

};

class WalkState : public CharacterState
{
	using CharacterState::CharacterState;
	void DoState()override
	{
		m_character.m_speed = m_character.m_walkSpeed;
		m_character.m_pCurrentAnim = &m_character.m_walkAnim;
	}
};

class RunState : public CharacterState
{
	using CharacterState::CharacterState;
	void DoState()override
	{
		m_character.m_speed = m_character.m_runSpeed;
		m_character.m_pCurrentAnim = &m_character.m_runAnim;
	}
};

class StateHandler
{
public:
	void DoInput( Keyboard &Kbd, Character &Ch )
	{
		if( Kbd.IsPressed( Keyboard::Key::LEFT ) || Kbd.IsPressed( Keyboard::Key::RIGHT ) )
		{
			if( Kbd.IsPressed( Keyboard::Key::SHIFT ) )
			{
				m_pCurrentState = std::make_unique<RunState>( Ch );
			}
			else
			{
				m_pCurrentState = std::make_unique<WalkState>( Ch );
			}
		}
		else
		{
			m_pCurrentState = std::make_unique<StandState>( Ch );
		}
	}
	CharacterState &GetState()
	{
		return *m_pCurrentState;
	}
private:
	std::unique_ptr<CharacterState> m_pCurrentState;
};

class Character
{
	friend class RunState;
	friend class WalkState;
	friend class StandState;

public:
	void DoInput( Keyboard &Kbd )
	{
		if( Kbd.IsPressed( Keyboard::Key::LEFT ) )
		{
			m_direction = { -1.f, 0.f };
		}
		else if( Kbd.IsPressed( Keyboard::Key::RIGHT ) )
		{
			m_direction = { 1.f, 0.f };
		}

		m_handler.DoInput( Kbd, *this );
	}
	void Update( float DeltaTime )
	{
		m_handler.GetState().DoState();
		m_pCurrentAnim->AdvanceFrame();
		m_position += ( m_direction * ( m_speed * DeltaTime ) );
	}
	void Draw( Graphics &Gfx )const
	{
		m_pCurrentAnim->Draw( m_position.x, m_position.y, Gfx );
	}

private:
	static constexpr float m_walkSpeed = 100.f;
	static constexpr float m_runSpeed = 200.f;
	StateHandler m_handler;
	Vec2 m_position, m_direction;
	AnimatedSprite* m_pCurrentAnim, m_standAnim, m_walkAnim, m_runAnim;
	float m_speed = 0.f;
};
If you think paging some data from disk into RAM is slow, try paging it into a simian cerebrum over a pair of optical nerves. - gameprogrammingpatterns.com

albinopapa
Posts: 4373
Joined: February 28th, 2013, 3:23 am
Location: Oklahoma, United States

Re: Simon Says Game

Post by albinopapa » July 22nd, 2017, 11:43 pm

There are different ways to handle states and transitioning those states, but I this was the fastest and easiest way I thought of. I'd normally want to separate the interface and implementation into header and source files, but it should be easier this way to browse the code.

Friending a class is sometimes considered taboo because it allows for one class to alter the state of another so you lose encapsulation. In this scenario, I thought it appropriate since that's exactly what I wanted, for a STATE class to change the STATE of the character class. For instance, the StateHandler can't access the private members of Character because it wasn't friended.

If you watch the old Intermediate series around episode 9 or 10, chili goes over polymorphism and starts getting into using it for states on a 2D platformer. You might like his implementation as well, it's pretty similar, but very different lol.
If you think paging some data from disk into RAM is slow, try paging it into a simian cerebrum over a pair of optical nerves. - gameprogrammingpatterns.com

User avatar
Zedtho
Posts: 189
Joined: February 14th, 2017, 7:32 pm

Re: Simon Says Game

Post by Zedtho » July 23rd, 2017, 6:04 pm

Thanks a lot for the super-detailed explanation! This will definitely be useful in future projects :O

While reading I noticed a few things:
Not very related to this, but you seem to have named everything with m_.
Just wondering if that's a convention (that I should also do).
Now I also understand why enums are important. Because they're "disguised" integers, switch cases can use them.
Again, thanks a lot for that, this will be very useful for future stuff.

albinopapa
Posts: 4373
Joined: February 28th, 2013, 3:23 am
Location: Oklahoma, United States

Re: Simon Says Game

Post by albinopapa » July 23rd, 2017, 7:10 pm

I've tried different naming conventions and the m_ is something that has just stuck. It means it's a member of a class or struct. This helps me avoid naming collisions so I can reuse the actual name in a function somewhere and not have to use: this->numApples = numApples;

I've also seen some just use _numApples, but I've also heard this is frowned upon as prefixing a variable name with _ is reserved for language or library implementations. Look through one of the C++ headers and you'll see that a lot of functions like:

Code: Select all

		iterator insert(const_iterator _Where, size_type _Count,
		const _Ty& _Val)
		{	// insert _Count * _Val at _Where
		return (_Insert_n(_Where, _Count, _Val));
		}
You have the std::vector::insert function that the end user is suppose to use, and the std::vector::_Insert_n function that the library uses as a helper function.

You should look up Hungarian notation. It will cover some naming conventions that will help read others code, specifically the code in the MSDN website. Microsoft uses it all throughout their api documentation.

For instance:
HRESULT hResult is a handle to a result or HWND hWnd is a handle to a window.
LPSTR szWinTitle is a null-terminated c-string ( LPSTR stands for Long Pointer STRing; sz is Zero terminated String ).

In chili's videos when he wants to use a pointer, he usually prefixes with a lower case p: pMyInts. This helps to know at a glance if you need to use a -> or a . to dereference an object.

Code: Select all

struct MyStuff{ int x, int y; };
MyStuff stuff;
MyStuff *pStuff = &stuff;

stuff.x = 42;
pStuff->y = stuff.x + 27;
Use it, don't use it...it's all a matter of taste. I only use the m_ to avoid naming collisions and var name reuse and it does help sometimes to let me know which vars are members and which are locals. I also use g_ when I declare global variables ( variables declared outside of a class or function ).

main.cpp

Code: Select all

int g_age = 30;

int main( int ArgC, char *pArgV[] )
{
     int age = g_age;
     return 0;
}
If you think paging some data from disk into RAM is slow, try paging it into a simian cerebrum over a pair of optical nerves. - gameprogrammingpatterns.com

albinopapa
Posts: 4373
Joined: February 28th, 2013, 3:23 am
Location: Oklahoma, United States

Re: Simon Says Game

Post by albinopapa » July 23rd, 2017, 7:30 pm

As for enums, don't just think of them as disguised ints really. The default underlying implementation of them are the same as constexpr unsigned int, but they are really meant to help make a list of distinguishable types or traits. Take for instance the C++11 enum class.

Code: Select all

enum EnumColors
{
     Red, Green, Blue
};

enum class EnumClassColors
{
     Red, Green, Blue
};

// With the old enum, you can assign to int variables without casting
int myage = Red;  // Doesn't make sense does it?

// With the new enum class, you cannot directly assign to int variables
int myage = EnumClassColors::Red; // error cannot assign to int
Even though EnumClassColors::Red has the same underlying value as EnumColors::Red, it's still not an int.

If I'm going to be tempted to use the enum as an int at some point, I'll prefer to use constexpr int inside a namespace.

Code: Select all

namespace EnumInts
{
     constexpr int First = 0;
     constexpr int Second = First + 1;
     constexpr int Count = Second + 1;
}
Now say I wanted to use them

Code: Select all

int arrMyInts[ EnumInts::Count ];
arrMyInts[ EnumInts::First ] = 42;
arrMyInts[ EnumInts::Second ] = 69;
There are some benefits to using the old enum as ints though. One being it does the counting for you, each enumeration is incremented by one, so if you add a new one later, count in this example would be increased automatically. Another benefit to using the old enum as ints would be the ability to use them in a for loop.

Code: Select all

enum EnumInts
{
     First, Second, Count
};

int arrMyInts[ Count ];
for(auto i = First; i < Count; ++i)
{
     arrMyInts[ i ] = i * 420;
}
If you think paging some data from disk into RAM is slow, try paging it into a simian cerebrum over a pair of optical nerves. - gameprogrammingpatterns.com

User avatar
Zedtho
Posts: 189
Joined: February 14th, 2017, 7:32 pm

Re: Simon Says Game

Post by Zedtho » July 27th, 2017, 10:05 am

Thanks a lot for this! Enums make sense now. Will definitely use these in my next project!

Edit:
I should add that I haven't been adding anything to this game yet. Currently I'm working on something else that requires states (well, it doesn't require them, but it'd be a complete mess without it). It's gonna be a simple PVE game (Player vs Entity).

Post Reply