Video Game Design/Structure
Though there are many different types of video games, there are a few properties that are constants: Every game requires at least one player, every game gives the player at least one challenge, Every game uses a display, Every game has at least one method of input/control.
The User Interface
editAs described at the beginning of this chapter, the user interface is made up of sprites, menus and so forth. Its what the user is given to control the actions within the game. These graphics are defined as buttons' which can be pushed, or a character which can be moved by the arrow keys. All of these elements are a part of the user interface.
The Main Menu
editTo start off with, just about every video game boots up to a main menu. This is usually a screen with some type of background, with an arrangement of buttons for actions such as new game or start game, options, load game and quit game.
This screen acts as a control panel for the game, allowing the player to change settings, choose modes, or access the actual game.
Sometimes, a game will use the main menu as the in game menu. The in game menu is usually accessed by the escape key or the start button during gameplay. The in game menu allows the player to access most of the main menu actions with additional ones such as displaying character stats, points, inventory and so forth. Not all menus have to be squares with words in them though. The game Secret of Mana uses a creative menu where the level stays in focus while the choices form a circle about the player.
These menus are not required, but it is traditional to include them.
Starting the game
editWhen you first start up the game a series of splash screens are shown. A splash screen contains elements such as logos, movies, and so forth. This often is used to tell the player the companies that contributed to the game firsthand and sometimes gives part or whole introduction to the plot.
When the actual game has started there is often an introductory movie that gives the prologue to the plot. This is not a movie like you see in the theater but usually a better rendered use of the game's own graphics and sounds.
In most games, you will then be asked for your name and in some games you will be allowed to customize your character, settings and so forth.
This stage of the game is called the tutorial. It is not always considered a part of the game's plot, but in some games it is integrated into the game that even though it is the tutorial stage it is a part of the game's plot anyways. We will call this tutorial integration. It is widely used in games such as The Legend of Zelda and Super Mario 64
Playing the game
editDuring gameplay there are some basic concepts that just about every game uses. They are listed below:
Player-character relationship
editThe player's role in the character. How does the player control the character? There are usually 3 types of PCR, 3rd person, 1st person and influence:
3rd person: The player is not the character but instead is controlling the character/characters impersonally.
1st Person: The player is the character/characters - and sees things from the characters point of view both personally and visually.
Influence: The player is not tied to any character/characters but merely has an influence in the game. This is seen in puzzle games such as Tetris, and also in RTS games.
Game world
editWhat is the 'world' that is portrayed by the game? Within this there are 2 considerations.
Character role: There is also the question of what role the character plays in the game itself, there are 3 types in this sense.
Protagonist: Everything revolves around the character/characters the save the day type deal. Seen in games such as Zelda, Mario, Final Fantasy, and so forth.
Arcadic Conventional: An impersonal arcade character.
Influence: The character is a faceless influence within the realm of the game.
Law What are the laws, concepts, rules etc. that define the realm?
Graphics What is seen and the laws of style
Sound What is heard and the laws of style
Gameplay What is played how the game is played
Saving/Loading
editConsidering the saving and loading of the game, usually this can be a basic menu action wherein the player types a save name and the game is saved. In some games though, more creative approaches are taken so that the player is not pulled out of the gaming experience. Metroid does this with its save stations.
Loading however, is usually a menu action.
The Main Loop
editAt the heart of our game is the main loop (or game loop). Like most interactive programs, our game runs until we tell it to stop. Each cycle through the loop is like the heart beat of the game. The main loop of a real time game is often tied to the video update (vsync). If our main loop is synchronized to a fixed time hardware event, such as a vsync, then we must keep the total processing time for each update call under that time interval or our game will "chug."
// a simple game loop in C++ int main( int argc, char* argv[] ) { game our_game; while ( our_game.is_running()) { our_game.update(); } return our_game.exit_code(); }
Each console manufacturer has their own standards for video game publication, but most require that the game should provide visual feedback within the first few seconds of starting. As a general design guideline, it is desirable to provide the player with feedback as quickly as possible.
For this reason most start up and shut down code is usually processed from within the main loop. Lengthy start up and shut down code can either run in a sub thread monitored from the main update() or sliced into small chunks and executed in order from within the update() routine itself.
State Machine
editEven without considering the various modes of play within the game itself, most game code will belong to one of several states. A game might contain the following states and sub states:
- start up
- licenses
- introductory movie
- front end
- game options
- sound options
- video options
- loading screen
- main game
- introduction
- game play
- game modes
- pause options
- end game movie
- credits
- shut down
One way to model this in the code is with a state machine:
class state { public: virtual void enter( void )= 0; virtual void update( void )= 0; virtual void leave( void )= 0; };
Derived classes can then override these virtual functions to provide state specific code. The main game object can then hold a pointer to the current state and allow the game to flow from state to state.
extern state* shut_down; class game { state* current_state; public: game( state* initial_state ): current_state( initial_state ) { current_state->enter(); } ~game() { current_state->leave(); } void change_state( state* new_state ) { current_state->leave(); current_state= new_state; current_state->enter(); } void update( void ) { current_state->update(); } bool is_running( void ) const { return current_state != shut_down; } };
Time
editA game loop must consider both how much real time has passed and how much game time has passed. Separating the two makes slow motion (i.e. BulletTime) effects, pause states and debugging much easier. If you intend to make a game that can rewind time, like Blinx or Sands of Time you will need to be able to run the game loop forward while running the game time backwards.
Another consideration surrounding time depends on whether you want to go for a fixed or variable frame rate. Fixed frame rates can simplify much of the maths and timings within the game but they can make the game much harder to port internationally (e.g. going from 60 Hz TVs in the US to 50 Hz TVs in Europe.) For this reason it is advisable to pass frame time as a variable even if the value never changes. Fixed frame rates suffer from stuttering when the work load per frame reaches the limits and this can feel worse than a lower frame rate.
Variable frame rates, on the other hand, automatically compensate for different TV refresh rates. But variable rates often feel soggy in comparison to fixed rate games. Debugging, particularly debugging timing and physics issues, is usually more difficult with variable time. When implementing timing in your code there are often several hardware timers available on a given platform, often with different resolutions, overheads for accessing them and latencies. Pay special attention to the real time clocks available. You must use a clock with a high enough resolution, while not using excessive precision. You might need to handle the case where the clock wraps (for example, a 32-bit nanosecond timer will overflow back to zero every 2^32 nanoseconds which is only 4.2949673 seconds).
const float game::NTSC_interval= 1.f / 59.94f; const float game::PAL_interval= 1.f / 50.f; float game::frame_interval( void ) { if ( time_system() == FIXED_RATE ) { if ( region() == NTSC ) { return NTSC_interval; } else { return PAL_interval; } } else { float current_time= get_system_time(); float interval= current_time - last_time; last_time= current_time; if ( interval < 0.f || interval > MAX_interval ) { return MAX_interval; } else { return interval; } } } void game::update( void ) { current_state->update( frame_interval()); }
Loading
editModern games are usually loaded either directly from CD or indirectly from the hard drive. Either way, your game could spend a significant amount of time in I/O access. Disc access, particularly CD and DVD access, is a lot slower than the rest of the game. Many console manufacturers make it a standard that all disc access must be indicated visually; and that is not a bad design choice anyway.
However, most disc access API functions (particularly those that map through the standard I/O of the C runtime library) stall the processor until the transfer is complete. This is called synchronous access.
Multi-threaded disc access
editOne way to get feedback while accessing the disc is to run disc operations in their own thread. This has the advantage of allowing other processing to continue, including drawing some visual feedback of the disc operation. But the cost is that there is a lot more code to write and access to resources needs to be synchronized.
Asynchronous disc access
editSome console operating system API's handle some of the multi-threading code for you by allowing disc access to be scheduled with asynchronous read operations. Asynchronous reads can tell that they are done either by polling with the file handle or using a callback.
Renderable Objects
editWhether a game uses 2D graphics, 3D graphics, or a combination of both, the engine should handle them similarly. There are three main things to consider.
- Certain objects may take a while to load, and can momentarily freeze the game.
- Some machines run slower than others, and the gameplay must continue with a low framerate.
- Some machines run faster, and animation could be smoother than the time interval with a higher framerate.
Therefore, it is a good idea to create a base class as an interface that separates these functions. This way, every drawable object can be treated the same way, all loading can be done at the same time (for load screens), and all drawing can be done independently of the time interval. OpenGL also requires object display lists to have a unique integer identifier, so we'll also need support for assigning that value.
class IDrawable { public: virtual void load( void ) {}; virtual void draw( void ) {}; virtual void step( void ) {}; int listID() {return m_list_id;} void setListID(int id) {m_list_id = id;} protected: int m_list_id; };
Bounding Boxes
editOne common method of collision detecting is by using axis-aligned bounding boxes. To implement this, we will build upon our previous interface, IDrawable. It should remain separate from IDrawable, because after all, not every object drawn on the screen will require collision detecting. A 3D box should be defined by six values: x, y, z, width, height, and depth. The box should also return the object's current minimum and maximum values in space. Here is an example 3D bounding box class:
class IBox : public IDrawable { public: IBox(); IBox(CVector loc, CVector size); ~IBox(); float X() {return m_loc.X();} float XMin() {return m_loc.X() - m_width / 2.;} float XMax() {return m_loc.X() + m_width / 2.;} float Y() {return m_loc.Y();} float YMin() {return m_loc.Y() - m_height / 2.;} float YMax() {return m_loc.Y() + m_height / 2.;} float Z() {return m_loc.Z();} float ZMin() {return m_loc.Z() - m_depth / 2.;} float ZMax() {return m_loc.Z() + m_depth / 2.;} protected: float m_x, m_y, m_z; float m_width, m_height, m_depth; }; IBox::IBox() { m_x = m_y = m_z = 0; m_width = m_height = m_depth = 0; } IBox::IBox(CVector loc, CVector size) { m_x = loc.X(); m_y = loc.Y(); m_z = loc.Z(); m_width = size.X(); m_height = size.Y(); m_depth = size.Z(); }
Make your own game engine
editComplexity
editWhile it is simple enough in the majority of APIs to display an image, or a textured cube, as you begin to add more complexity to your game, the task naturally becomes slightly harder. With a poorly structured engine this complexity becomes increasingly more so as your engine becomes larger. It can become unclear what changes are needed, and you may end up with huge special case switch blocks where some simple abstraction would have simplified the problem.
Extensibility
editThis ties in with the above point - as your game engine evolves you are going to want to add new features. With an unstructured engine these new features are hard to add in, and a lot of time may be spent finding out why the feature is not working as expected. Maybe its some strange function that is interrupting it. A carefully crafted engine separates tasks out so that extending a certain area is just that - and not having to modify prior code.
Know your code
editWith a well thought out game engine design, you will begin to know your code. You'll find yourself spending a lot less time staring (or maybe cursing) blindly at a blank screen wondering why on Earth your code is not doing what you thought you had told it.
DRY Code
editDRY is an acronym frequently used (especially in the extreme programming environment) that means do not repeat yourself. It sounds simple but can provide you with a lot more time to do other things. Also, code that does a specific task is in one central location so you can modify that small section and see your changes take effect everywhere.
In fact, it's common sense
editThe above points probably do not seem that incredible to you - they are really common sense. But without the thought and planning in designing a game engine, you will find reaching these targets a lot harder.