Download as pdf or txt
Download as pdf or txt
You are on page 1of 8

Overview

• Dealing with large projects


– Logical/physical structure
Game Programming in C++ – (Precompiled) headers
– The PIMPL pattern
Arjan Egges – Libraries
• Crash-proofing your game
Lecture #15: Game Development – Asserts
– Running the game
– Dealing with bad data

Logical vs. Physical Structure Logical vs. Physical Structure


• Logical structure: • A normal PC/console game will have at
– Classes least about 4000-5000 files to compile
– Algorithms
– Functions
• MMOGs will have even more files
– … • With projects of this size, a proper physical
• Physical structure: structure becomes crucial
– Files • Physical structure is determined by which
– Directories files need other files in order to compile
– Projects
– Makefiles
• In C++, this “need” translates into #include
– … directives

Logical vs. Physical Structure Logical vs. Physical Structure


• In an ideal world, every file would compile • A class that hides well its implementation
by itself will have better encapsulation
• Not possible, because a program is made – Less other classes depend on it
up of interacting objects – Cleaner logical structure
• We could try to minimize the amount of – Clear debugging
connections between files – Simpler testing
• The level of connections between a file • General rule: whenever a file is modified,
and the rest of the code is called insulation all files that include that file need to be
– Fewer connections Æ more insulated recompiled

1
Header files Header files
• Header files should contain the minimum • Also, beware of this:
amount of code that still allows everything to
compile and to run // GameConst.h // Enemy.h
#ifndef GAMECONST_H_ #ifndef ENEMY_H_
• Move non-essential information out of the #define GAMECONST_H_ #define ENEMY_H_
header file into the implementation file
– Constants only used in the implementation, local #define GAMELEVELS 42 #include “GameConst.h”
structure definitions, etc… // … // …
• However, C++ does not provide for a very clear
#endif #endif
distinction in this
– We still need to define the private members of a class
in the header file

Header files Header files


// MainApplication.cpp
#include “Enemy.h” • You might have encountered the preprocessor
directive #pragma once
void main() {
// … • It prevents the preprocessor from ever including
for (int i = 0; i < GAMELEVELS; ++i) the file twice
InitializeLevel(i); • It even prevents a file from being opened more
// … than once, which should speed up compilation
} times
• Now we depend on the Enemy.h file for • Although it is clearer and easier to type, it is not
including GameConst.h part of the C++ standard
• If the Enemy class for some reason does not – Not all compilers understand it so beware if you’re
need the GameConst header anymore Æ developing for multiple platforms!
compiler errors in seemingly unrelated files

Header files Header files


• We often see both #pragma once and the • Minimize the amount of #include directives
include guards used at the same time:
in your header files
#ifndef MYHEADERFILE_H_
#define MYHEADERFILE_H_
• Determine which includes are absolutely
necessary:
#if _MSC_VER > 1000
// B.h
#pragma once
#include “A.h”
#endif
// B inherits from A, so we have to include it
// Contents of the header file here
class B : public A {
// …
// …
};
#endif

2
Header files Header files
// GameCamera.h
// GameCamera.h #include “GameEntity.h”
#include “GameEntity.h” class GamePlayer; // forward declaration
#include “GamePlayer.h”
class GameCamera : public GameEntity {
class GameCamera : public GameEntity { public:
public: GamePlayer* getPlayer();
GamePlayer* getPlayer(); private:
private: GamePlayer* player_;
GamePlayer* player_; };
};
// GameCamera.cpp
// GameCamera.cpp #include “GameCamera.h”
#include “GameCamera.h” #include “GamePlayer.h”
// Rest of GameCamera implementation // Rest of GameCamera implementation

Header files Precompiled headers


• Why is moving an #include to the • Still… after minimizing the amount of
implementation file a gain? includes, we still include sometimes the
– Suppose that 100 other files need to include same headers over and over again
GameCamera.h // MyClass.cpp
– By removing the include from the header, we #include “MyClass.h”
prevented GamePlayer.h to be opened and #include <vector>
#include <iostream>
included 100 times
#include <map>
– If GamePlayer included another 10 files, we #include <windows.h>
prevented opening and including 1000 files! #include <algorithm>

// …

Precompiled headers Precompiled headers


• A precompiled header will be loaded and
• These APIs can have quite large and processed only once
complex header files • So we put only the headers that generally do not
change during the development phase
• These header files are not going to
• Visual Studio supports precompiled headers
change during the development phase
// MyClass.cpp
• Yet we keep including and processing #include “precomp.h” // Precompiled headers
them every time we compile any file #include “MyClass.h”
• Solution: precompiled headers
// other includes
– Not platform independent #include “YourClass.h”
– But if it is possible, take advantage of it! #include “SomethingElse.h”

// The rest of the MyClass implementation…

3
Precompiled headers The PIMPL pattern
Drawbacks: • Already following the previous guidelines
• Number of included headers grows should give you more self-sufficient
– Every file that uses the precompiled headers headers
automatically knows about all the files that are
part of the precompiled headers
• If you have many files, perhaps a
– If we add “windows.h” to the precompiled reorganization in some smaller self-
headers, possible compromise of multi- contained systems is a good idea
platform compatibility • A façade pattern can help in additional
• If we decide to compile without hiding of contents to the users
precompiled headers, greatly increased
compile time

The PIMPL pattern The PIMPL pattern


// GameCamera.h
#include “GameEntity.h”
#include “CameraLens.h”
• We need to include the STL string and the
#include <string> CameraLens headers, because they are
class GamePlayer; private members
• Forward declaration will not work because
class GameCamera : public GameEntity {
public:
CameraLens is not a pointer
GamePlayer* getPlayer(); • Only GameCamera will have access to
these members
private:
GamePlayer* pPlayer_; • But: all classes using GameCamera will
CameraLens lens_; also be including these implementation-
std::string name_; related headers
};

The PIMPL pattern The PIMPL pattern


// GameCamera.h
• The PIMPL pattern allows us to avoid #include “GameEntity.h”
including files required only for private class GamePlayer;
variables
class GameCamera : public GameEntity {
• Create a simple structure or class that public:
contains the private implementation GamePlayer* getPlayer();

• We create and destroy it along with the private:


object itself class PIMPL;
PIMPL* pimpl_;
};

4
The PIMPL pattern The PIMPL pattern
// GameCamera.cpp
#include “GameCamera.h” • We removed all includes related to private
#include “GamePlayer.h” implementation
#include “CameraLens.h”
#include <string>
• Reduction in dependencies between
headers
class GameCamera::PIMPL { • We have also removed all implementation
public: details from the header
CameraLens lens_;
std::string name_; • A little added complexity
GamePlayer* pPlayer_; • Minor performance cost for dynamic
};
// …
allocation and redirection

The PIMPL pattern Libraries


GameCamera::GameCamera() {
• In the case of larger development projects
pimpl_ = new PIMPL();
} (like games!), it is better to break down
code into independent subsystems
GameCamera::~GameCamera() { • Each subsystem is a set of related source
delete pimpl_;
}
code with a logical, cohesive objective
• These subsystems are compiled
GamePlayer* GameCamera::getPlayer() { separately from the main application into
return pimpl_->pPlayer_; static (.lib) or dynamic (.dll) libraries
}
• Examples of libraries: graphics rendering,
// … animation, sound, physics, …

Libraries Libraries
• Class dependencies without libraries • Class dependencies with libraries
Class1 Class1
Class2
Class3

Class2
Class6 Class5
Class3 Class4

Class5

Class6 Class7

Class4
Class7
Class8
Class8

5
Libraries Configurations
• We can test each library in isolation, • We generally also compile the engine libraries
working our way from the bottom up and applications with different configuration
settings
• In-house tools can pick and choose the
– Debug configuration: no compiler optimizations, all
libraries they need debug information is included, not focused on
• It becomes easier to share already-built performance
code – Release configuration: all optimization is done, no
debug information is kept, performance is maximal,
• Separating engine and game-specific code but difficult to debug this version
becomes easier – Other configurations between these two extremes are
• We can take a modular approach to the possible (debug-optimized, different platforms…)
code

Crash-proofing your game Asserts


• Stop the program if parameters passed to
• During debug we can try to catch as many some function are not according to the
bugs as possible functions precondition
– Depends heavily on how well we test the – Inventory::addItem does not allow a NULL
game pointer to be passed
• Debug asserts are a helpful tool for this • Check that the program is in a consistent
state
void Inventory::addItem(Item* pItem) {
assert(pItem != NULL); void GameEntity::handleKillMsg(const Msg& m) {
// Add the item to the inventory since we assert(!this->isDead());
// know for sure the item is not NULL this->setIsDead(true);
} // Do the rest of the killing process here…
}

Asserts Asserts
• Check that a complicated algorithm is
working the way we expect
– “sanity check”
• Stop the program if a function fails and we
– Checking postconditions after performing the cannot recover
operations – A NULL pointer was returned when trying to
allocate some memory
void MsgQueue::processAll() {
// Do all the processing here. Maybe from – Generally the case that a major problem
// different queues, so this is not a occurs
// straightforward process
// …

// double-check there are no messages left


assert(this->isEmpty());
}

6
Asserts Running the game
• When not to use asserts: • Unlike other applications, games are
– Trying to open or load a file that doesn’t exist sometimes run during 5, 10, 20 (or more!)
– Trying to load a file of the wrong format or an hours
older version – Tiny problems can become huge when not
– Entering “bad” data dealt with
– One of the objects didn’t load correctly • Difficult to trace and solve:
• Generally: anything dealing with factors – Memory leaks
exterior to the game engine should not be – Memory fragmentation
solved with asserts – Clock drift
– Error accumulation

Running the game Dealing with bad data


void Vector3D::normalize() {
• Keep the machine fresh float fLength = sqrt(x*x + y*y + z*z);
– Perform reset operations between levels x /= fLength;
– Some consoles can even be completely y /= fLength;
z /= fLength;
rebooted }
– Perhaps run some form of garbage collection
–…
• In this function, we might divide by 0!
• Tendency to propagate to other
functions/methods

Dealing with bad data Dealing with bad data


• Solution 1: Use asserts • Solution 2: Cope with it
void Vector3D::normalize() { void Vector3D::normalize() {
float fLength = sqrt(x*x + y*y + z*z); float fLength = sqrt(x*x + y*y + z*z);
assert(fLength != 0); if (fLength != 0) {
x /= fLength; x /= fLength;
y /= fLength; y /= fLength;
z /= fLength; z /= fLength;
} } else { // return a unit vector
x = 1.0f;
• Drawback: asserts are removed in the y = 0.0f;
release version of the game z = 0.0f;
– We end up with the same problem again… }
}

7
Dealing with bad data Dealing with bad data
• Solution 3: A compromise
• Advantages: void Vector3D::normalize() {
– Functions become much more robust float fLength = sqrt(x*x + y*y + z*z);
#ifdef ASSERTBADDATA
– There is no need for asserts assert(fLength != 0);
• Disadvantages: #endif
if (fLength != 0) {
– By adding extra logic, performance dropped x /= fLength;
– Perhaps function was inline earlier Æ might y /= fLength;
not be possible anymore z /= fLength;
} else { // return a unit vector
– Function still returns incorrect data x = 1.0f; y = 0.0f; z = 0.0f;
}
}

Dealing with bad data Summary


• We can add an ‘unsafe’ normalize function • Dealing with large projects
that can be used if it is already guaranteed • Crash-proofing your game
that no zero-length vector will be
normalized
void Vector3D::normalizeUnsafe() {
float fLength = sqrt(x*x + y*y + z*z);
Final lecture:
x /= fLength; • Course conclusion
y /= fLength;
z /= fLength; • Exam questions
}

You might also like