Upload
francis-serina
View
49
Download
1
Embed Size (px)
DESCRIPTION
Building a simple C++ Game Engine using SDL1.2 GD4N stands for Game Development for Noobs Used by UP ITTC (2011-2012)
Citation preview
GD4N C++ wrapper for SDL
Purpose
Ê Object-‐Oriented Programming approach on video game development
Ê Automatic Resource Management
Ê GD4N means Game Development for Noobs J
Set Up
Ê Create a solution (or workspace) called GD4N_Games. This is where we’ll put our GD4N and the games that depend on it.
Ê Create a project in GD4N_Games, called GD4N, that builds a library (either static or dynamic).
Ê Link the following libraries to GD4N: Ê SDL, SDL_image, SDL_mixer, SDL_ttf, OpenGL
Ê Add the corresponding header & library locations
Ê SDL_image and SDL_mixer may require additional libraries, add those accordingly
Ê Build the library which will be linked by succeeding projects
Test
Ê Create an empty project in GD4N_Games called GD4N_Test Ê Windows: Win32 Application
Ê Mac OSX: Cocoa Application
Ê Link the following libraries: Ê GD4N, SDL, OpenGL
Ê Add the header locations: Ê GD4N Project folder, all SDL headers
Test
Ê Copy the files from src/GD4N_Test to your project directory
Ê Load the files into your project
Ê Build and Run
Ê You should see a black 800x600 screen that can only be closed by pressing the close button
Ê If successful, create a template based on GD4N_Test
Design Paradigms Optional Topic
Design Paradigms
Ê GD4N uses a list of design paradigms to perform resource management and implement the managers to be as separate as possible
Ê It is not necessary to fully understand these since the point of a framework is to remove the necessity to know these things but knowing these will help you see the game engine pieces clearly
Singleton Design Pattern
Ê Limit the instance of a class to only one, hence SINGLEton
Ê The Singleton is created upon first use (lazy initialization) and is destroyed when the program ends
Ê Usually used to represent Managers and Devices
(Simplified) Object Pool
Ê A dynamic container that stores objects
Ê Adding and Removing instances are facilitated by the pool
Ê Note: Ideally, an object pool prevents the overhead of new and delete operators by re-‐initializing and shutting down instances. The client simply requests for an instance and the object pool gives a pre-‐allocated resource (if available).
Template Method Pattern
Ê Define a set of methods to be overridden by its children
Ê The parent class is intended to be abstract
Ê Not all the methods need to be overridden
GD4N Resources
constants.h
Ê Each project should have a constants.h which will contain all the unique IDs pertaining to the different resources. Each resource to be used should have a corresponding ID.
CSurface
Ê Wraps around SDL_Surface
Ê To load an image Ê CSurface::Load(imagepath, id) where imagepath is the path to
the image and id is a unique identifier
Ê Drawing the image is in the SVideoManager
Ê See the CSurface.h for more information
CSurfaceSheet
Ê A Surface Sheet (or Sprite Sheet) is an image composed of equal-‐sized frames.
Ê In this example, we have an image composed of a snake head looking at 4 different directions.
Ê The image is 40x40 pixels with 2 rows and 2 cols, making each frame 20x20 pixels. The maximum number of frames is rows x cols, which is 4 in this case.
Ê See CSurfaceSheet.h for more information
CSurfaceSheetAnimated
Ê This is the animated sprite sheet. Inheriting from CSurfaceSheet, this class animates by going to the next frame automatically.
Ê Before animating, it is important to set the sprite dimensions as well as the animation speed (frames per second).
Ê See CSurfaceSheetAnimated.h for more information
CFont
Ê Wraps around TTF_Font (SDL_ttf)
Ê To load a font Ê CFont::Load(fontpath, id) where fontpath is the path to the true
type font and id is a unique identifier
Ê To use the font (create a surface with text) Ê CSurface::CreateText(fontId, text, textcolor, id) where fontId is
the id of the font to be used and id is the unique identifier to the generated surface.
Ê See CFont.h for more options
CMusic (background music)
Ê Wraps around Mix_Music (SDL_mixer)
Ê To load music Ê CMusic::Load(musicpath, id) where musicpath is the path to the
audio file and id is a unique identifier
Ê Playing music is handled by the SAudioManager
Ê See CMusic.h for more information
CChunk (sound effects)
Ê Wraps around Mix_Chunk (SDL_mixer)
Ê To load a sound effect Ê CChunk::Load(sfxpath, id) where sfxpath is the path to the
sound effect and id is the unique identifier
Ê Player sound effects is handled by the SAudioManager
Ê See CChunk.h for more information
GD4N Managers
SInputManager
Ê All the input states and events are stored and updated through this manager
Ê This manager handles multiple devices such as mouse and keyboard
Ê To access, use sInput
SVideoManager
Ê Everything seen on the screen is handle by this manager
Ê This manager represents the video device
Ê To access, use sVideo
SAudioManager
Ê Playing, pausing, and stopping sound effects and music tracks are handled by this manager
Ê Multiple sound effects can be played simultaneously
Ê Only 1 music track can be played at a time
Ê To access, use sAudio
STimeManager
Ê This manager keeps track of the time since the game has started and the number of seconds between frames
Ê To access, use sTime
Game Manager
CGameManager
Ê The umbrella class that represents the game itself
Ê Special type of Manager because it needs to be inherited
Ê Initializes and Shuts down all other modules (managers)
Ê Handles the changing of scenes, loading of resources, management of game objects
Ê Each game should have a class that inherits from CGameManager and overrides Init()
CGameManager Inherited Properties
Ê char *windowTitle; Ê Defines the title of the window
Ê Must be changed before calling Init();
Ê char *icon; Ê Defines the path to the icon of the window
Ê Must be changed before calling Init();
CGameManager Overrideable Methods
Ê bool Init(); Ê By default, this initializes SDL and GD4N (must be called in derived
class) Ê Register scenes Ê Set SDL and GD4N settings Ê Determine initial scene
Ê void CleanUp(); Ê By default, this releases and shuts down SDL Ê Not normally overridden
Ê void LoadResources(); Ê Loading of assets should occur here
CGameManager Utility Methods
Ê void StopPlaying(); Ê Public method to shut down the game
Ê void ChangeScene(int scene); Ê Change scenes
Ê void AddScene(SceneInit newScene); Ê Register a scene to be used by ChangeScene()
CGameManager Header
Ê There are more methods at your disposal
Ê See CGameManager.h for more information
Game Object
Game Object
Ê Everything that either has logic or is visible is a game object.
Ê CGameObject is the template class for all game objects.
Ê Each game object should inherit from the CGameObject class. The game objects should override the methods which are called by the game loop.
CGameObject Inherited Properties
Ê int id; Ê A unique identifier generated when added to the pool (handled by the
constructor) Ê Has a getter
Ê int type; Ê A type that is game-‐specific used for collisions (assigned during
constructor). The different types are located in constants.h Ê Has a getter
Ê bool isVisible; Ê A flag to determine if the object should be draw or not Ê Has a getter and setter
Ê bool isActive; Ê A flag to determine if the object should perform logic or not Ê Has a getter and setter
CGameObject Overrideable Methods
Ê void Draw(); Ê Contains the draw calls
Ê void DrawGUI(); Ê Contains the drawing of GUI calls
Ê void Update(); Ê Performs logic
Ê void CollidesWith(CGameObject *other); Ê Performs logic upon collision
Ê bool IsCollidingWith(CGameObject *other); Ê Checks for collision against another game object
CGameObject Utility Methods
Ê bool IsVisible();
Ê bool IsActive();
Ê int GetType();
Ê int GetID();
Ê void SetVisible();
Ê void SetActive();
SnakeGD4N So it has begun
Game Specifications
Ê The game can be played by 1-‐2 players
Ê Controls Ê First player: arrow keys
Ê Second player: WASD
Ê Eating the apple would increase the consuming snake’s length by 1
Ê Hitting the grass (boundary), another snake or yourself would be your doom
Getting Started
Ê Create a project, SnakeGD4N, based on GD4N_Test Ê Double check the framework, libraries and header paths!
Ê Rename CTestGameManager to CSnakeGameManager Ê In the h, cpp and main.cpp
Ê Don’t forget the macro sGameManager!
Ê Copy the assets in SnakeGD4N-‐00 to your working directory Ê bkgd Ê images
Ê sfx
Resources
Ê Create constants.h defining the following enumerations
Ê Include constants.h in CSnakeGameManager.cpp
enum SFX_IDS { SFXID_EAT,
}; enum MUSIC_IDS {
MUSICID_01, }; enum SURFACE_IDS {
SURFID_SNAKEHEAD1 = 0, SURFID_SNAKEHEAD2, SURFID_SNAKEBODY1, SURFID_SNAKEBODY2, SURFID_FOOD, SURFID_GROUND, SURFID_TITLE, SURFID_TITLEOPTION1, SURFID_TITLEOPTION2, SURFID_TITLEOPTION3, SURFID_SELOPTION, SURFID_INGAMEMENU,
};
Resources
Ê Include the following header files into CSnakeGameManager.cpp Ê CChunk.h Ê CMusic.h Ê CSurface.h
Ê Use the namespace GD4N globally
Ê In CSnakeGameManager, override LoadResources() with the following CChunk::Load((char *)"sfx/eat.wav", SFXID_EAT); sChunkPool->CleanUp();
CMusic::Load((char *)"01-Molly.mp3", MUSICID_01); sMusicPool->CleanUp();
Resources
Ê LoadResources() continued
CSurface::Load((char *)"images/head1.png", SURFID_SNAKEHEAD1); CSurface::Load((char *)"images/head2.png", SURFID_SNAKEHEAD2); CSurface::Load((char *)"images/body1.png", SURFID_SNAKEBODY1); CSurface::Load((char *)"images/bkgd.jpg", SURFID_GROUND); CSurface::Load((char *)"images/food.png", SURFID_FOOD); CSurface::Load((char *)"images/titlescreen.png", SURFID_TITLE); CSurface::Load((char *)"images/option01.png", SURFID_TITLEOPTION1); CSurface::Load((char *)"images/option02.png", SURFID_TITLEOPTION2); CSurface::Load((char *)"images/option03.png", SURFID_TITLEOPTION3); CSurface::Load((char *)"images/option.png", SURFID_SELOPTION); CSurface::Load((char *)"images/ingamemenu.png", SURFID_INGAMEMENU); sSurfacePool->CleanUp();
Initialization
Ê Include the following header files Ê SVideoManager.h Ê SAudioManager.h
Ê In the constructor, we set the window title. This can actually be placed inside the Init(). windowTitle = (char *)"Snake!";
Ê Override the Init() and set the video mode to 440x400x32 with flags SDL_HWSURFACE | SDL_DOUBLEBUF. This should be called before the parent’s Init() otherwise, this will have no effect.
sVideo->SetVideoMode(440, 440, 32, SDL_HWSURFACE | SDL_DOUBLEBUF);
Ê Call the parent’s Init() to initialize SDL and GD4N if (!CGameManager::Init()) return false;
Ê When Init() goes well, return true return true;
Test!
Ê Build and run the program
Ê You should see a black 440x440 window that does nothing
Ê It is a good practice to test after every few lines of code. Believe me, the benefits will go a long way.
Ê See src/SnakeGD4N-‐01 for any clarifications
The (Back)Ground The first game object and the first scene
Creating the Ground
Ê Let’s create our first Game Object! Let’s call it CGround.
Ê First, we create the header file called CGround.h #include "CGameObject.h" class CGround : public GD4N::CGameObject { protected: void Draw(); public: CGround(); ~CGround(); };
Creating the Ground
Ê Then the source code, CGround.cpp #include "CGround.h" #include "SVideoManager.h" #include "constants.h" CGround::CGround() : CGameObject() { } CGround::~CGround() { } void CGround::Draw() { sVideo->Draw(SURFID_GROUND); } Ê Question: What does the ground do?
Creating Scenes
Ê Now we have created a game object, we need to put it in a scene in the CSnakeGameManager.
Ê Create a method in CSnakeGameManager void CSnakeGameManager::Scene00() { new CGround(); }
Ê Then we register that scene and load it in Init() AddScene(CSnakeGameManager::Scene00); ChangeScene(0);
Test!
Ê Build and run the program.
Ê You should see the ground displayed on the screen.
Ê The Game Loop inside CGameManager automatically calls the Draw() method of all the visible game objects in the scene.
Ê See src/SnakeGD4N-‐02 for any clarifications
The Snake Head Reacting to Input, Using the Time, Sprite Sheets
Creating the Snake Head
Ê Our CSnake game object will react to the arrow keys, moving the snake head accordingly
CSnake.h
#include "CGameObject.h" #include "TVector2.h" #include "constants.h" class CSnake : public GD4N::CGameObject { protected: void Update(); void Draw(); GD4N::TVector2<int> position; public: CSnake(); ~CSnake(); };
CSnake Constructor and Deconstructor
#include "SInputManager.h" #include "SVideoManager.h" #include "STimeManager.h" #include "CSnake.h" CSnake::CSnake() : CGameObject() { position.x = 1; position.y = 1; } CSnake::~CSnake() { }
CSnake Update and Draw
void CSnake::Update() { if (sInput->GetKeyDown(SDLK_RIGHT)) { position.x++; } else if (sInput->GetKeyDown(SDLK_LEFT)) { position.x--; } else if (sInput->GetKeyDown(SDLK_DOWN)) { position.y++; } else if (sInput->GetKeyDown(SDLK_UP)) { position.y--; } } void CSnake::Draw() { sVideo->Draw(SURFID_SNAKEHEAD1, position * 20); }
Instantiate CSnake
Ê Create an instance of CSnake in Scene00 after CGround
Ê Build and run the program and you will see the head1.png displayed on the screen
Ê Moving the arrow keys would move the snake head accordingly
Explaining the Position
Ê The position is represented using 2 integers, x and y. Note that our snake head is actually an image with width and height equal to 20
Ê When the snake moves to the right, it’s actually moving 20 pixels to the right though it is only incremented by 1 in the Update()
Ê The factor of 20 is applied in the Draw() method. Meaning, our play area (or screen dimensions) is 440x440 but the position values will only range from 0 to 22.
Problems
Ê Things you may have noticed 1. The snake should be moving continuously, frame-‐independently 2. head1.png contains four snake heads! 3. No boundary conditions!
Ê We fix the first problem by adding 2 variables for the movement relative to time and 2 variables for the direction (current and next)
Ê For the second problem, we use a Surface Sheet to represent the snake head image in order to draw the corresponding head depending on the direction
Ê We will consider boundary conditions later on when we go to collisions
Direction Type
Ê In constants.h, add the following enum enum direction_t {
DIR_UP = 0, DIR_DOWN, DIR_LEFT, DIR_RIGHT
};
More Snake Properties and Methods
Ê In CSnake.h, add the following include
#include "CSurfaceSheet.h"
Ê And add the following protected properties and methods void ReactToInput(); void Move();
direction_t dir; direction_t newDir; float timeLast; // time since last movement float timeBetween; // time between movements GD4N::CSurfaceSheet *headTexture;
CSnake() and ~CSnake()
Ê If you looked at head1.png, you will notice that it’s a 2x2 sprite sheet where the first frame (upper left) is facing left and the second (upper right) is facing right. The third frame (lower left) is for up and the last frame is for down.
CSnake::CSnake() : CGameObject() { position.x = 1; position.y = 1; newDir = dir = DIR_RIGHT; timeBetween = 0.1f; timeLast = -timeBetween; headTexture = new GD4N::CSurfaceSheet(SURFID_SNAKEHEAD1); headTexture->SetSpriteDimensions(2, 2); headTexture->SetCurrentFrame(1); } CSnake::~CSnake() { delete headTexture; headTexture= 0; }
CSnake::Update()
Ê Update is growing very big and we need to modularize it into 2 smaller functions, ReactToInput() and Move();
Ê The snake will only move when timeBetween seconds have lapsed void CSnake::Update() { ReactToInput(); if (timeLast + timeBetween < sTime->GetTime()) { timeLast = sTime->GetTime(); dir = newDir; Move(); } }
CSnake::ReactToInput() and Draw()
Ê As a rule in Snake, if you’re moving to the right, you can’t make a 180 degree turn and face left. You can only change your direction to up or down.
Ê dir is the current direction while newDir is the next direction
void CSnake::ReactToInput() { if (sInput->GetKeyDown(SDLK_RIGHT) && dir != DIR_LEFT) { newDir = DIR_RIGHT; } else if (sInput->GetKeyDown(SDLK_LEFT) && dir != DIR_RIGHT) { newDir = DIR_LEFT; } else if (sInput->GetKeyDown(SDLK_DOWN) && dir != DIR_UP) { newDir = DIR_DOWN; } else if (sInput->GetKeyDown(SDLK_UP) && dir != DIR_DOWN) { newDir = DIR_UP; } } void CSnake::Draw() { sVideo->Draw(headTexture, position * 20); }
CSnake::Move()
Ê When the snake moves, it changes its position and the frame to display void CSnake::Move() { switch (dir) { case DIR_UP: position.y--; surface->SetCurrentFrame(2); break; case DIR_DOWN: position.y++; surface->SetCurrentFrame(3); break; case DIR_LEFT: position.x--; surface->SetCurrentFrame(0); break; case DIR_RIGHT: position.x++; surface->SetCurrentFrame(1); break; } }
Test!
Ê Build and run the program!
Ê You will see the snake head moving in its current direction unless it’s changed by pressing the arrow keys
Ê See src/SnakeGD4N-‐03 for any clarifications
Snake Body Game Objects controlling other Game Objects
Body Movement
Ê The body of the snake is composed of segments
Ê Each segment follows the one before it
Ê The “neck” is the segment right before the head
Ê The “tail” is the last segment of the body
CSnakeSegment.h
#include "CGameObject.h" #include "CSurfaceSheet.h" #include "TVector2.h" #include "constants.h" class CSnakeSegment : public GD4N::CGameObject { protected: void Draw(); GD4N::CSurfaceSheet *bodyTexture; public: CSnakeSegment(); ~CSnakeSegment(); GD4N::TVector2<int> position; };
CSnakeSegment.cpp
CSnakeSegment::CSnakeSegment() : CGameObject() { bodyTexture = new GD4N::CSurfaceSheet(SURFID_SNAKEBODY1); bodyTexture->SetSpriteDimensions(2, 5); bodyTexture->SetCurrentFrame(0); } CSnakeSegment::~CSnakeSegment() { delete bodyTexture; bodyTexture = 0; } void CSnakeSegment::Draw() { sVideo->Draw(bodyTexture, position * 20); } Ê The CSnakeSegment does not have logic. Notice that it’s only concerned with drawing
itself. Its position property is public, however. Which means, another game object could simply change the segment’s position and it will be drawn unto to that new position.
CSnake.h
Ê CSnake will handle the segments. Add the following includes and protected properties
#include "CSnakeSegment.h" #include <vector> int length; int desiredLength; std::vector<CSnakeSegment*> body;
CSnake.cpp
Ê Set the initial value of length and desiredLength to 0 in the constructor.
length = desiredLength = 0;
Ê For testing purposes, we increase the length of the snake by pressing the space bar. Add the following lines in ReactToInput()
if (sInput->GetKeyDown(SDLK_SPACE))
desiredLength++;
CSnake.cpp
Ê In the Move(), add the following before the switch statement if (length < desiredLength) { length++; body.push_back(new CSnakeSegment()); } if (length > 0) { for (int i = length-1; i > 0; i--) { body[i]->position = body[i-1]->position; // all segments will copy the one // before it except for the neck } body[0]->position = position; // neck will copy its position from the head }
Test!
Ê Build and run the program!
Ê Press the space bar to increase the length of the snake
Ê See src/SnakeGD4N-‐04 for any clarifications
Body as Sprite Sheet
Ê Notice that body1.png is a sprite sheet with 5 cols and 2 rows
Ê Currently, we’re only displaying the first frame (frame 0)
Ê To display the proper frame from our sprite sheet, we need to determine if the segment is the tail or not.
Ê If it’s the tail, we only care what is the direction of the previous segment
Ê If it’s not the tail, determine if the direction of the previous segment is the same as the current
Tail and Same Directions
Tail
Next Direction Frame
DIR_UP 2
DIR_DOWN 7
DIR_LEFT 6
DIR_RIGHT 5
Same Direction
Direction Frame
DIR_UP or DIR_DOWN 1
DIR_LEFT or DIR_RIGHT 0
Note: Frame indices start at 0
Different Directions (turning)
Ê If the direction of the previous segment is different from the direction of the current segment, it must be turning
Current Direction Next Direction Frame
DIR_UP DIR_RIGHT 3
DIR_LEFT DIR_DOWN
DIR_RIGHT DIR_DOWN 4
DIR_UP DIR_LEFT
DIR_DOWN DIR_RIGHT 8
DIR_LEFT DIR_UP
DIR_RIGHT DIR_UP 9
DIR_DOWN DIR_LEFT
Note: Frame indices start at 0
CSnakeSegment
Ê Add the following protected property and public methods in the header
direction_t dir; direction_t GetDirection() { return dir; } void SetDirection(direction_t newDir, bool isTail = false);
Ê Implement SetDirection() given the tables in the previous slide. Remember to assign direction on the given newDir.
CSnake.cpp
Ê In Move(); a slight modification is applied in order to set the direction of the segments (aside from the position)
if (length > 0) { for (int i = length-1; i > 0; i--) { // all segments will copy from the one before it except for the neck body[i]->SetDirection(body[i-1]->GetDirection(), (i == length-1)); body[i]->position = body[i-1]->position; } // neck will copy its direction and position from the head body[0]->SetDirection(dir, (length == 1)); body[0]->position = position; }
Test!
Ê Once you’re done implementing SetDirection(), build and run your program
Ê Ideally, the snake’s body would be more realistic J
Ê See src/SnakeGD4N-‐05 for the solution
Food Point Collision
Food
Ê The food is a simple game object in the sense that it doesn’t perform any logic until something collides with it. Upon collision, the food simply randomizes its position.
Ê Our food will use an animated sprite sheet that has an oscillating animation. Meaning, it animates from frame 0 to 3 then back to 0 and repeats, given that the animation only has 4 frames.
Collisions
Ê To work with collisions, we first need to set the different game object types
Ê Add the following enum in constants.h enum GAMEOBJECT_TYPES { TYPE_FOOD = 1, TYPE_SNAKE, TYPE_SNAKESEGMENT, TYPE_GROUND, };
Type
Ê Set the type of each game object in their constructor
Ê This will affect CSnake.cpp, CSnakeSegment.cpp and CGround.cpp
Ê If this step is skipped, collisions will not be detected
CFood.h
Ê Create CFood.h #include "CGameObject.h" #include "CSurfaceSheetAnimated.h" #include "TVector2.h" class CFood : public GD4N::CGameObject { protected: void Update(); void Draw(); bool IsCollidingWith(GD4N::CGameObject* other); void CollidesWith(GD4N::CGameObject* other); GD4N::CSurfaceSheetAnimated* foodTexture; GD4N::TVector2<int> position; void RandomizePosition(); public: CFood(); ~CFood(); const GD4N::TVector2<int> & GetPosition() { return position; } };
CFood.cpp
Ê The food will use an animated sprite sheet. #include "CFood.h" #include "SVideoManager.h" #include "SRandom.h" #include "constants.h" #include "CSnake.h" #include "CSnakeSegment.h" #include "CGround.h” CFood::CFood() : CGameObject() { foodTexture = new GD4N::CSurfaceSheetAnimated(SURFID_FOOD); foodTexture->SetSpriteDimensions(2, 2); foodTexture->SetAnimationSpeed(10); foodTexture->SetOscillating(true); RandomizePosition(); type = TYPE_FOOD; }
CFood.cpp
CFood::~CFood() { delete foodTexture; foodTexture = 0; } void CFood::Update() { foodTexture->Update(); } void CFood::Draw() { sVideo->Draw(foodTexture, position * 20); } void CFood::RandomizePosition() { position.x = sRand->Generate(1, 21); position.y = sRand->Generate(1, 21); }
CFood Collision Testing
bool CFood::IsCollidingWith(GD4N::CGameObject* other) { switch (other->GetType()) { case TYPE_SNAKE: { CSnake* snake = dynamic_cast<CSnake*>(other); return (position == snake->GetPosition()); } case TYPE_FOOD: { CFood* food = dynamic_cast<CFood*>(other); return (position == food->GetPosition()); } case TYPE_SNAKESEGMENT: { CSnakeSegment* segment = dynamic_cast<CSnakeSegment*>(other); return (position == segment->position); } case TYPE_GROUND: { CGround* ground = dynamic_cast<CGround*>(other); return (position.x < 1 || position.x >= ground->GetWidth() - 1 || position.y < 1 || position.y >= ground->GetHeight() - 1); } } return CGameObject::IsCollidingWith(other); }
CFood Collision Reaction
void CFood::CollidesWith(GD4N::CGameObject* other) { switch (other->GetType()) { case TYPE_SNAKESEGMENT: case TYPE_FOOD: case TYPE_GROUND: // Respawn! do { RandomizePosition(); } while (IsCollidingWith(other)); break; case TYPE_SNAKE: // Snake ate this food! RandomizePosition(); break; } }
CGround GetWidth() and GetHeight()
Ê Notice that CFood::IsCollidingWith() requires GetWidth() and GetHeight() from CGround. Simply add these 2 methods in CGround that returns the width and height respectively.
Test!
Ê Build and run the program.
Ê The food should be randomly generated in the area.
Ê When the snake head eats (collides with) the food, the food will be placed in a different random position.
Ê See src/SnakeGD4N-‐06 for any clarifications
Snake Collisions
Snake Collisions
Ê The snake only has 2 collision reactions; death and grow
Ê When a snake consumes (collides) with the food, the desired length of the snake increments by 1. Thus, we no longer need the quick fix of pressing the space bar to increase the length.
Ê When a snake collides with anything else (another snake head, a snake body regardless of who it belongs to, the ground), the snake will die.
Ê We simply need to override the IsCollidingWith() and CollidesWith() methods from CGameObject.
CSnake Collision Testing
bool CSnake::IsCollidingWith(GD4N::CGameObject* other) { switch (other->GetType()) { case TYPE_SNAKE: { CSnake* snake = dynamic_cast<CSnake*>(other); return (position == snake->GetPosition()); } case TYPE_FOOD: { CFood* food = dynamic_cast<CFood*>(other); return (position == food->GetPosition()); } case TYPE_SNAKESEGMENT: { CSnakeSegment* segment = dynamic_cast<CSnakeSegment*>(other); return (position == segment->position); } case TYPE_GROUND: { CGround* ground = dynamic_cast<CGround*>(other); return (position.x < 1 || position.x >= ground->GetWidth() - 1 || position.y < 1 || position.y >= ground->GetHeight() - 1); } } return CGameObject::IsCollidingWith(other); }
CSnake Collision Reaction
Ê Note: SAudioManager.h should be included void CSnake::CollidesWith(GD4N::CGameObject* other) { switch (other->GetType()) { case TYPE_FOOD: desiredLength++; sAudio->PlaySound(SFXID_EAT); break; default: // die isActive = false; break; } }
Test!
Ê Build and run your code.
Ê Test if all the collision reactions are correct.
Ê Note that the quick fix of pressing space to increase the snake’s length should be removed.
Ê See src/SnakeGD4N-‐07 for any clarifications
Title Screen Changing Scenes
Snake
Ê Now that we have the core gameplay working, we will polish the game.
Ê We first add a Title Screen where the player could select between single player and 2 players. Also, the Title Screen will give the player an option to exit the game.
CTitle
Ê The Title Screen is actually another Game Object. This is true for most games – the GUI is a Game Object.
Ê The main difference between this Game Object will be the overriding of DrawGUI(). This method is where we perform all GUI-‐related functionality.
CTitle.h
#include "CGameObject.h" class CTitle : public GD4N::CGameObject { protected:
int selected;
public: CTitle(); void Update(); void DrawGUI();
};
CTitle Constructor and Includes
#include "CTitle.h” #include "CSnakeGameManager.h” #include "SVideoManager.h” #include "SInputManager.h” #include "constants.h” #include "TVector2.h” CTitle::CTitle() : CGameObject() { selected = 0; }
CTitle Update
void CTitle::Update() { if (sInput->GetKey(SDLK_ESCAPE)) { sGameManager->StopPlaying(); // quit game } if (sInput->GetKeyDown(SDLK_DOWN)) { selected++; } else if (sInput->GetKeyDown(SDLK_UP)) { selected--; } selected = (selected + 3) % 3; if (sInput->GetKeyDown(SDLK_RETURN)) { switch (selected) { case 0: sGameManager->ChangeScene(1); break; case 1: sGameManager->ChangeScene(2); break; case 2: sGameManager->StopPlaying(); break; } } }
CTitle DrawGUI
Ê The DrawGUI() is a bit more complicated than the Draw() methods because we’re handling different images at once.
void CTitle::DrawGUI() { using GD4N::TVector2; sVideo->Draw(SURFID_TITLE, TVector2<int>(73, 50)); sVideo->Draw(SURFID_TITLEOPTION1, TVector2<int>(170, 200)); sVideo->Draw(SURFID_TITLEOPTION2, TVector2<int>(170, 250)); sVideo->Draw(SURFID_TITLEOPTION3, TVector2<int>(170, 300)); switch (selected) { case 0: sVideo->Draw(SURFID_SELOPTION, TVector2<int>(140, 200)); break; case 1: sVideo->Draw(SURFID_SELOPTION, TVector2<int>(140, 250)); break; case 2: sVideo->Draw(SURFID_SELOPTION, TVector2<int>(140, 300)); break; } }
Adding the Scenes
Ê Now that we have the Title screen, we need to add scenes which will show the Title screen first before going to the different scenes.
Ê In CSnakeGameManager.h, add Scene01() and Scene02() similar to Scene00().
Ê Register these scenes in CSnakeGameManager.cpp similar to Scene00() in the Init().
Ê Add/modify the scenes to be similar to the next slide. Note that the appropriate headers have been included.
Scene Definitions
void CSnakeGameManager::Scene00() { new CGround(); new CTitle(); } void CSnakeGameManager::Scene01() { new CGround(); new CSnake(); new CFood(); } void CSnakeGameManager::Scene02() { new CGround(); }
Test!
Ê Build and run your program!
Ê The game should begin with the Title Screen where you can select the number of players by pressing the up or down arrow keys then pressing enter to confirm the selection.
Ê Selecting QUIT exits the game.
Ê Select 1 Player goes to basic gameplay with 1 player.
Ê Select 2 Player and it enters an empty scene (except for the ground).
Ê See src/SnakeGD4N-‐08 for any clarifications
In-‐Game Menu Pause and Resume
In-‐Game Menu
Ê Purpose: Ê Pause the Game
Ê Return to Title Screen
Ê Similar to CTitle, CInGameMenu is a game object that overrides DrawGUI() instead of Draw()
Ê What’s special with CInGameMenu is that it should be able to freeze all the snakes in the game when the game is paused. Therefore, CInGameMenu should have a container of game objects to pause/resume.
CInGameMenu.h
#include "CGameObject.h" #include "CSnake.h" #include <list> class CInGameMenu : public GD4N::CGameObject { protected: int selected; std::list<GD4N::CGameObject*> objs; void Update(); void DrawGUI(); void SetObjectsActive(bool isActive); public: CInGameMenu(); ~CInGameMenu(); void AddGameObject(GD4N::CGameObject* obj); };
CInGameMenu.cpp
Ê Includes, Constructor and Deconstructor #include "CInGameMenu.h" #include "SVideoManager.h" #include "SInputManager.h" #include "CSnakeGameManager.h" CInGameMenu::CInGameMenu() : GD4N::CGameObject() { // isVisible is used for pause state as well isVisible = false; selected = 0; } CInGameMenu::~CInGameMenu() { objs.erase(objs.begin(), objs.end()); }
CInGameMenu.cpp
Ê DrawGUI() and necessary methods void CInGameMenu::DrawGUI() { sVideo->Draw(SURFID_INGAMEMENU, GD4N::TVector2<int>(113, 153)); sVideo->Draw(SURFID_SELOPTION, GD4N::TVector2<int>(126, (selected == 0) ? 205 : 242)); } void CInGameMenu::SetObjectsActive(bool isActive) { for (std::list<GD4N::CGameObject*>::iterator it = objs.begin(); it != objs.end(); it++) { (*it)->SetActive(isActive); } } void CInGameMenu::AddGameObject(GD4N::CGameObject *obj) { objs.push_front(obj); }
CInGameMenu.cpp
void CInGameMenu::Update() { if (sInput->GetKeyDown(SDLK_ESCAPE)) { SetObjectsActive(isVisible); isVisible = !isVisible; selected = 0; } if (isVisible) { if (sInput->GetKeyDown(SDLK_DOWN)) { selected++; } else if (sInput->GetKeyDown(SDLK_UP)) { selected--; } selected &= 1; // limit to 0 or 1 only if (sInput->GetKeyDown(SDLK_RETURN)) { switch (selected) { case 0: SetObjectsActive(isVisible); isVisible = false; break; case 1: // return to title screen sGameManager->ChangeScene(0); break; } } } }
Modify Scene01
Ê Now that we have the CInGameMenu, we add it in Scene01() and register the snakes to it.
void CSnakeGameManager::Scene01() { new CGround(); new CFood(); CSnake *snake = new CSnake(); CInGameMenu *igm = new CInGameMenu(); igm->AddGameObject(snake); }
Test!
Ê Build and run your program!
Ê From the Title Screen, select 1 Player
Ê Press ESC to open the In-‐Game Menu. Notice how the snakes stop moving. Press ESC again or select resume and press enter to resume the game
Ê See src/SnakeGD4N-‐09 for any clarifications
Ê The In-‐Game Menu also allows the player to leave the game when the snake dies
Challenge Complete 2 Player
2 Player
Ê Since we’ve completed the single player of snake, it’s time to modify it to become 2-‐player. Most of the modifications are in CSnake
Ê Things to do: Ê The snake should have a way to know if it’s first or second player
Ê Depending on the player number; their initial position, initial direction and controls should differ
Ê There should be a way to differentiate one player from the other
Ê Both snakes need to be paused by the in-‐game menu
Tetris GD4N