The majority of the work that went into creating Alien Attack was done in the object classes, while almost everything else was already being handled by manager classes in the framework. Here are the most important changes:
The GameObject
base class has a lot more to it than it previously did.
class GameObject { public: // base class needs virtual destructor virtual ~GameObject() {} // load from file virtual void load(std::unique_ptr<LoaderParams> const &pParams)=0; // draw the object virtual void draw()=0; // do update stuff virtual void update()=0; // remove anything that needs to be deleted virtual void clean()=0; // object has collided, handle accordingly virtual void collision() = 0; // get the type of the object virtual std::string type() = 0; // getters for common variables Vector2D& getPosition() { return m_position; } int getWidth() { return m_width; } int getHeight() { return m_height; } // scroll along with tile map void scroll(float scrollSpeed) { m_position.setX(m_position.getX() - scrollSpeed); } // is the object currently being updated? bool updating() { return m_bUpdating; } // is the object dead? bool dead() { return m_bDead; } // is the object doing a death animation? bool dying() { return m_bDying; } // set whether to update the object or not void setUpdating(bool updating) { m_bUpdating = updating; } protected: // constructor with default initialisation list GameObject() : m_position(0,0), m_velocity(0,0), m_acceleration(0,0), m_width(0), m_height(0), m_currentRow(0), m_currentFrame(0), m_bUpdating(false), m_bDead(false), m_bDying(false), m_angle(0), m_alpha(255) { } // movement variables Vector2D m_position; Vector2D m_velocity; Vector2D m_acceleration; // size variables int m_width; int m_height; // animation variables int m_currentRow; int m_currentFrame; int m_numFrames; std::string m_textureID; // common boolean variables bool m_bUpdating; bool m_bDead; bool m_bDying; // rotation double m_angle; // blending int m_alpha; };
This class now has a lot of the member variables that used to be in SDLGameObject
. New variables for checking whether an object is updating, doing the death animation, or is dead, have been added. Updating is set to true when an object is within the game screen after scrolling with the game level.
In place of a regular pointer to LoaderParams
in the load function, an std::unique_ptr
pointer is now used; this is part of the new
C++11 standard and ensures that the pointer is deleted after going out of scope.
virtual void load(std::unique_ptr<LoaderParams> const &pParams)=0;
There are two new functions that each derived object must now implement (whether it's owned or inherited):
// object has collided, handle accordingly virtual void collision() = 0; // get the type of the object virtual std::string type() = 0;
The SDLGameObject
class has now been renamed to ShooterObject
and is a lot more specific to this type of game:
class ShooterObject : public GameObject { public: virtual ~ShooterObject() {}// for polymorphism virtual void load(std::unique_ptr<LoaderParams> const &pParams); virtual void draw(); virtual void update(); virtual void clean() {}// not implemented in this class virtual void collision() {}//not implemented in this class virtual std::string type() { return "SDLGameObject"; } protected: // we won't directly create ShooterObject's ShooterObject(); // draw the animation for the object being destroyed void doDyingAnimation(); // how fast will this object fire bullets? with a counter int m_bulletFiringSpeed; int m_bulletCounter; // how fast will this object move? int m_moveSpeed; // how long will the death animation takes? with a counter int m_dyingTime; int m_dyingCounter; // has the explosion sound played? bool m_bPlayedDeathSound; };
This class has default implementations for draw and update that can be used in derived classes; they are essentially the same as the previous SDLGameObject
class, so we will not cover them here. A new function that has been added is doDyingAnimation
. This function is responsible for updating the animation when enemies explode and then setting them to dead so that they can be removed from the game.
void ShooterObject::doDyingAnimation() { // keep scrolling with the map scroll(TheGame::Instance()->getScrollSpeed()); m_currentFrame = int(((SDL_GetTicks() / (1000 / 3)) % m_numFrames)); if(m_dyingCounter == m_dyingTime) { m_bDead = true; } m_dyingCounter++; //simple counter, fine with fixed frame rate }
The Player object now inherits from the new ShooterObject
class and implements its own update function. Some new game-specific functions and variables have been added:
private: // bring the player back if there are lives left void ressurect(); // handle any input from the keyboard, mouse, or joystick void handleInput(); // handle any animation for the player void handleAnimation(); // player can be invulnerable for a time int m_invulnerable; int m_invulnerableTime; int m_invulnerableCounter; };
The ressurect
function resets the player back to the center of the screen and temporarily makes the Player
object invulnerable; this is visualized using alpha
of the texture. This function is also responsible for resetting the size value of the texture which is changed in doDyingAnimation
to accommodate for the explosion texture:
void Player::ressurect() { TheGame::Instance()->setPlayerLives(TheGame::Instance() ->getPlayerLives() - 1); m_position.setX(10); m_position.setY(200); m_bDying = false; m_textureID = "player"; m_currentFrame = 0; m_numFrames = 5; m_width = 101; m_height = 46; m_dyingCounter = 0; m_invulnerable = true; }
Animation is a big part of the feel of the Player
object; from flashing (when invulnerable), to rotating (when moving in a forward or backward direction). This has led to there being a separate function dedicated to handling animation:
void Player::handleAnimation() { // if the player is invulnerable we can flash its alpha to let people know if(m_invulnerable) { // invulnerability is finished, set values back if(m_invulnerableCounter == m_invulnerableTime) { m_invulnerable = false; m_invulnerableCounter = 0; m_alpha = 255; } else// otherwise, flash the alpha on and off { if(m_alpha == 255) { m_alpha = 0; } else { m_alpha = 255; } } // increment our counter m_invulnerableCounter++; } // if the player is not dead then we can change the angle with the velocity to give the impression of a moving helicopter if(!m_bDead) { if(m_velocity.getX() < 0) { m_angle = -10.0; } else if(m_velocity.getX() > 0) { m_angle = 10.0; } else { m_angle = 0.0; } } // our standard animation code - for helicopter propellors m_currentFrame = int(((SDL_GetTicks() / (100)) % m_numFrames)); }
The angle and alpha
of an object are changed using new parameters to the drawFrame
function of TextureManager
:
void TextureManager::drawFrame(std::string id, int x, int y, int width, int height, int currentRow, int currentFrame, SDL_Renderer *pRenderer, double angle, int alpha, SDL_RendererFlip flip) { SDL_Rect srcRect; SDL_Rect destRect; srcRect.x = width * currentFrame; srcRect.y = height * currentRow; srcRect.w = destRect.w = width; srcRect.h = destRect.h = height; destRect.x = x; destRect.y = y; // set the alpha of the texture and pass in the angle SDL_SetTextureAlphaMod(m_textureMap[id], alpha); SDL_RenderCopyEx(pRenderer, m_textureMap[id], &srcRect, &destRect, angle, 0, flip); }
Finally the Player::update
function ties this all together while also having extra logic to handle when a level is complete:
void Player::update() { // if the level is complete then fly off the screen if(TheGame::Instance()->getLevelComplete()) { if(m_position.getX() >= TheGame::Instance()->getGameWidth()) { TheGame::Instance()->setCurrentLevel(TheGame::Instance() ->getCurrentLevel() + 1); } else { m_velocity.setY(0); m_velocity.setX(3); ShooterObject::update(); handleAnimation(); } } else { // if the player is not doing its death animation then update it normally if(!m_bDying) { // reset velocity m_velocity.setX(0); m_velocity.setY(0); // get input handleInput(); // do normal position += velocity update ShooterObject::update(); // update the animation handleAnimation(); } else // if the player is doing the death animation { m_currentFrame = int(((SDL_GetTicks() / (100)) % m_numFrames)); // if the death animation has completed if(m_dyingCounter == m_dyingTime) { // ressurect the player ressurect(); } m_dyingCounter++; } } }
Once a level is complete and the player has flown offscreen, the
Player::update
function also tells the game to increment the current level:
TheGame::Instance()->setCurrentLevel(TheGame::Instance()->getCurrentLevel() + 1);
The
Game::setCurrentLevel
function changes the state to BetweenLevelState
:
void Game::setCurrentLevel(int currentLevel) { m_currentLevel = currentLevel; m_pGameStateMachine->changeState(new BetweenLevelState()); m_bLevelComplete = false; }
A game such as Alien Attack needs a lot of enemy types to keep things interesting; each with its own behavior. Enemies should be easy to create and automatically added to the collision detection list. With this in mind, the Enemy
class has now become a base class:
// Enemy base class class Enemy : public ShooterObject { public: virtual std::string type() { return"Enemy"; } protected: int m_health; Enemy() : ShooterObject() {} virtual ~Enemy() {} // for polymorphism };
All enemy types will derive from this class, but it is important that they do not override the type
method. The reason for this will become clear once we move onto our games collision detection classes. Go ahead and take a look at the enemy types in the Alien Attack source code to see how simple they are to create.
Scrolling backgrounds are important to 2D games like this; they help give an illusion of depth and movement. This
ScrollingBackground
class uses two destination rectangles and two source rectangles; one expands while the other contracts. Once the expanding rectangle has reached its full width, both rectangles are reset and the loop continues:
void ScrollingBackground::load(std::unique_ptr<LoaderParams> const &pParams) { ShooterObject::load(std::move(pParams)); m_scrollSpeed = pParams->getAnimSpeed(); m_scrollSpeed = 1; m_srcRect1.x = 0; m_destRect1.x = m_position.getX(); m_srcRect1.y = 0; m_destRect1.y = m_position.getY(); m_srcRect1.w = m_destRect1.w = m_srcRect2Width = m_destRect1Width = m_width; m_srcRect1.h = m_destRect1.h = m_height; m_srcRect2.x = 0; m_destRect2.x = m_position.getX() + m_width; m_srcRect2.y = 0; m_destRect2.y = m_position.getY(); m_srcRect2.w = m_destRect2.w = m_srcRect2Width = m_destRect2Width = 0; m_srcRect2.h = m_destRect2.h = m_height; } void ScrollingBackground::draw() { // draw first rect SDL_RenderCopyEx(TheGame::Instance()->getRenderer(), TheTextureManager::Instance()->getTextureMap()[m_textureID], &m_srcRect1, &m_destRect1, 0, 0, SDL_FLIP_NONE); // draw second rect SDL_RenderCopyEx(TheGame::Instance()->getRenderer(), TheTextureManager::Instance()->getTextureMap()[m_textureID], &m_srcRect2, &m_destRect2, 0, 0, SDL_FLIP_NONE); } void ScrollingBackground::update() { if(count == maxcount) { // make first rectangle smaller m_srcRect1.x += m_scrollSpeed; m_srcRect1.w -= m_scrollSpeed; m_destRect1.w -= m_scrollSpeed; // make second rectangle bigger m_srcRect2.w += m_scrollSpeed; m_destRect2.w += m_scrollSpeed; m_destRect2.x -= m_scrollSpeed; // reset and start again if(m_destRect2.w >= m_width) { m_srcRect1.x = 0; m_destRect1.x = m_position.getX(); m_srcRect1.y = 0; m_destRect1.y = m_position.getY(); m_srcRect1.w = m_destRect1.w = m_srcRect2Width = m_destRect1Width = m_width; m_srcRect1.h = m_destRect1.h = m_height; m_srcRect2.x = 0; m_destRect2.x = m_position.getX() + m_width; m_srcRect2.y = 0; m_destRect2.y = m_position.getY(); m_srcRect2.w = m_destRect2.w = m_srcRect2Width = m_destRect2Width = 0; m_srcRect2.h = m_destRect2.h = m_height; } count = 0; } count++; }
3.145.50.222