Galaga Tutorial

From LagoonWiki

Jump to: navigation, search

Galaga was a third-generation sequel to the popular Space Invaders arcade game. In this tutorial you will create the skeleton of a Galaga-like game using the Entropy game engine, which is built on the OGRE FOSS 3-D rendering engine.

Concepts Covered: EVA System, Project System, Ogre, Ogre Art, Input System, Yaml

EVA is an acronym for Entity-Variable-Aspect, the sytem that Entropy uses to manipulate game objects (entities).

We will create galaga, in several phases

  1. Create our project
  2. Create a ship for the player to fly
    1. Make it visible
    2. Make it move
  3. Create enemy ships
    1. Make them visible
    2. Make them move
  4. Shoot enemies
    1. Create bullets
    2. Allow the player to fire
    3. Allow the bullets to do damage
  5. Create some enemy ship AI

Keep in mind these are the steps I took to create galaga, which is currently included in entropy. If you want to create your galaga, name it something else (or delete the existing one that you got when you checked entropy out).

Contents

Before you Start

If you have been working through the CS 381 tutorials, make sure all your assets are saved in your own directory and your scripts are up to date, because this tutorial has you remove the entire galaga/ subdirectory tree.

Note: All directory paths are relative to the entropy root directory

If you have been working on the CS 381 tutorials, do the following in the entropy/ directory:

rm core/ogre/ogreApplication.*
rm *.cfg
svn update

This will make sure any changes you have previously made do not conflict with this tutorial.

Regardless of whether you have been working on the CS 381 tutorials, do the following in the entropy/ directory:

rm .emake
rm -rf galaga/

Do not worry if it says that .emake does not exist.

Create galaga project

Create and populate some directories:

  • Working in the entropy/ directory, create:
    • galaga/
    • galaga/data/
    • galaga/data/entity/
    • galaga/src/
  • Copy in some template files:
    • templates/startup.py --> galaga/galaga_startup.py
      • This is the second python file to run on boot (right after go.py which does the very basic initialization
      • It tells the dataserver a couple of directories to load:
        • data/common
        • galaga/data (Edit this line!)
      • It then creates the game world and triggers some initialization events.
    • templates/options.yaml --> galaga/galaga_options.yaml
      • This file determines what modules are activated at run time: OGRE 3D for the graphics rendering, and Bullet for the physics modeling.

Create Abstract Entity Types

  • To simplify things we define some abstract entity types, from which the various things in our world will inherit.
    • We will create two, first a generic unit, which is something in the world
    • Secondly, an actual ship class, which has a gun, etc...
    • This is a bit of overkill for galaga, but would be very reasonable on a larger project

unit_ent.yaml

  • Create galaga/data/entity/unit_ent.yaml
    • Copy from templates/unit_ent.yaml
      • Leave the Inheritence to visual, since we want our entity to be displayed
      • Remove the Selection inheritence, since we wont be selecting things in our arcade game.
      • Remove that other aspects and variables, since we aren't using them
      • Add an inheritence to physics, since we will want to have physics attached to our entity

It should look something like:

define_entity:
  unit:
    m_inheritsFrom:
        visual:  True
        physics: True
    m_aspect:
    m_var:

ship_ent.yaml

  • Create galaga/data/entity/ship_ent.yaml
    • Copy from templates/unit_ent.yaml
      • Change 'unit' to 'ship', since that's what this file defines.
      • Inherit from unit
      • Attach an HP aspect
        • The HP aspect will monitor how many hitpoints this entity has, reducing them as it takes damage, and destroying the entity when it runs out of HP.
      • Create variables describing the hp, maxhp, and hpRegenerationRate for the entity.
      • Define the various physics settings attached to this entity.

It should look something like

define_entity:
  ship:
    m_inheritsFrom:
        unit: True
    m_aspect:
        HP: True
    m_var:
        hp                 : 100
        maxhp              : 100
        hpRegenerationRate : 10
        mass               : 50
        linearDamping      : 10
        angularDamping     : 5

Create A Ship for the Player to Fly

  • Create a directory for this type.
    • By convention in entropy, each entity type has its own directory, and we store all the files related to that entity (*_ent.yaml, *.mesh, etc.) within that directory.
    • mkdir galaga/data/entity/playerShip
  • Define the variables and aspects that make up the entity
    • Copy templates/tank_ent.yaml -> galaga/data/entity/playerShip/playerShip_ent.yaml
    • The _ent.yaml suffix designates this yaml file as one containing an entity definition -- any other suffix will not be parsed.
  • Edit playerShip_ent.yaml
    • Change 'tank' to 'playerShip'.
    • Change the inheritance from 'unit' to 'ship' (which you created above).
    • Remove all aspects.
    • Remove all variables except for 'pos'.
    • Add a new variable for the mesh name (see example below)

Entity definition files have three fundamental parts. The first is a set of parent entity types, from which all definitions can be loaded. The second is a set of aspects, which are either attached or not in the m_aspect section. The third is a set of variables; this section provides default values for various variables that overwrite the defaults within the _var.yaml files. Note that as we create various aspects for PlayerShip later in the tutorial, we will add them to this entity definition.

playerShip_ent.yaml should ultimately look something like:

define_entity:
  playerShip:
    m_inheritsFrom:
        ship: True
    m_aspect:
        #PlayerShip: True
    m_var:
        pos:            [0,0,0]
        mesh:           playerShip

Create Art for PlayerShip

  • First of all, setup blender
  • Next, use blender to create a model for your player ship.
    • This is outside the current scope of this tutorial.
    • You can also just copy my playerShip model if you want
      • Copy playerShip.mesh -> yourEntityType.mesh
      • Copy playerShip.material -> yourEntityType.material
  • Create directories to store the artwork:
    • galaga/data/models/
    • galaga/data/materials/
    • galaga/data/materials/scripts/
    • galaga/data/materials/textures/
  • Add those directories to resources.cfg in the root entropy directory.
  • Put the model and its texture in the proper directories.
    • See the CS 381 tutorials for instructions on how assets work.
    • Note: If you put a .mesh.xml in the models/ directory, running 'make' in the entropy directory will run the OgreXMLConverter program to create the .mesh file, overwriting any .mesh with the same name.

Run and Test - PlayerShip visible

  • Edit galaga/galaga_startup.py
    • Create a playerShip, and move it somewhere.
    • Disable gravity, since galaga takes place in deep space.
    • Make the camera moveable.
      • Ignore the horrendous quaternion setting for now.

Fill in the following definition of on_initUniverse in galaga_startup.py

  • The 'def' line should already be there.
def on_initUniverse(event):
    print '*** galaga.on_initUniverse ***'
    from entropy import Vec3f, Quatf
    from entityServer import entityServer
    from varServer import varServer
 
    # Physics - disable gravity in outer space:
    from physics import control_physics
    varHandle_gravity = varServer.find_varHandle('gravity')
    var_gravity       = control_physics.find_var(varHandle_gravity)
    var_gravity.set(Vec3f(0.0, 0.0, 0.0))
 
    # Prepare to find variables used for ships and camera:
    varHandle_pos = varServer.find_varHandle('pos')
    varHandle_rot = varServer.find_varHandle('rot')
 
    # Create the player's ship:
    ent           = entityServer.createEntity('playerShip')
    var_pos       = ent.find_var(varHandle_pos)
    var_pos.set(Vec3f(0.0, 5, 45.00))

    # Position the camera:
    from camera import camera
    var_pos = camera.find_var(varHandle_pos)
    var_pos.set(Vec3f(0,100,0))
    var_rot = camera.find_var(varHandle_rot)
    var_rot.set(Quatf(-(2**0.5 / 2), 0.0, 0.0, 2**0.5 / 2.0))

Run entropy:

make
python go.py galaga

When you run it you should see the playership somewhere near the bottom of the screen. The camera is looking straight down at map coordinates (0,0), from an elevation of 100. The ship is at map coordinates (0,45), at elevation 5. (If you have been working on the CS 381 tutorials, you may have to edit ogreApplication.cc to remove your terrain or anything else that is obstructing the view, and recompile before you run this.)

Troubleshooting

Simple typos in your data definitions can cause the engine to fail miserably. It emits so much information upon startup that what you need to see may scroll off the screen (especially if it starts spilling garbage characters). You can capture everything to a file with -

python go.py galaga 1> stdout.log 2> stderr.log

That will create (or overwrite) files with the indicated names, catching the two standard UNIX output streams. Most likely you should work your way up from the bottom of the files to identify the error that caused the failure. The messages are often vague, but will generally narrow down your search -- e.g., tip you off as to whether a variable is not defined properly or some critical piece of artwork cannot be found. (Unfortunately there will probably be a lot of spurious error messages higher in the file, which you will learn to ignore.)

These logs can become very large if you leave the engine running for very long, so you should put them somewhere where they won't count against your disk quota.

When using the GNU debugger:

  • Note that if you gdb it, and it crashes it will lock your system -- stupid OIS.
    • To get around this run it with the --debug option within gdb.
      • This will disable input, but it will also repeat all the commands you issued the last time you ran it, which is awesomely useful.

Make the playerShip move

  • Create a playerShip aspect, which will interface between the inputServer, and the physics attached to the entity

Create the .h and .cc files using the templated code generator

Create the code using the templated code generator:

  • pushd core/templates
  • python templatedCodeGenerator.py
    • A window pops up.
    • Select 'aspect' on the left.
    • Set the 'dir' field to '../../galaga/src/'
    • Set 'ClassName' to 'PlayerShip'. (N.B. -- Aspect names are capitalized.)
    • Press 'Create'.
    • Preview windows for the .h and .cc code will pop up. Click 'create' at the bottom of one of them, and your generated code will be written out to the directory you indicated.

Edit the PlayerShip Aspect to do what we want

Edit the .h file

Add the following variable definitions in a new 'public' section just before the closing curly brace:

 public:
   // Aspect's inputs:
   EVA::Var<gmtl::Vec3f> var_player_movement; // for player inputs
   EVA::Var<bool>        var_player_firing;   // for later use
   // Aspect's outputs:
   EVA::Var<gmtl::Vec3f> var_force;  // to be read by the physics engine
   EVA::Var<bool>        var_firing; // for later use

Edit the .cc file

The templated code generator set up the include files that any aspect will need, but this example code will need another include file for the input server, and it uses a namespace declaration. So add two lines near the top, so that the first five lines look like this:

#include "playerShip.h"
#include "entity.h"
#include "aspectServer.h"
#include "inputServer.h"
using namespace gmtl;

Fill in the template function definitions as follows.

Connect the variables to our entity when we are created:

void PlayerShip::on_attach(){
    CONNECT_VAR(var_player_movement);
    CONNECT_VAR(var_player_firing);
    CONNECT_VAR(var_firing);
    CONNECT_VAR(var_force);

In the same procedure, register variables for the input server to write to:

    InputServer* inputServer = InputServer::getInstance();
    inputServer->register_inputVar(var_player_movement.get_internalVar(), "player_movement");
    inputServer->register_inputVar(var_player_firing.get_internalVar(),   "player_firing");
}

Unregister variables from the input server when we are destroyed:

bool PlayerShip::on_entityDeath(){
   InputServer* inputServer = InputServer::getInstance();
   inputServer->unregister_inputVar(var_player_movement.get_internalVar(), "player_movement");
   inputServer->unregister_inputVar(var_player_firing.get_internalVar(), "player_firing");
   return true;
}

(Note the return true -- aspects can return false when the entity is dying, which can be used to extend its death for a while.)

Define what happens to the entity at each clock tick. This example just applies a force to the entity. (It multiplies the player input by 10,000 in order to overcome the high resistance attached to the entity.)

void PlayerShip::on_entityTick(const TickInfo* tickInfo){
   Vec3f player_movement = var_player_movement.get();
   bool player_firing = var_player_firing.get();
   Vec3f force;
   force[0] = player_movement[0] * 10000;
   force[1] = player_movement[1] * 10000;
   force[2] = player_movement[2] * 10000;
   var_force.set(force);
   var_firing.set(player_firing);
}

Note that the get and set functions read from and write to the entity's aspect's variables.

Associate the Aspect with the Entity

In playerShip_ent.yaml, uncomment the line PlayerShip: True to tell the playerShip entity that it is supposed to use the PlayerShip aspect.

Identify the Variables to the Entropy Variable Server

Create a new file galaga/data/galaga_var.yaml with the following contents:

m_var_type:
   player_movement    : [Vec3f, [0,0,0]]
   player_firing      : [bool, False]
   firing             : [bool, False]

Setup KeyMappings

  • KeyMappings are really just a generic input mapping, and should be renamed.
    • Create galaga/data/galaga_keymapping.yaml
    • The keymapping file is just a list of what to do when various keys are pressed
    • When variables are registered with the InputServer they are assigned to some name, which can be mapped to here.
    • The yaml file is fairly self-explanatory.
      • The one confusing this is the shadow system.
      • Many times in games some keys can subsum the effect of other keys, such as the left / right arrow.
        • If a player presses the left arrow, the ship moves left. If the player then presses the right arrow, the ship will then move right, if the player releases the right, the ship will return to moving left.
        • This gives better behavior then the atlernative of having keys sum to 0, or of having one key always taking priority.
        • The complete list of keycodes is in src/ui/ei.h

My _keymapping Looks like:

keymapping:
    KC_LEFT:
        var:            player_movement
        axis:           x
        on_press:       -1.0
        on_release:     0.0
        shadows:        KC_RIGHT
    KC_RIGHT:
        var:            player_movement
        axis:           x
        on_press:       +1.0
        on_release:     0.0
        shadows:        KC_LEFT
    KC_UP:
        var:            player_movement
        axis:           z
        on_press:       -1.0
        on_release:     0.0
        shadows:        KC_DOWN
    KC_DOWN:
        var:            player_movement
        axis:           z
        on_press:       +1.0
        on_release:     0.0
        shadows:        KC_UP
    KC_SPACE:
        var:            player_firing
        on_press:       True
        on_release:     False

Test Movement

Run entropy again

make
python go.py galaga

This time, pressing those keys should move the entity.

The Big Picture

This is a good time to review what you have done so far, to make sure the big picture does not get lost in the details of implementation.

Specifying the Game's Startup

You created the initial skeleton of a game to run under the Entropy engine, which is a wrapper around the OGRE 3D rendering engine. You told Entropy:

  • which major modules your game will use (galaga_options.yaml)
  • how to start up the game (galaga_startup.py)

For the startup you used a template that includes the necessary bootstrap commands, and sets up three things specific to this game:

  • tells the physics engine to turn off gravity
  • creates a ship for the player to control, and positions it in the world model
  • positions a static 'camera' in the world model, to define the viewpoint for rendering the 3D world

Most of the rest of what you did involved defining the entity that represents the player's ship. Notice that your game creates a single instance of the entity. It is possible to create multiple instances of an entity. (See the section on enemy ships, below.)

Examine the galaga_startup.py script to see how you created the entity. The line -

ent = entityServer.createEntity('playerShip')

creates one instance of an entity called playerShip, and catches it in the script object ent. The lines -

varHandle_pos = varServer.find_varHandle('pos')
varHandle_rot = varServer.find_varHandle('rot')

tell the variable server the names of a couple of game variables associated with the ent entity (and/or the camera, later in the script). The variable server returns "handles" that can be used as arguments to OO methods in the script, and the script saves the handles in its own objects varHandle_pos and varHandle_rot. The line -

 var_pos = ent.find_var(varHandle_pos)

invokes ent's method to look up the actual value of its game variable named pos, and asigns that value to a script object named var_pos. The line -

 var_pos.set(Vec3f(0.0, 5, 45.00))

invokes var_pos's set method to set the game variable pos to a specific value, i.e. the entity's position in the 3D world model.

Defining an Entity

You defined the playerShip entity type using Entropy's EVA system, which automatically loads your definitions at startup. Without your definitions, galaga_startup.py would not know what a 'playerShip' is.

The entity type was defined in playerShip_ent.yaml. In that file you told it -

  • what more generic entity (or entities) it should inherit properties from (m_inheritsFrom)
  • what additional aspects should be associated with it (m_aspect)
  • what additional game variables are associated with it (m_var).

In this case, playerShip inherits the properties of ship (ship_ent.yaml), which in turn inherits the properties of unit (ship_ent.yaml). From the generic unit entity, playerShip inherits the properties of the pre-defined visual and physics entities. The generic ship entity inherits all that, and adds the predefined HP aspect (for managing hit points) and some variables for manipulating the hit points and properties relevant to the physics modeling. Finally, the playerShip entity inherits all that, and adds a new PlayerShip aspect (which you defined), plus a variable for tracking the location of the ship in the world model (pos) and a variable to identify what model to use for rendering the ship (mesh).

Notice that the definition of the game variable pos in playerShip_ent.yaml is what makes a variable of that name available to galaga_startup.py. If you had created more than one playerShip entity, each of them would have a variable with this name. That is why galaga_startup.py had to do the lookups to find the pos variable for the ent object -- it's not just a single global variable that can be referenced directly.

Defining an Aspect

The entity definition in playerShip_ent.yaml associates playerShip entities with an aspect named PlayerShip. Since that is not a predefined aspect, you defined it in playerShip.h and playerShip.cc. The .h file has some boilerplate definitions, to which you added several variable definitions. The .cc file has some boilerplate code, including empty definitions of three methods which you had to fill out:

  • on_attach - what to do when an entity that utilizes this aspect is created
  • on_entityDeath - what to do when an entity that utilizes this aspect is destroyed
  • on_entityTick - what to do about an entity that utilizes this aspect during each 'tick' of the game clock

The only thing on_attach does in this example is register several variables used by the aspect (and defined in the .h file) with the input server, so that their values can be shared between this compiled code and other components of the Entropy engine. Notice that the mechanism is completely different from the way pos was defined in playerShip_ent.yaml and manipulated in galaga_startup.py. The aspect variables must be defined both in the aspect's compiled code and in galaga_var.yaml, after which they can be manipulated within the aspect (and remotely via the keyboard, per below).

The on_entityDeath method unregisters those same variables from the input server.

The on_entityTick method is invoked by the game engine at regular intervals as the game runs. The interval is defined as a constant in playerShip.h, but more complex arrangements are also possible. (See the comments there.) Whever it is invoked, it looks up the values of a couple of the variables registered earlier by the on_attach method, does some calculations with those values, and updates the values of a couple of the other registered variables. The values are then used by other parts of the game engine.

  • In particular, the force variable is used by the physics engine. When you play it looks like pressing a key simply moves the ship a bit, but it is actually simulating what would happen if you applied a force for as long as you hold the key down. To see what is happening better, edit ship_ent.yaml and reduce mass and linearDamping to 1/10th of their current values. Now you will see the ship accelerate while you hold the key down, but start slowing down as soon as you release it. If you set linearDamping to zero it will not slow down at all when you let off the key. (Though you will see it bounce when it hits the invisible edge of the playing area.) These parameters let you model a ship in water realistically; higher mass makes it slower to accelerate (for a given force), and linear damping simulates the friction of the water, which brings a ship to a halt when you quit pushing with the engines.

Defining Keyboard I/O

Entropy manages keyboard input with a module named OIS. You defined bindings for various keypresses in galaga_keymapping.yaml. The key bindings directly manipulate game variables defined in galaga_var.yaml. The changed values are made available to aspects as described above.

It this example the various keypresses either change the value of the x or z coordinate of a 3D vector (player_movement), or else toggle the value of the boolean player_firing variable. Notice that the neutral values are restored when the key is released, so a keypress will have no effect unless the aspect is 'ticked' while the key is down.

If you have been working through the CS 381 tutorials, you made the camera move by replacing player_movement with camera_movement in the keymapping file. This worked without further changes because the necessary definitions are already coded into entropy/data/common/camera_ent.yaml and entropy/core/camera/camera.cc.

More Information about Variables, Entities, Aspects, and Key Mappings

If you capture a stdout.log as described above, you will see that during initialization Entropy lists all the registered entities, variables, aspects, and events, plus the key mappings. For the entities, it also lists the associated aspects and variables, along with the initial values. This can be useful for determining where a game variable gets used. E.g., the linearDamping mentioned above shows up for the physics aspect (where it is initialized to 1.0), and also for the ship aspect, which inherited the variable from physics but provided a new initial value for it.

This information can be helpful for troubleshooting (e.g., to discover that you misspelled an identifier), or for basic game information, by tracing through the inheritances to find out what variables and aspects an entity inherits and what their initial values are.

You can find a number of predefined entities/variables/aspects as follows:

  • entities - entropy/data/common/*_ent.yaml
  • variables - entropy/data/common/*_var.yaml
  • aspects - entropy/core/aspect/

Create Enemy Fleet

  • Create an enemyShip entity type similar to your playerShip definition.
    • Inherit from ship.
    • Do not attach the PlayerShip aspect -- these will not be controlled by the keyboard.
    • Use the same mesh for now.
  • Edit galaga_startup.py to create the enemies when the game starts, by adding this code after the code that creates the player's ship:
   # Create a fleet of enemy ships:
   for i in range(11):
      ent           = entityServer.createEntity('enemyShip')
      var_pos       = ent.find_var(varHandle_pos)
      import random
      var_pos.set(Vec3f((i - 5) * 5.0, 5, random.uniform(-35, -45)))
      var_rot       = ent.find_var(varHandle_rot)
      var_rot.set(Quatf(1,0,0,0))

The quaternion sets the the enemy ships' rotation so that they face back toward the player. They are positioned randomly along the north side of the screen. (If you have been working the CS 381 tutorials, notice that Entropy places them in OGRE for you; you no longer edit ogreApplication.cc for this.)

Test Visible

Run entropy again, make sure the entities are created.

Thought Questions

These ships are not controlled by the keyboard. How do you make them move under their own control?

  • What if you wanted to make a moon that orbits a planet?
  • What if you wanted to use AI to control an entity's movements?

Advanced Notes

The tutorial basically ends here. The following are a few notes on how to make the ships shoot. For the complete implementation (including the EnemyShip aspect that controls the enemy ships), see the definitions in the original galaga/ directory.

  • If you deleted that directory, rename yours to galaga-per-tutorial/, and type svn update in the main entropy/ directory. That will restore the original galaga/ from the repository.

Create Bullets

  • Create a bullet entity type as before
    • Do not inherit from ship or unit
    • Inherit from visual and physics
    • mesh: cube
    • Attach a Projectile aspect, that we will create in a sec

Create Projectile Aspect

  • Create .h, .cc as before, its an aspect in the same directory.
    • Only need one variable, damage which can be a float.
    • We need an on_collision callback, which we can register to get called everytime this entity hits something
      • When we hit something, just reduce its hps and lets the physics bounce of as usual
      • Since I am making a weird galaga, I am not having bullets destroy themselves when they collide, this would be an easy call to parentEntity->die()
void Projectile::on_collision(EVA::Entity* otherEntity){
   float damage = var_damage.get();

   EVA::Var<float> var_other_hp;
   CONNECT_VAR_TO_WITH_NAME(var_other_hp, otherEntity, "hp");
   float other_hp = var_other_hp.get();
   other_hp -= damage;
   var_other_hp.set(other_hp);
}

Create Turret Aspect

The turret creates bullets, when a firing variable is set