Chapter 9. Making the Game Engine

The previous chapters have developed an excellent reusable body of code. Rather than copy this code from project to a project, it should be collected together in its own awesome engine library. It can then be shared by several game projects so that all changes and improvements happen in one place.

A New Game Engine Project

The game engine project will be unlike any of the projects we’ve created so far. The game engine doesn’t run by itself; instead, it is used by a separate game project. This means the game engine is more correctly referred to as a library rather than a program. As you learn more techniques, you can add more code to this library and tune it to your needs. Figure 9.1 shows how the library can be used by several projects at once.

Using the game engine library.

Figure 9.1. Using the game engine library.

Close any projects in Visual Studio and start a new project by going to File > New Project. This will bring up a dialog box, as shown in Figure 9.2. Choose the Class Library option. The game engine will be a library used by other projects. Next to the name, I’ve chosen simply “Engine,” but this is your game engine so name it whatever you like.

Create a class library project.

Figure 9.2. Create a class library project.

If you are using Visual Studio 2008 it’s important to remember to change the build type to ×86. This is because the DevIL libraries are currently only available for 32-bit projects. Right-click the project, choose Properties, and click the Build tab. On the Configuration drop-down box, choose All Configurations, then on the Platform Target drop-down box chose ×86. The settings can be seen in Figure 9.3. If your result doesn’t look like Figure 9.3, you’re already set up for 32 bit. In Visual Studio 2010, these settings should be handled automatically. If you do need to edit the build targets in 2010, then right-click the solution in the solution explorer and open the Properties window. In the Properties dialog, click on Configuration and then click Configuration Manager. In the Configuration Manager, you can add the ×86 platform and then select it for the engine project.

The next step is to add all the classes, tests, and references that make up the engine and then add the new project to source control. You can add the Tao references by clicking the browse tab of the Add References dialog and manually browsing to the dll files, which should be in the Tao framework install directory (with Vista and Windows 7 it will probably be C:Program Files (×86)TaoFrameworkin and for Windows XP it should be C:Program FilesTaoFrameworkin). Alternatively, you may find the libraries already exist under the Recent tab in Add References dialog.

Settings for a 32-bit project.

Figure 9.3. Settings for a 32-bit project.

The references required are

  • Tao framework OpenGL binding for .NET

  • Tao framework Tao.DevIL binding for .NET

  • Tao framework Windows Platform API binding for .NET

  • nuint.framework

  • System.Drawing

  • System.Windows.Forms

The core engine components are

  • Batch

  • CharacterData

  • CharacterSprite

  • Color

  • FastLoop

  • Font

  • FontParser

  • IGameObject

  • Input

  • Matrix

  • Point

  • PreciseTimer

  • Renderer

  • Sprite

  • StateSystem

  • Text

  • Texture

  • TextureManager

  • Tween

  • Vector

These core engine classes should all be set to public. If they are private, protected, or internal then any game using the engine library will not be able to access them. There may also be private, protected, or internal functions that now need to be made public so that they may be accessed from the library; change these to public as you encounter them.

As you develop games, you can extend the engine with useful code you create. The engine should never have any gameplay code for a specific game; the engine should be general enough to form the basis of many different games.

Extending the Game Engine

The classes we’ve created so far cover a good deal of what is desirable in a game engine. There are a few omissions: the input library could be better developed, there is no support for handling multiple textures, and there is no support for sound. These are all simple changes that will round out the engine.

You may notice that if you try to run the game engine project, you’ll receive this error: “A project with an Output Type of Class Library cannot be started directly.” Class libraries have no main function; they do not directly run code. Instead, other projects make use of the code they contain. That means if you want to test part of the engine you’ll need to make a new project that uses the engine.

Using the Game Engine in a Project

A new project needs to be created that will make use of the new engine class. This project will be used to run the engine code and test the various improvements we’ll be making.

Close the engine project in Visual Studio and start a new project by selecting File > New > Project. This time choose a Windows Form Application instead of a Class Library. This project is going to be used to test the engine library so call it “EngineTest.” Click OK and a new project will be generated.

This project now needs to load the engine library. Visual Studio uses two terms: Solution and Project. Each time we’ve started a new project, Visual Studio has created a solution to contain that project. The solution usually shares the same name as the main project; in this case, there is a solution called EngineTest that contains a project with the same name. This can be seen in the solution explorer shown in Figure 9.4.

A solution with one project.

Figure 9.4. A solution with one project.

A solution can contain several different projects. Our EngineTest solution needs to contain the engine project so that it can use the engine code. It’s very simple to add new projects. Right-click the EngineTest solution in the solution explorer; this will display a context menu as can be seen in Figure 9.5. Choose Add > Existing Project and the Add Existing Project dialog box will appear.

Adding a project to a solution.

Figure 9.5. Adding a project to a solution.

The dialog will provide a view of the project directory on your machine (see Figure 9.6). One of these subdirectories will contain your engine library. Visual Studio displays the solution and its projects using a directory structure. Open the Engine folder (if you named your game engine some other name, then find the corresponding folder). Inside this folder is another folder called Engine. (The first Engine folder is the solution and the second engine folder is the project.) Open the second Engine folder. Inside this folder is a file called Engine.csproj; this is the file that represents the C# program. Open this file.

List of Solution folders.

Figure 9.6. List of Solution folders.

The solution explorer will now have two projects: Engine, the class library, and EngineTest, the project that will use the engine library. The engine library project hasn’t been copied across to the EngineTest solution; it’s just a reference. All its code stays in the same place. This makes it easier to work with several projects using the engine library but without duplicating the code. The Engine code can be edited from any solution and all the changes will be shared with all projects using the library.

You may notice that the font for the EngineTest project is in bold. This is because when you run the solution, the EngineTest project will be executed. A solution might contain several projects that could generate executables, and the Solution Explorer needs to know which project you want to run and debug. You can make the Engine project the start-up project by right-clicking its name in the Solution Explorer and choosing Set as Start Up Project. The Engine project’s name will now turn bold, and the EngineTest project name will turn non-bold. Running the project will produce an error stating class libraries are not executable. Right-click the TestEngine project and set that as the start-up project again so you can once again run the solution. This functionality is useful if you also wanted to develop a level editor or other tool for your game. It could be added to the solution as an additional project that makes use of the engine and game project.

Engine and TestEngine exist in the same solution, but at the moment they do not have access to each other. TestEngine needs to access the code in Engine, but Engine should never need to know about the TestEngine project. In the Solution Explorer, expand the EngineTest project; beneath the project is a References folder, as shown in Figure 9.7.

The References folder.

Figure 9.7. The References folder.

Right-click the Folder icon and choose Add Reference from the context menu. This will bring up the dialog box that we’ve used before to add references to NUnit and the Tao libraries. The Add Reference dialog box has a number of tabs along its top; by default, the .NET tab is selected. The .NET tab lists all the references to .NET libraries that are installed on the computer. This time we want to include a reference to the Engine project. Click the Projects tab, as shown in Figure 9.8.

Adding a project as a reference.

Figure 9.8. Adding a project as a reference.

There is only one project; select it and press OK. Now the EngineTest project has a reference to the Engine project. It also needs the following .NET references added; System.Drawing, Tao.OpenGL, Tao.DevIL, and Tao.Platforms.Window. This is so the SimpleOpenGL control can be added to the form and OpenGL can be set up. Some of this could be moved into the engine library using a default set up class. The dll files for DevIL, ILU, and ILUT need to be copied to the bin/debug/ and bin/release/ directories of the EngineTest project.

Drop a SimpleOpenGL control on to the EngineTest form and set its dock property to fill, the same way that you have done for earlier projects. Now view the form.cs code.

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;

namespace EngineTest
{
  public partial class Form1 : Form
  {
    public Form1()
    {
     InitializeComponent();
    }
  }
}

This is default code created by Visual Studio for a Windows Form project. The using statements at the top refer to the different libraries the form is using. Now that we have an Engine project, a new using statement can be added.

using Engine;

This will provide access to all the classes in the engine library. The normal setup code also needs to be written. Here is the default setup code for a new game project.

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using Tao.OpenGl;
using Tao.DevIl;
using Engine;

namespace EngineTest
{
  public partial class Form1 : Form
  {
    bool            _fullscreen     = false;
    FastLoop        _fastLoop;
    StateSystem     _system         = new StateSystem();
    Input           _input          = new Input();
    TextureManager  _textureManager = new TextureManager();

    public Form1()
    {
      InitializeComponent();
      simpleOpenGlControl1.InitializeContexts();

      InitializeDisplay();
      InitializeTextures();
      InitializeGameState();

      _fastLoop = new FastLoop(GameLoop);
    }

    private void InitializeGameState()
    {
      //Load the game states here
    }

    private void InitializeTextures()
    {
      //Init DevIl
      Il.ilInit();
      Ilu.iluInit();
      Ilut.ilutInit();
      Ilut.ilutRenderer(Ilut.ILUT_OPENGL);

      //Load textures here using the texture manager.
    }

    private void UpdateInput()
    {
      System.Drawing.Point mousePos = Cursor.Position;
      mousePos = simpleOpenGlControl1.PointToClient(mousePos);

      //Now use our point definition,
      Engine.Point adjustedMousePoint = new Engine.Point();
      adjustedMousePoint.X = (float)mousePos.X - ((float)ClientSize.
        Width / 2);
      adjustedMousePoint.Y = ((float)ClientSize.Height / 2) - (float)
        mousePos.Y;
      _input.MousePosition = adjustedMousePoint;
    }

    private void GameLoop(double elapsedTime)
    {
      UpdateInput();
      _system.Update(elapsedTime);
      _system.Render();
      simpleOpenGlControl1.Refresh();
    }

    private void InitializeDisplay()
    {
      if (_fullscreen)
      {
        FormBorderStyle = FormBorderStyle.None;
        WindowState = FormWindowState.Maximized;
      }
      else
      {
        ClientSize = new Size(1280, 720);
      }
      Setup2DGraphics(ClientSize.Width, ClientSize.Height);
    }

    protected override void OnClientSizeChanged(EventArgs e)
    {
      base.OnClientSizeChanged(e);
      Gl.glViewport(0, 0, this.ClientSize.Width, this.ClientSize.
        Height);
      Setup2DGraphics(ClientSize.Width, ClientSize.Height);
    }

    private void Setup2DGraphics(double width, double height)
    {
      double halfWidth = width / 2;
      double halfHeight = height / 2;
      Gl.glMatrixMode(Gl.GL_PROJECTION);
      Gl.glLoadIdentity();
      Gl.glOrtho(-halfWidth, halfWidth, -halfHeight, halfHeight,
        -100, 100);
      Gl.glMatrixMode(Gl.GL_MODELVIEW);
      Gl.glLoadIdentity();
    }
  }
}

This same type of setup code can be used to start any number of projects, so it’s worth keeping it as a file somewhere or refactoring it into the engine. The UpdateInput code has had to change from our earlier examples. There are two classes with the same name—Point. One Point is from System.Drawing and the other is from our Engine library. To let the compiler know which point we mean, the fully qualified name is used; for example:

// Using the fully qualified name removes potential confusion.
System.Drawing.Point point = new System.Drawing.Point();
Engine.Point point = new Engine.Point();

Running this test program will produce a blank screen, which means that everything is working as expected. This project can now be used to run any engine tests required. Extra game states can be added as required. The engine currently doesn’t support multiple textures very well so that will be the next change.

Multiple Textures

The engine library currently handles sprites with differing textures poorly. To demonstrate how it handles different textures, create a new game state called MultipleTexturesState. As with earlier states, this should implement IGameObject and be loaded using the StateSystem. This state will be testing the texture system; therefore, it will also need to take a TextureManager object in its constructor. The loading code can be placed in the InitializeGameState method in form.cs.

private void InitializeGameState()
{
  //Load the game states here
  _system.AddState("texture_test", new MultipleTexturesState
(_textureManager));

  _system.ChangeState("texture_test");
}

This new state will need to create sprites with different textures. Two new textures are provided on the CD in the Assets directory: spaceship.tga and spaceship2.tga. These are large low detail sprites of two different spaceships. Add these .tga files to the solution and set the properties so that they will be copied into the build directory.

The new textures need to be loaded into the TextureManager; this is done in the IntializeTextures method.

//Load textures here using the texture manager.
_textureManager.LoadTexture("spaceship", "spaceship.tga");
_textureManager.LoadTexture("spaceship2", "spaceship2.tga");

Now that the textures are loaded, they can be used in the MultipleTexturesState.

class MultipleTexturesState : IGameObject
{
  Sprite _spaceship1 = new Sprite();
  Sprite _spaceship2 = new Sprite();
  Renderer _renderer = new Renderer();

  public MultipleTexturesState(TextureManager textureManager)
  {
    _spaceship1.Texture = textureManager.Get("spaceship");
    _spaceship2.Texture = textureManager.Get("spaceship2");

    // Move the first spaceship, so they're not overlapping.
    _spaceship1.SetPosition(-300, 0);
  }

  public void Update(double elapsedTime) {}

  public void Render()
  {
    _renderer.DrawSprite(_spaceship1);
    _renderer.DrawSprite(_spaceship2);
    _renderer.Render();
  }
}

The state creates two sprites, one for each of the spaceship textures. The first spaceship is moved back 300 pixels on the X axis. This prevents the spaceships from overlapping. Run this state and you will see something similar to Figure 9.9.

Incorrect texturing.

Figure 9.9. Incorrect texturing.

In Figure 9.9, the second spaceship is drawn correctly, but the first one has the wrong texture. The first spaceship’s sprite is the correct dimensions for its texture so it seems squashed compared to the second spaceship sprite, which has both correct dimensions and the correct texture.

The problem here is that the OpenGL is never being told when the texture changes. If it doesn’t know, it uses whatever texture was set last. In the game loop a lot of texture changing per frame can slow things down, but a moderate amount of texture changing is nothing to worry about and indeed is essential for most games.

The class that needs to be modified is the Renderer class. The Renderer class batches up all the sprites so that they can be sent to the graphics card all at once. At the moment the Renderer class doesn’t check which texture a sprite uses. It needs to check to see if it’s drawing something with a new texture. If it is, then it can tell OpenGL to draw whatever has been batched so far with the old texture and then start a new batch using this new texture.

Here’s the current Renderer.DrawSprite code.

public void DrawSprite(Sprite sprite)
{
 _batch.AddSprite(sprite);
}

The extra logic transforms it to this code.

int _currentTextureId = -1;
public void DrawSprite(Sprite sprite)
{
  if (sprite.Texture.Id == _currentTextureId)
  {
    _batch.AddSprite(sprite);
  }
  else
  {
    _batch.Draw(); // Draw all with current texture

    // Update texture info
    _currentTextureId = sprite.Texture.Id;
    Gl.glBindTexture(Gl.GL_TEXTURE_2D, _currentTextureId);
    _batch.AddSprite(sprite);
  }
}

Rerun the project and you should see something similar to Figure 9.10.

Correct texturing.

Figure 9.10. Correct texturing.

The code isn’t quite as simple as before, but it now allows the engine to support sprites with different textures. You might be thinking about the worst case situation for this code; every single sprite that’s drawn may force a texture change. The way to avoid this is to sort out the things being drawn and ensure that the maximum numbers of vertices that are using the same texture are sent together. There is another way to minimize the number of texture changes. Instead of using lots of separate small textures, textures are grouped together and combined into a bigger texture; this is often referred to as a texture atlas. Sprites can then reference this big texture but change the U,V coordinates of their vertices so they only use a small part of it (this is how the font rendering works). Generally, if it’s not a problem, then don’t try to fix it!

Adding Sound Support

Sound is very easy to ignore, but it can change the feeling of how a game plays. Sound is also very easy to add using the excellent OpenAL library. Before worrying about the details, think about how a very simple sound library interface might work using a code sketch.

SoundManager soundManager = new SoundManager();
soundManager.Add("zap", "zap.wav");
Sound zapSound = soundManager.Play("zap")
if (soundManager.IsSoundPlaying("zap"))
{
    soundManager.StopPlaying(zapSound);
}

This code indicates that a sound library should manage sounds in a similar way to the texture manager. When a sound is played, a reference will be returned. The reference can be used to control the sound; check if it’s playing with the IsSoundPlaying method or stop it playing with the StopPlaying method. It would also be nice to have looping sounds and a volume control.

Creating Sound Files

To test the new sound code, some sounds will be needed. Broadly speaking, games have two categories of sound: sound effects like a gun shooting and background music or ambient sound.

A great way to generate sound effects is to use the hard to pronounce sfxr program created by Tomas Pettersson. It’s available on the CD and be can seen in Figure 9.11.

Creating sound effects with sfxr.

Figure 9.11. Creating sound effects with sfxr.

Sfxr is a program that randomly generates sound effects for games. The sound effects sound a little like those that might have been generated by NES consoles. Keep clicking the Randomize button and new sounds will be generated. Once you’ve found a sound you like, it can be exported as a wave file. I’ve included two generated sound effects called soundeffect1.wav and soundeffect2.wav in the Assets directory on the CD. These should be added to the TestEngine project in the same way the textures were added. Sfxr sounds are great for retro-style games or placeholder sounds.

Developing a SoundManager

OpenAL is a professional-level library used by many modern games, so it has a wide range of support for more advanced sound effects, such as playing sounds from a 3D position. It also has functions to generate the Doppler Effect.

A common example of the Doppler Effect is a police car or ambulance driving past—as the vehicle passes, the frequency of the siren changes. Because the game we’ll be making will be 2D, we won’t use this functionality, but it can be very useful. Dig around the documentation and experiment with the library to get the full benefit from it.

To start using OpenAL the reference needs to be added to the engine library project. It’s called “Tao Framework Tao.OpenAl Binding for .NET.” If it is not listed under the .NET tab of the Add Reference dialog then choose the Browse tab and navigate to the Tao framework install directory TaoFrameworkin and choose it from there. OpenAL also requires alut.dll, ILU.dll, and OpenAL32.dll to be copied to the bin/debug and bin/release directories. These dll files can be found in your Tao Framework install directory, TaoFrameworklib. A skeleton class can then be created based on the simple idealized API and how the TextureManager works. First, a sound class needs to be made. This class will represent a sound being played.

public class Sound
{
}

This sound class can then be used to build up the sound manager skeleton class. Remember these classes should be added to the engine project because this code can be used by many games.

public class SoundManager
{
  public SoundManager()
  {
  }

  public void LoadSound(string soundId, string path)
  {
  }

  public Sound PlaySound(string soundId)
  {
    return null;
  }

  public bool IsSoundPlaying(Sound sound)
  {
    return false;
  }

  public void StopSound(Sound sound)
  {
  }
}

Sound hardware can only play a limited number of sounds at the same time. The number of sounds that can be played is known as the number of channels. Modern sound hardware can often play up to 256 sounds at the same time. The OpenAL way to discover how many sound channels are available is to keep requesting channels and when the hardware can’t give any more, then you have the maximum number.

readonly int MaxSoundChannels = 256;
List <int> _soundChannels = new List<int>();

public SoundManager()
{
  Alut.alutInit();
  DicoverSoundChannels();
}

private void DicoverSoundChannels()
{
  while (_soundChannels.Count < MaxSoundChannels)
  {
    int src;
    Al.alGenSources(1, out src);
    if (Al.alGetError()== Al.AL_NO_ERROR)
    {
      _soundChannels.Add(src);
    }
    else
    {
      break; // there's been an error - we've filled all the channels.
    }
  }
}

Each sound channel is represented by a number so the available sound channels can simply be stored as a list of integers. The SoundManager constructor initializes OpenAL and then discovers all the sound channels up to a maximum of 256. The OpenAL function alGenSources generates a sound source that reserves one of the channels. The code then checks for errors; if an error is detected that means that OpenAL is unable to generate anymore sound sources so it’s time to break out of the loop.

Now that sound sources are being discovered, it’s time to start adding this to the EngineTest project. The sound manager should exist in the form.cs class and then be passed through to any state that wants to use it.

public partial class Form1 : Form
{
  bool           _fullscreen     = false;
  FastLoop       _fastLoop;
  StateSystem    _system         = new StateSystem();
  Input          _input          = new Input();
  TextureManager _textureManager = new TextureManager();
  SoundManager   _soundManager   = new SoundManager();

The last line creates the sound manager object and it will discover the number of sound channels available. The next step is to load files from the hard disk into memory. A new structure needs to be created to hold the data for the sound files loaded from the disk.

public class SoundManager
{
  struct SoundSource
  {
    public SoundSource(int bufferId, string filePath)
    {
      _bufferId = bufferId;
      _filePath = filePath;
    }
    public int _bufferId;
    string _filePath;
  }
  Dictionary<string, SoundSource> _soundIdentifier = new
    Dictionary<string, SoundSource>();

The SoundSource structure stores information about the loaded sounds. It will only ever be used internally by the sound manager so the structure exists inside the SoundManager class. When sound data is loaded in OpenAL an integer will be returned; this is used to keep a reference of the sound for when it needs to be played. The sound identifier maps a sound’s name onto the sound data in memory. The dictionary will be a big table of all the sounds available to the game with an easy English language string to identify each one.

To load a sound file the using System.IO; statement needs to added to the top of the file. The LoadSound function will fill up the _soundIdentifier dictionary with game sounds.

public void LoadSound(string soundId, string path)
{
  //Generate a buffer.
  int buffer = -1;
  Al.alGenBuffers(1, out buffer);

  int errorCode = Al.alGetError();
  System.Diagnostics.Debug.Assert(errorCode == Al.AL_NO_ERROR);

  int format;
  float frequency;
  int size;
  System.Diagnostics.Debug.Assert(File.Exists(path));
  IntPtr data = Alut.alutLoadMemoryFromFile(path, out format, out size,
    out frequency);
  System.Diagnostics.Debug.Assert(data != IntPtr.Zero);
  // Load wav data into the generated buffer.
  Al.alBufferData(buffer, format, data, size, (int)frequency);
  // Everything seems ok, add it to the library.
  _soundIdentifier.Add(soundId, new SoundSource(buffer, path));
}

The LoadSound method first generates a buffer. This buffer is an area in memory where the sound data from the disk will be stored. The OpenAL utility function alutLoadMemoryFromFile is used to read the data for a .wav into memory. The memory containing the file data is then put into the buffer along with the format, size, and frequency data that was also read in. This buffer is then put into our dictionary using the soundId to identify it later.

The sounds added to the project can now be loaded up using the sound manager.

public Form1()
{
  InitializeComponent();
  simpleOpenGlControl1.InitializeContexts();

  InitializeDisplay();
  InitializeSounds();       //Added
  InitializeTextures();
  InitializeGameState();

The InitializeSound method will be responsible for loading in all the sound files.

private void InitializeSounds()
{
  _soundManager.LoadSound("effect", "soundEffect1.wav");
  _soundManager.LoadSound("effect2", "soundEffect2.wav");
}

To test the sound manager, we really need some code that will let these sounds be played. When OpenAL plays a sound, it returns an integer that is used to reference the sound being played. For our sound manager we are going to wrap that integer up in the Sound class; this gives it more context in the code. Instead of just being a random integer, it’s now a typed sound object. If there is an error playing the sound, then minus one will be returned. Here’s the Sound class that will wrap up OpenAL’s reference integer.

public class Sound
{
  public int Channel { get; set; }

  public bool FailedToPlay
  {
    get
    {
     // minus is an error state.
     return (Channel == -1);
    }
  }
  public Sound(int channel)
  {
    Channel = channel;
  }
}

Now that the sound wrapper class is properly defined a PlaySound method can be added. A sound has to be played on a channel. There are only a limited number of channels so it is possible that all the channels are full. In this case the sound won’t be played. This is a very simple heuristic, but for games with hundreds of sounds going off at once, it may be better to give each sound a priority and then the sounds of high priority can take over the channels of those sounds with a lower priority. The priority value would probably be linked to the sound’s volume and distance from the player.

A method needs to be written that will determine if a given channel is free or not. An easy way to determine if a channel is free is to ask OpenAL if it is currently playing anything on that channel. If nothing is being played on the channel then it is free. This new IsChannelPlaying method should be added to the SoundManager class.

private bool IsChannelPlaying(int channel)
{
  int value = 0;
  Al.alGetSourcei(channel, Al.AL_SOURCE_STATE, out value);
  return (value == Al.AL_PLAYING);
}

The OpenAL function alGetSourcei queries a particular channel about some property. The property is determined by the second argument; in this case we’re asking what the current state of the source is. The size and speed of the sound on the channel can also be queried in this way. The IsChannelPlaying function checks to see if the channel’s current state is set to playing; if so it returns true, otherwise false.

With the IsChannelPlaying function defined we can now use it to build up another function that will return a free channel. This new function will be called GetNextFreeChannel and will iterate through the list of channels that the function DicoverSoundChannels made in the constructor. If it can’t find a sound channel free it will return minus one as an error flag.

private int FindNextFreeChannel()
{
  foreach (int slot in _soundChannels)
  {
    if (!IsChannelPlaying(slot))
    {
     return slot;
    }
  }

  return -1;
}

This function finds a free channel for our sounds to be played on. This makes it easy to write a robust PlaySound method that will be able to deal with a limited number of sound channels.

public Sound PlaySound(string soundId)
{
  //Default play sound doesn't loop.
  return PlaySound(soundId, false);
}
public Sound PlaySound(string soundId, bool loop)
{
  int channel = FindNextFreeChannel();
  if (channel != -1)
  {
    Al.alSourceStop(channel);
    Al.alSourcei(channel, Al.AL_BUFFER, _soundIdentifier[soundId].
     _bufferId);
    Al.alSourcef(channel, Al.AL_PITCH, 1.0f);
    Al.alSourcef(channel, Al.AL_GAIN, 1.0f);

    if (loop)
    {
     Al.alSourcei(channel, Al.AL_LOOPING, 1);
    }
    else
    {
     Al.alSourcei(channel, Al.AL_LOOPING, 0);
    }
    Al.alSourcePlay(channel);
    return new Sound(channel);
  }
  else
  {
    //Error sound
    return new Sound(-1);
  }
}

There are two PlaySound methods implemented here; one takes a flag to indicate if the sound should be looped, the other takes one less parameter and assumes the sound should only be played once by default. The PlaySound method finds a free channel and loads the buffered data from the SoundSource on to the channel. It then resets the default properties on the sound channel, including the pitch and gain and the looping flag is set. This determines if the sound will finish after being played once or if it just repeats. Finally it’s told to play the sound and a Sound object is returned.

The sound system is now working well enough to test. It can load files from the disk and play them as needed. Here is a new test state that will demonstrate the functionality.

class SoundTestState : IGameObject
{
  SoundManager _soundManager;
  double _count = 3;

  public SoundTestState(SoundManager soundManager)
  {
    _soundManager = soundManager;
  }

  public void Render()
  {
    //The sound test doesn't need to render anything.
  }

  public void Update(double elapsedTime)
  {
    _count -= elapsedTime;
    if (_count < 0)
    {
      _count = 3;
      _soundManager.PlaySound("effect");
    }
  }
}

Load it in the normal way and make sure it’s the default state being run. Every 3 seconds it will play the first sound effect. Feel free to change the numbers around or play both effects at once.

This code snippet plays both sounds at the same time, demonstrating that the SoundManager uses hardware channels correctly.

public void Update(double elapsedTime)
{
  _count -= elapsedTime;

  if (_count < 0)
  {
    _count = 3;
    _soundManager.PlaySound("effect");
    _soundManager.PlaySound("effect2");
  }
}

The SoundManager class needs a few final functions to make it more complete. It needs to be able test if a sound is playing and also to stop a sound. Volume control would also be useful.

public bool IsSoundPlaying(Sound sound)
{
  return IsChannelPlaying(sound.Channel);
}
public void StopSound(Sound sound)
{
  if (sound.Channel == -1)
  {
    return;
  }
  Al.alSourceStop(sound.Channel);
}

The check to see if a sound is playing reuses the code that checks if a sound is playing on a particular channel. The stop sound function uses an OpenAL method to stop the sound on a certain channel. These new functions can be tested in the sound state again.

public void Update(double elapsedTime)
{
  _count -= elapsedTime;

  if (_count < 0)
  {
    _count = 3;
    Sound soundOne = _soundManager.PlaySound("effect");
    Sound soundTwo = _soundManager.PlaySound("effect2");

    if (_soundManager.IsSoundPlaying(soundOne))
    {
     _soundManager.StopSound(soundOne);
    }
  }
}

Here the first sound is told to play, then immediately after there’s a check to see if it is playing. This returns true and another call stops the sound. The first is never heard as it’s always stopped by the end of the game loop.

float _masterVolume = 1.0f;
public void MasterVolume(float value)
{
  _masterVolume = value;
  foreach (int channel in _soundChannels)
  {
    Al.alSourcef(channel, Al.AL_GAIN, value);
  }
}

In OpenAL the volume can be altered by specifying the gain on a channel. The gain goes from 0 to 1. Here the master volume is set for every channel. The volume is also stored in a class variable. When a new sound is played it will overwrite the current gain setting for the channel so it needs to be reapplied. This can be done at the bottom of the PlaySound method.

{
  Al.alSourcef(channel, Al.AL_GAIN, _masterVolume);
  Al.alSourcePlay(channel);
  return new Sound(channel);
}

This now gives a master volume control for all the sound channels. The volume can also be set per channel; this is useful when fading out music or making one sound effect more or less noticeable.

public void ChangeVolume(Sound sound, float value)
{
  Al.alSourcef(sound.Channel, Al.AL_GAIN, _masterVolume * value);
}

Here the volume of a particular sound is scaled by the master volume; this ensures that if the player sets his master volume to low setting then a new sound isn’t going to suddenly be very loud. The value should be between 0 and 1. Here’s some more test code that shows these volume changes.

public SoundTestState(SoundManager soundManager)
{
  _soundManager = soundManager;
  _soundManager.MasterVolume(0.1f);
}

This will set the volume to be one tenth its previous value. If you run the test state again the difference should be immediately noticeable. The final task for the sound manager is to make sure it closes down correctly. It creates a lot of references to sound files and data and it needs to free these when it is destroyed. The best way to do this is to implement the IDisposable interface.

public class SoundManager : IDisposable
public void Dispose()
{
  foreach (SoundSource soundSource in _soundIdentifier.Values)
  {
    SoundSource temp = soundSource;
    Al.alDeleteBuffers(1, ref temp._bufferId);
  }
  _soundIdentifier.Clear();
  foreach (int slot in _soundChannels)

  {
    int target = _soundChannels[slot];
    Al.alDeleteSources(1, ref target);
  }
  Alut.alutExit();
}

This function goes through all the sound buffers and frees them and then through all the sound card channels and frees those too.

Improving Input

The engine has very limited input support at the moment. The input class can be queried to find the mouse cursor position, but there is no other support. The ideal input for arcade-style games is a gamepad—a purpose-built piece of hardware to play games. First-person shooter games, strategy, and simulation games tend to work best with a mouse and keyboard combination. For this reason the mouse should be more fully supported by the engine and the state of keyboard should also be queryable.

Wrapping Game Controllers

For arcade games there’s no better controller than a gamepad. PCs have always been behind the curve when compared to console controllers, but recent console controllers have begun to use USB connections. This makes it very easy to add support for console controllers on the PC. These controllers also tend to be the most popular for users as they’re high quality, widely supported, and very easy to find.

Controller support in the TaoFramework is good but not excellent. There is support for control sticks and analog buttons but no support for haptic effects like rumble or more exotic controllers like the Wiimote. External C# libraries for these newer controls do exist if they tickle your interest but they will require some research to use.

The controller support in this engine will be limited to the more standard controllers. For PC gamers the most popular controller is likely to be the Xbox 360 controller because it’s immediately recognized by Windows and has a large selection of buttons and control sticks. The gamepad code will be flexible enough that any standard joypad could be used, but for testing purposes, it’s the Xbox 360 pad that will be targeted. It can be seen in Figure 9.12.

Xbox 360 controller.

Figure 9.12. Xbox 360 controller.

The Xbox 360 controller has a few different types of controls. It has two control sticks on the front. We’ll want to be able to get a value from –1 to 1 on the X and Y axis to determine where the player moves the control stick. The D-pad supports basic left, right, up, and down commands. The rest of the controls on the face of the controller are made from simple buttons: Back, Start, A, B, X, and Y. These are quite simple with only two states, pressed and not pressed, which can be represented by a boolean variable. On the top of the controller are four shoulder buttons, the buttons nearest the face of the controller are simple buttons, but the back two buttons are triggers. The triggers are special because they are not digital on/off buttons; they have a range of values from 0 to 1, depending on how hard they are pressed. All these controls together make quite a complicated gamepad, and a number of classes will be needed to describe the different functionality of each type of control.

To begin, a new game test state needs to be made. Call this state InputTestState and make it the default loaded state in the normal way. The gamepad wrappings exist in the SDL section of the Tao Framework; this means another reference needs to be added—Tao framework SDL Binding for .NET (if you don’t have this reference then select the Browse tab, navigate to the TaoFrameworklib directory, and choose Tao.Sdl.dll). Another dll also needs to be copied to the bin directories—sdl.dll. We’ll go through the various controls one by one and then finally build up a fully supported controller. If you’re not using an Xbox 360 controller, you should still be able to follow this section with any other controller. Just substitute the controls used where necessary.

The controller is the main way that the player interacts with the game. For that reason we always want to know the current state of the controller. Every frame will include code that asks the controller the state of all its controls, and we’ll update our representation in memory. The constant querying of a device is sometimes known as polling.

The SDL library requires its joypad subsystem to be initiated before it can be used. For now this code can be placed in the InputTestState, but it will eventually need to be moved to the input class.

bool _useJoystick = false;
public InputTestState()
{
  Sdl.SDL_InitSubSystem(Sdl.SDL_INIT_JOYSTICK);
  if (Sdl.SDL_NumJoysticks() > 0)
  {
    //Start using the joystick code
    _useJoystick = true;
  }
}

The setup code is quite readable. The joystick subsystem of SDL is initiated, and if it works, then we can use joysticks. Next in the engine library project, a class needs to be made to represent the controller. Because a very specific controller is going to be represented, I’m going to call this class XboxController.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Tao.Sdl;

namespace Engine
{
  public class XboxController : IDisposable
  {
    IntPtr _joystick;
    public XboxController(int player)
    {
     _joystick = Sdl.SDL_JoystickOpen(player);
    }

    #region IDisposable Members

    public void Dispose()
    {
     Sdl.SDL_JoystickClose(_joystick);
    }

    #endregion
  }
}

This code creates the joystick using a player index. You might want to create a game that supports two or more players; in this case, you’d need to create a controller for each player. The joystick is also disposable so it will release its reference to the controller once it’s destroyed.

The controller can now be created in the test state.

XboxController _controller;
public InputTestState()
{
  Sdl.SDL_InitSubSystem(Sdl.SDL_INIT_JOYSTICK);
  if (Sdl.SDL_NumJoysticks() > 0)
  {
    //Start using the joystick code
    _useJoystick = true;
    _controller = new XboxController(0);
  }
}

The first type of control we’re going to wrap is the control stick. Control sticks are great for moving the character and positioning the camera. Control sticks aren’t made perfectly. They will often report that they’re being pushed even when they are centered and the controller is resting on the desk. The solution to this problem is to ignore all output from the control stick unless it’s pushed over a certain threshold. The part that is ignored is known as the dead zone and can vary from controller to controller (for this reason it’s best to be a little generous when specifying your dead zone).

The control stick is treated as two axes—an X axis from left to right and a Y axis from top to bottom. SDL returns the axis information as short number value but a more convenient representation would be a float from –1 to 1. The –1 value is the control stick pushed far to the left (or down) and 1 would be fully pushed in the opposite direction.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Tao.Sdl;

namespace Engine
{
  public class ControlStick
  {
    IntPtr _joystick;
    int _axisIdX = 0;
    int _axisIdY = 0;
    float _deadZone = 0.2f;

    public float X { get; private set; }
    public float Y { get; private set; }

    public ControlStick(IntPtr joystick, int axisIdX, int axisIdY)
    {
     _joystick = joystick;
     _axisIdX = axisIdX;
     _axisIdY = axisIdY;
    }

    public void Update()
    {
     X = MapMinusOneToOne(Sdl.SDL_JoystickGetAxis(_joystick,
       _axisIdX));
     Y = MapMinusOneToOne(Sdl.SDL_JoystickGetAxis(_joystick,
       _axisIdY));
    }

    private float MapMinusOneToOne(short value)
    {
     float output = ((float)value / short.MaxValue);

     // Be careful of rounding error
     output = Math.Min(output, 1.0f);
     output = Math.Max(output, -1.0f);

     if (Math.Abs(output) < _deadZone)
     {
       output = 0;
     }

     return output;
    }
  }

}

SDL represents analog controls using axes that can be polled with SDL_JoystickGetAxis. The control sticks are made from two axes. A controller might have a number of different axes, so in the constructor, two indices are passed in to identify which axis we want this control stick to represent. These identifying numbers will change for each type of controller. The numbers that represent the different controls on the gamepad aren’t guaranteed to be the same for every type of gamepad. One gamepad’s left control stick might have the index one, but another type of gamepad might index the left control stick with the index five. For this reason, it’s often a good idea to allow the player to remap his controls.

The Update method is called once per frame, and it updates the X and Y values with values from –1 to 1, depending on the position of the stick. There’s also a little buffer for the dead zone that ignores small movements of the control stick.

The Xbox controller has two control sticks so the controller class can now be updated to represent this.

public ControlStick LeftControlStick { get; private set; }
public ControlStick RightControlStick { get; private set; }
public XboxController(int player)
{
  _joystick = Sdl.SDL_JoystickOpen(player);
  LeftControlStick = new ControlStick(_joystick, 0, 1);
  RightControlStick = new ControlStick(_joystick, 4, 3);
}

public void Update()
{
  LeftControlStick.Update();
  RightControlStick.Update();
}

Now that there are some controls on the controller, it can be used in the test state to move things around. The Update function of InputTestState updates the SDL joystick system and then updates the controller, updating all of its control values.

public void Update(double elapsedTime)
{
  if (_useJoystick == false)
  {
    return;
  }

  Sdl.SDL_JoystickUpdate();
  _controller.Update();
}

public void Render()
{
  if (_useJoystick == false)
  {
    return;
  }
  Gl.glDisable(Gl.GL_TEXTURE_2D);
  Gl.glClearColor(1, 1, 1, 0);

  Gl.glClear(Gl.GL_COLOR_BUFFER_BIT);
  Gl.glPointSize(10.0f);
  Gl.glBegin(Gl.GL_POINTS);
  {
    Gl.glColor3f(1, 0, 0);
    Gl.glVertex2f(
     _controller.LeftControlStick.X * 300,
     _controller.LeftControlStick.Y * -300);
    Gl.glColor3f(0, 1, 0);
    Gl.glVertex2f(
     _controller.RightControlStick.X * 300,
     _controller.RightControlStick.Y * -300);
  }
  Gl.glEnd();
}

The Render function draws a white background and a green and red dot representing each of the control sticks. The point size is increased and texture mode is disabled to make the dots more visible. Run the program and move the dots around the screen. The control stick’s values only go from –1 to 1, which isn’t a large enough number to visually move the dots around the screen; therefore, the value is multiplied by 300. The Y axis is multiplied by –300 to invert it; try removing the minus sign and see which control scheme you prefer.

The next control to wrap is the button. There are actually ten buttons on the Xbox 360 controller. The X, Y, A, B buttons, start and back, the two shoulder buttons, and pushing in the two control sticks. As before, add the following control class to the engine library project.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Tao.Sdl;

namespace Engine
{
  public class ControllerButton
  {
    IntPtr _joystick;
    int _buttonId;

    public bool Held { get; private set; }

    public ControllerButton(IntPtr joystick, int buttonId)
    {
     _joystick = joystick;
     _buttonId = buttonId;
    }
    public void Update()
    {
     byte buttonState = Sdl.SDL_JoystickGetButton(_joystick,
_buttonId);
     Held = (buttonState == 1);
    }

  }
}

The Update function of the button updates the Held variable. The controller class can now have its buttons added.

public ControllerButton ButtonA { get; private set; }
public ControllerButton ButtonB { get; private set; }
public ControllerButton ButtonX { get; private set; }
public ControllerButton ButtonY { get; private set; }

// Front shoulder buttons
public ControllerButton ButtonLB { get; private set; }
public ControllerButton ButtonRB { get; private set; }

public ControllerButton ButtonBack { get; private set; }
public ControllerButton ButtonStart { get; private set; }

// If you press the control stick in
public ControllerButton ButtonL3 { get; private set; }
public ControllerButton ButtonR3 { get; private set; }
public XboxController(int player)
{
  _joystick = Sdl.SDL_JoystickOpen(player);
  LeftControlStick = new ControlStick(_joystick, 0, 1);
  RightControlStick = new ControlStick(_joystick, 4, 3);
  ButtonA = new ControllerButton(_joystick, 0);
  ButtonB = new ControllerButton(_joystick, 1);
  ButtonX = new ControllerButton(_joystick, 2);
  ButtonY = new ControllerButton(_joystick, 3);
  ButtonLB = new ControllerButton(_joystick, 4);
  ButtonRB = new ControllerButton(_joystick, 5);
  ButtonBack = new ControllerButton(_joystick, 6);
  ButtonStart = new ControllerButton(_joystick, 7);
  ButtonL3 = new ControllerButton(_joystick, 8);
  ButtonR3 = new ControllerButton(_joystick, 9);
}

The buttons all need to update their state.

public void Update()
{
LeftControlStick.Update();
RightControlStick.Update();
ButtonA.Update();
ButtonB.Update();
ButtonX.Update();
ButtonY.Update();
ButtonLB.Update();
ButtonRB.Update();
ButtonBack.Update();
ButtonStart.Update();
ButtonL3.Update();
ButtonR3.Update();
}

To represent these buttons on screen we need a new function in the test state.

private void DrawButtonPoint(bool held, int yPos)
{
  if (held)
  {
    Gl.glColor3f(0, 1, 0);
  }
  else
  {
    Gl.glColor3f(0, 0, 0);
  }
  Gl.glVertex2f(-400, yPos);
}

This function makes it easy for all the buttons to be rendered, the color changing as they’re pressed. The function calls can go in the test state Render function just under where the control stick axes are rendered but still before the Gl.glEnd(); statement.

DrawButtonPoint(_controller.ButtonA.Held, 300);
DrawButtonPoint(_controller.ButtonB.Held, 280);
DrawButtonPoint(_controller.ButtonX.Held, 260);
DrawButtonPoint(_controller.ButtonY.Held, 240);
DrawButtonPoint(_controller.ButtonLB.Held, 220);
DrawButtonPoint(_controller.ButtonRB.Held, 200);
DrawButtonPoint(_controller.ButtonBack.Held, 180);
DrawButtonPoint(_controller.ButtonStart.Held, 160);
DrawButtonPoint(_controller.ButtonL3.Held, 140);
DrawButtonPoint(_controller.ButtonR3.Held, 120);

Run the test state and the now the buttons and axes are both displayed on the screen. Each button pressed will be represented visually on screen.

There are only two types of controls left that need to be handled—the trigger buttons and the D-pad. The triggers are actually represented by a single axis—the left trigger represents 0 to 1 on the axis and the right trigger represents 0 to –1. This means the code to wrap the trigger can be quite similar to the control pad.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Tao.Sdl;

namespace Engine
{
  public class ControlTrigger
  {
    IntPtr _joystick;
    int _index;
    bool _top = false; //The triggers are treated as axes and need split-
ting up
    float _deadZone = 0.24f;
    public float Value { get; private set; }

    public ControlTrigger(IntPtr joystick, int index, bool top)
    {
     _joystick = joystick;
     _index = index;
     _top = top;
    }
    public void Update()
    {
     Value = MapZeroToOne(Sdl.SDL_JoystickGetAxis(_joystick, _index));
    }

    private float MapZeroToOne(short value)
    {
     float output = ((float)value / short.MaxValue);

     if (_top == false)
     {
       if (output > 0)
       {
        output = 0;

       }
       output = Math.Abs(output);
     }

     // Be careful of rounding error
     output = Math.Min(output, 1.0f);
     output = Math.Max(output, 0.0f);

     if (Math.Abs(output) < _deadZone)
     {
       output = 0;
     }

     return output;
    }
  }

}

The ControlTrigger class operates on an axis but only takes half the value of it. There are only two triggers so it’s not much to add to the controller class. In the constructor, the two triggers are set up using the same axis.

Finally, both the triggers must be added to the Update function of the controller.

public ControlTrigger RightTrigger { get; private set; }

public ControlTrigger LeftTrigger { get; private set; }
// in the constructor
RightTrigger = new ControlTrigger(_joystick, 2, false);
LeftTrigger = new ControlTrigger(_joystick, 2, true);
// in the update function
RightTrigger.Update();
LeftTrigger.Update();

These triggers can be visualized very simply; two more points are rendered, each representing one of the triggers. The more the trigger is pressed, the further the point moves. The colors are slightly different so that they can be distinguished from the control sticks. This code should be added to the test state near the button and control stick visualizations but between the glBegin and glEnd statements.

Gl.glColor3f(0.5f, 0, 0);
Gl.glVertex2f(50, _controller.LeftTrigger.Value * 300);
Gl.glColor3f(0, 0.5f, 0);
Gl.glVertex2f(-50, _controller.RightTrigger.Value * 300);

The final control to be added is the D-pad. The D-pad will be treated as four buttons: up, down, left, and right.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Tao.Sdl;

namespace Engine
{
  public class DPad
  {
    IntPtr _joystick;
    int _index;

    public bool LeftHeld { get; private set; }
    public bool RightHeld { get; private set; }
    public bool UpHeld { get; private set; }
    public bool DownHeld { get; private set; }
    public DPad(IntPtr joystick, int index)

    {
     _joystick = joystick;
     _index = index;
    }

    public void Update()
    {
     byte b = Sdl.SDL_JoystickGetHat(_joystick, _index);
     UpHeld = (b == Sdl.SDL_HAT_UP);
     DownHeld = (b == Sdl.SDL_HAT_DOWN);
     LeftHeld = (b == Sdl.SDL_HAT_LEFT);
     RightHeld = (b == Sdl.SDL_HAT_RIGHT);
    }
  }
}

The controller only has one D-pad, but it still needs to be added.

public DPad Dpad { get; private set; }

public XboxController(int player)
{
  _joystick = Sdl.SDL_JoystickOpen(player);
  Dpad = new DPad(_joystick, 0);
// ... later in the code
public void Update()
{
  Dpad.Update();

The D-pad also needs to be added to the update loop, and that completes all the controls on the controller. Finally, it can be visualized by reusing the button display code.

DrawButtonPoint(_controller.Dpad.UpHeld, 80);
DrawButtonPoint(_controller.Dpad.DownHeld, 60);
DrawButtonPoint(_controller.Dpad.LeftHeld, 40);
DrawButtonPoint(_controller.Dpad.RightHeld, 20);

All the controls of the Xbox 360 controller are now supported and it’s relatively easy to construct any other type of controller from these control pieces. The controller is quite easy to use, but the buttons could do with a little more work. At the moment, the button reports if it is held down or not; in games it’s often more useful to know if the button was just pressed. Pressing the button once for instance can be used for selecting a menu option or firing a gun. It’s a simple piece of code to add. This code should be added to the ControllerButton class.

bool _wasHeld = false;
public bool Pressed { get; private set; }
public void Update()
{
  //reset the pressed value
  Pressed = false;

  byte buttonState = Sdl.SDL_JoystickGetButton(_joystick, _buttonId);
  Held = (buttonState == 1);

  if (Held)
  {
    if(_wasHeld == false)
    {
     Pressed = true;
    }
    _wasHeld = true;
  }
  else
  {
    _wasHeld = false;
  }
}

The Pressed value is only true for one frame when the button is pressed, the Held value is true for as long as the button is pressed. The controller class and the various controls it uses have resulted in quite a lot of code. This code is all related and could be better organized by separating it into its own namespace, Engine.Input. Reorganizing your code is an important part of building up a reusable library. All the controller classes are involved with input so a separate input sub-library can be created.

Right-click the engine project and choose New Folder on the context menu, as shown in Figure 9.13.

Creating a new project subfolder.

Figure 9.13. Creating a new project subfolder.

Call the new folder Input and drag and drop all the control classes into the folder. The input class should also be added to this folder so you end with something similar to Figure 9.14. Anytime you create a new class in the Input folder it will automatically use the namespace Engine.Input. The classes we’ve just added however need their namespaces changed manually. For each of the classes, change the line namespace Engine to namespace Engine.Input.

Separating out the input classes.

Figure 9.14. Separating out the input classes.

Try running the code. There will probably be a few errors complaining that the input classes cannot be found. To resolve these errors, add the statement using Engine.Input; to the top of the file.

Finally, the controller should be added to the Input class. In this case the Xbox 360 controller is used and the assumption is that if any user wants to use a different controller, it will have equivalent functionality to an Xbox 360 controller.

public class Input
{
  public Point MousePosition { get; set; }
  bool _usingController = false;
  XboxController Controller { get; set; }

  public Input()
  {
    Sdl.SDL_InitSubSystem(Sdl.SDL_INIT_JOYSTICK);
    if (Sdl.SDL_NumJoysticks() > 0)
    {
     Controller = new XboxController(0);
     _usingController = true;
    }
  }

  public void Update(double elapsedTime)
  {
    if (_usingController)
    {
     Sdl.SDL_JoystickUpdate();
     Controller.Update();
    }
  }
}

The Input class only supports one controller here, but it would be simple to extend to several controllers if you wanted to support that.

Adding Better Mouse Support

The mouse support at the moment is quite minor. The position of the cursor relative to the form is calculated in the form.cs and then this updates the input class mouse position. The mouse input is bound to the form and so to some extent the input class must be aware of the form. Make a new class called Mouse in the Engine.Input namespace; this class will store information about the current state of the mouse.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows.Forms;

namespace Engine.Input
{
  public class Mouse
  {
    Form _parentForm;
    Control _openGLControl;

    public Point Position { get; set; }

    public Mouse(Form form, Control openGLControl)
    {
     _parentForm = form;
     _openGLControl = openGLControl;
    }
    public void Update(double elapsedTime)
    {
     UpdateMousePosition();
    }

    private void UpdateMousePosition()
    {
     System.Drawing.Point mousePos = Cursor.Position;
     mousePos = _openGLControl.PointToClient(mousePos);

     // Now use our point definition,
     Engine.Point adjustedMousePoint = new Engine.Point();
     adjustedMousePoint.X = (float)mousePos.X - ((float)_parentForm.
       ClientSize.Width / 2);
     adjustedMousePoint.Y = ((float)_parentForm.ClientSize.Height / 2)
       - (float)mousePos.Y;
     Position = adjustedMousePoint;
    }
  }
}

The Mouse class updates its own position using the form and OpenGL control that are passed into the constructor. This new Mouse class needs to be added to the Input class.

public class Input
{
  public Mouse Mouse { get; set; }

It replaces the previous code to get the mouse position. The new Mouse class also needs to have its Update function called from the Input class.

public void Update(double elapsedTime)
{
  if (_usingController)
  {
    Sdl.SDL_JoystickUpdate();
    Controller.Update();
  }
  Mouse.Update(elapsedTime);
}

The Mouse class isn’t constructed in the Input class because the Input class shouldn’t have to know about the form or simple OpenGL control; instead, the mouse object is constructed in the form.cs constructor.

public Form1()
{
  InitializeComponent();
  simpleOpenGlControl1.InitializeContexts();
  _input.Mouse = new Mouse(this, simpleOpenGlControl1);

Now that the Mouse class exists, the UpdateInput function in the form class can be simplified.

private void GameLoop(double elapsedTime)
{
  UpdateInput(elapsedTime);
  _system.Update(elapsedTime);
  _system.Render();
  simpleOpenGlControl1.Refresh();
}

private void UpdateInput(double elapsedTime)
{
  // Previous mouse code removed.
  _input.Update(elapsedTime);
}

The elapsedTime is passed from the main GameLoop method into the UpdateInput method.

The elapsedTime has been passed to both the UpdateInput and Update functions in case we ever want the mouse to support a hover event. For instance, in a real-time strategy game, you might hover the mouse over a unit for a second or two, and on detecting that hover behavior, the game might pop up a tool tip displaying the unit’s name and stats.

The mouse buttons can be treated in the same way as the controller buttons. The button will have Held and Pressed members. For the mouse, the state of the Pressed member corresponds to a Windows Forms click event. The gamepad works by polling, which means every frame the program queries the gamepad to find out what buttons are pressed and where the control sticks are. The mouse works a little differently than the gamepad, and it’s not as straightforward to poll. Windows Form controls are associated with mouse events. An event is a way to call code when something happens; for instance, when the mouse moves, a button is clicked or double-clicked. These events can be hooked up to functions. We’ll use the click event to determine when the mouse buttons are pressed and the up and down events to determine when the mouse button is held.

bool _leftClickDetect = false;
bool _rightClickDetect = false;
bool _middleClickDetect = false;

public bool MiddlePressed { get; private set; }
public bool LeftPressed { get; private set; }
public bool RightPressed { get; private set; }

public bool MiddleHeld { get; private set; }
public bool LeftHeld { get; private set; }
public bool RightHeld { get; set; }

public Mouse(Form form, Control openGLControl)
{
  _parentForm = form;
  _openGLControl = openGLControl;
  _openGLControl.MouseClick += delegate(object obj, MouseEventArgs e)
  {
    if (e.Button == MouseButtons.Left)
    {
      _leftClickDetect = true;
    }
    else if (e.Button == MouseButtons.Right)
    {
      _rightClickDetect = true;
    }
    else if (e.Button == MouseButtons.Middle)
    {
      _middleClickDetect = true;
    }
  };

  _openGLControl.MouseDown += delegate(object obj, MouseEventArgs e)
  {
    if (e.Button == MouseButtons.Left)
    {
      LeftHeld = true;
    }
    else if (e.Button == MouseButtons.Right)
    {
      RightHeld = true;
    }
    else if (e.Button == MouseButtons.Middle)
    {
      MiddleHeld = true;
    }
  };

  _openGLControl.MouseUp += delegate(object obj, MouseEventArgs e)
  {
    if (e.Button == MouseButtons.Left)
    {
      LeftHeld = false;
    }
    else if (e.Button == MouseButtons.Right)
    {
      RightHeld = false;
    }
    else if (e.Button == MouseButtons.Middle)
    {
      MiddleHeld = false;
    }
  };

  _openGLControl.MouseLeave += delegate(object obj, EventArgs e)
  {
    // If you move the mouse out the window then release all held buttons
    LeftHeld = false;
    RightHeld = false;
    MiddleHeld = false;
  };
}

public void Update(double elapsedTime)
{
  UpdateMousePosition();
  UpdateMouseButtons();
}

private void UpdateMouseButtons()
{
  // Reset buttons
  MiddlePressed = false;
  LeftPressed = false;
  RightPressed = false;

  if (_leftClickDetect)
  {
    LeftPressed = true;
    _leftClickDetect = false;
  }

  if (_rightClickDetect)
  {
    RightPressed = true;
    _rightClickDetect = false;
  }

  if (_middleClickDetect)
  {
    MiddlePressed = true;
    _middleClickDetect = false;
  }
}

In the constructor of the mouse, four OpenGL control events have an anonymous delegate attached to them. The mouse click event detects if the left, right, or middle mouse buttons have been pressed, and this sets a boolean to report if the event occurred. In the UpdateMouseButtons function, the detect booleans are used to set the public MiddlePressed, LeftPressed, and RightPressed variables. Presses should only occur for one frame, so at the start of the function all the button press flags are set to false. After the reset, the detect variables are checked, and if a press is detected, they are set back to true. The double-click event could be supported in a similar way.

The next three events determine if any of the mouse buttons are being held down. The down event detects when the mouse button is pressed down, and the up event detects when it’s released again. These events are pretty straightforward, and they toggle the held flags for each button. The third event detects when the mouse leaves the control. This is important because once the mouse button leaves, no more events will be reported. This means the user could click down inside the control, leave the control, and release the mouse button. The release event would never be passed on and the held flags would get out of sync with the actual state of the mouse. For this reason, if the mouse leaves the control’s area, then all the held flags are set to false.

The mouse input can be tested by creating a new game state—MouseTestState—and loading it as the default game state in the EngineTest project.

class MouseTestState : IGameObject
{
  Input _input;

  bool _leftToggle = false;
  bool _rightToggle = false;
  bool _middleToggle = false;

  public MouseTestState(Input input)
  {
    _input = input;
  }

  private void DrawButtonPoint(bool held, int yPos)
  {
    if (held)
    {
     Gl.glColor3f(0, 1, 0);
    }
    else
    {
     Gl.glColor3f(0, 0, 0);
    }
    Gl.glVertex2f(-400, yPos);
  }

  public void Render()
  {
    Gl.glDisable(Gl.GL_TEXTURE_2D);
    Gl.glClearColor(1, 1, 1, 0);

    Gl.glClear(Gl.GL_COLOR_BUFFER_BIT);
    Gl.glPointSize(10.0f);
    Gl.glBegin(Gl.GL_POINTS);
    {
     Gl.glColor3f(1, 0, 0);
     Gl.glVertex2f(_input.Mouse.Position.X, _input.Mouse.Position.Y);

     if (_input.Mouse.LeftPressed)
     {
       _leftToggle = !_leftToggle;
     }

     if (_input.Mouse.RightPressed)
     {
       _rightToggle = !_rightToggle;
     }

     if (_input.Mouse.MiddlePressed)
     {
       _middleToggle = !_middleToggle;
     }

     DrawButtonPoint(_leftToggle, 0);
     DrawButtonPoint(_rightToggle, -20);
     DrawButtonPoint(_middleToggle, -40);

     DrawButtonPoint(_input.Mouse.LeftHeld, 40);
     DrawButtonPoint(_input.Mouse.RightHeld, 60);
     DrawButtonPoint(_input.Mouse.MiddleHeld, 80);
    }
    Gl.glEnd();
  }

  public void Update(double elapsedTime)
  {
  }
}

This test state reuses the DrawButtonPoint function from the earlier InputTestState that was used to test the gamepad. This test state renders a dot beneath the mouse cursor and six other dots represent the button states. The top three dots represent the held state of each button. Hold down a button and its dot will light up; release the button and it will go black. The bottom three buttons represent the press state of the buttons. Each time a button is clicked, the dot that represents it will toggle its color.

This is all that’s required for basic mouse support. There is still more work that can be done; for instance, detecting double-clicks of the mouse and having a way to poll the scroll wheel, but for most games what has been covered so far will be fine. The only remaining control method left to add is the keyboard.

Adding Keyboard Support

The keyboard is unlike the gamepad and the mouse because the method of interacting with it depends on the situation. If you have a screen that asks the user to enter his name, then you want a callback function that will tell you each character pressed by the user. But if you are in the middle of a fighting game, then you just want to be able to ask if a certain key has just been pressed; you don’t care about the rest of the keys. These are two different modes of interaction and both are important and need to be supported.

The keyboard has an event-based system like the mouse. The form has OnKey-Down and OnKeyUp events that can have delegates attached. Unfortunately, these events ignore the arrow keys, and the arrow keys are very important in games because they are often used to control movement. The Alt key is also ignored as are a few other keys known collectively as the control keys. These keys generally have some meaning in the form and are therefore hidden from general use. Games need to use these keys so an alternative method of polling these keys needs to be implemented.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Runtime.InteropServices;
using System.Windows.Forms;

namespace Engine.Input
{
  public class Keyboard
{
  [DllImport("User32.dll")]
  public static extern short GetAsyncKeyState(int vKey);

  Control _openGLControl;
  public KeyPressEventHandler KeyPressEvent;

  class KeyState
  {
    bool _keyPressDetected = false;
    public bool Held { get; set; }
    public bool Pressed { get; set; }

    public KeyState()
    {
     Held = false;
     Pressed = false;
    }

    internal void OnDown()
    {
     if (Held == false)
     {
       _keyPressDetected = true;
     }
     Held = true;
    }

    internal void OnUp()
    {
     Held = false;
    }

    internal void Process()
    {
     Pressed = false;
     if (_keyPressDetected)
     {
       Pressed = true;
       _keyPressDetected = false;
     }
    }
  }
Dictionary<Keys, KeyState> _keyStates = new Dictionary<Keys,
  KeyState>();

public Keyboard(Control openGLControl)
{
  _openGLControl = openGLControl;
  _openGLControl.KeyDown += new KeyEventHandler(OnKeyDown);
  _openGLControl.KeyUp += new KeyEventHandler(OnKeyUp);
  _openGLControl.KeyPress += new KeyPressEventHandler(OnKeyPress);
}

void OnKeyPress(object sender, KeyPressEventArgs e)
{
  if (KeyPressEvent != null)
  {
    KeyPressEvent(sender, e);
  }
}

void OnKeyUp(object sender, KeyEventArgs e)
{
  EnsureKeyStateExists(e.KeyCode);
  _keyStates[e.KeyCode].OnUp();
}

void OnKeyDown(object sender, KeyEventArgs e)
{
  EnsureKeyStateExists(e.KeyCode);
  _keyStates[e.KeyCode].OnDown();
}

private void EnsureKeyStateExists(Keys key)
{
  if (!_keyStates.Keys.Contains(key))
  {
    _keyStates.Add(key, new KeyState());
  }
}

public bool IsKeyPressed(Keys key)
{
  EnsureKeyStateExists(key);
  return _keyStates[key].Pressed;
}

public bool IsKeyHeld(Keys key)
{
  EnsureKeyStateExists(key);
  return _keyStates[key].Held;
}

public void Process()
{
  ProcessControlKeys();
  foreach (KeyState state in _keyStates.Values)
  {
    // Reset state.
    state.Pressed = false;
    state.Process();
  }
}

private bool PollKeyPress(Keys key)
{
  return (GetAsyncKeyState((int)key) != 0);
}

private void ProcessControlKeys()
{
  UpdateControlKey(Keys.Left);
  UpdateControlKey(Keys.Right);
  UpdateControlKey(Keys.Up);
  UpdateControlKey(Keys.Down);
  UpdateControlKey(Keys.LMenu); // this is the left alt key
}

private void UpdateControlKey(Keys keys)
{
  if (PollKeyPress(keys))
  {
    OnKeyDown(this, new KeyEventArgs(keys));
  }

     else
     {
       OnKeyUp(this, new KeyEventArgs(keys));
     }
    }
  }

}

The keyboard state treats each key as a button with a Pressed and Held member. A subclass called KeyState contains the state of each key on the keyboard. The Keyboard constructor takes in a reference to the OpenGL control and adds delegates to its KeyUp and KeyDown events. These events are then used to update the state of the entire keyboard. The KeyPress event is also given a delegate, and this in turn fires another event called KeyPressEvent, passing on the data. KeyPressEvent is used when the user is typing the player name or entering data. When using the keyboard as a gaming device, the keys can be treated as buttons and queried with the functions IsKeyPressed and IsKeyHeld.

The slightly complicated part of the keyboard class is the polling of keys. This requires a C function to be imported from User32.dll and the using System.Runtime.InteropServices; needs to be added to the top of the file. The KeyUp and KeyDown events aren’t fired for the arrow keys so the state of these keys is determined by GetAsyncKeyState. The PollKeyPress function uses GetAsyncKeyState to return true if the key is pressed and false if it isn’t. Each frame the arrow keys and the Alt key are polled and the state is updated.

Create a new test state to confirm that the keyboard works. There’s no code listing for this test state as it’s very similar to the mouse state. Test out some of the keys and the arrow keys to confirm everything is working nicely. Once you are satisfied—that ends the modifications to the engine. The next step now is to create a game!

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.145.8.222