libCON - Game Programming Library

  Version 0.25b
July 18, 1999
http://photoneffect.com


Contents:

1. Quick Startup
2. Library's purpose, and its pros and cons
3. Application Architecture
4. Using 2D Graphics
5. Using User Input
6. Using Sound
7. Using File Resources
8. Using Bitmaps
9. Using 3D Meshes
10. Using the Library's Interfaces.


1. Quick Startup

In order to create a basic game environment, you will need to provide a console initialization routine and a loop routine.   These routines will determine the behaviour of the program.

The first function is the initialization routine, which will set the graphics and acceleration modes:

int initConsole(int& Width, int& Height, int& FullScreen, 
                int& Flags, Screen* S);

Notice that the parameters of this function are reference parameters and must be set by this function.  If the desired mode is available and the program should continue, 0 (zero) should be returned .  If this routine returns a non-zero value, the program will terminate immediately.

Here is an example:

{
  Width=640;
  Height=480;
  FullScreen=1;
  Flags=NoAccel;
  return 0;
}

This example doesn't do any checks, but assumes that this mode is supported.

Here is a little bit more elaborate example:

{
  if (!S->isModeAvailable(640,480)) 
  {
    MessageBox(NULL,"Display Mode 640x480x16 not available.",
               "Error",MB_OK);
    return -1;
  }
  Width=640;
  Height=480;
  FullScreen=1;
  Flags=NoAccel;
  return 0;
}

This example will check for the 640x480 mode and will terminate if it is not available. The MessageBox function is a windows function (requiring #include <windows.h>).   It is optional and can be replaced by other means of error reporting.

The second required routine, the game loop function, will be called once per frame.   It is responsible for updating the game state and drawing the frame:

int action(Console* C);

The parameter it receives is the pointer to the console as initialized by the first routine.   It will be passed to this routine every time, so recording it is not a necessity, but is not prohibited.  This routine must also return zero to indicate that the program should continue to run, and a non-zero value to signal the end of the program.   NOTE: You must not do a frame loop in this routine, but render only one frame and return.   Failing to do so may cause problems with windows. You do not 'have' to render a frame, but you must perform what you would consider one iteration of the game loop and not the entire loop itself.

Here's an example of an action routine that does noting but draw a line to the screen.

{ 
  Screen* S=C->getScreen(); 
  if (S->lock()==0) 
  { 
    S->drawLine(100,100,150,150,0xffff); 
    S->unlock(); 
  } 
  S->flip(); 
  return 0;  // Will run forever. 
}

For more practical action routines, you must either read the rest of this document, look at the API Reference, or examine the code examples.


2. Library's purpose, and its pros and cons

The purpose of this library is to supply an easy to use interface for people who know how to use C++ and want to write a game or other graphic application without messing around with tough APIs such as DirectX.

Pros:

Cons:


3. Application Architecture

The library defines several concepts which relate to intuitive objects:

'Screen' is the graphics display used to draw pixels.
'Sound' is the audio system of the machine.  Used to create sounds. (optional).
'Keyboard' is the system keyboard.
'Mouse' is the system mouse.
'Joystick' is the default game controller (optional).

'Console' is an abstract term that describes the group of the previous terms as one unit.

The screen object can be used to draw onto the next frame, and then can be flipped to display that frame. Single buffered operation also exists (in Full screen mode only).   Specify the constant SingleBuffer in the Flags variable in initConsole().
The drawing routines will be discussed in the next section.
The sound object is a sound controller and does not represent any actual sound.  It is used to create SoundClip and SoundStream objects which can be played.
The keyboard, mouse and joystick are discussed in the 'Using User Input' section.

'Screen3D' is a screen interface that sits on top of 'Screen' and is used to draw 3D objects.   It is not initialized automatically, like other parts of the Console, since not all applications and games require 3D output.  If necessary, you can initialize this object in your application and use it.   This is described in the 'Using 3D Objects' section.


4. Using 2D Graphics

2D graphics refers to drawing pixels on a two dimension drawing surface which can be considered as a drawing canvas.
All 2D operations (Screen and Off Screen drawing) are used using an interface called:  'Drawable'
This interface is a representation of an abstract canvas which has a certain width and height.   The 'Screen' object inherits Drawable methods as it is also a canvas that can be drawn to.   Another entity that can be drawn to is a 'Bitmap' which is an off screen canvas, that can be copied to and from the screen or other bitmaps. Bitmaps are discussed in a later section in detail.

The Drawable interface supplies 5 kinds of operations:


Direct copies
are operations that copy whole or part of a drawable to another one.  Copying a small bitmap to the screen at changing locations can be used for sprites.  Copying from the screen to itself is also possible (to duplicate sections).  The operation is done by this Drawable method:

Drawable::copy(Drawable *Source, int x, int y, 
               int x1=-1, int y1=-1, int x2=-1, int y2=-1,
               int Transparent=-1);

The method should be activated on the destination.  x,y are the coordinates in the destination.   x1,y1,x2,y2 specify a rectangle to copy from, or all -1 for the entire area. The Transparent parameter overrides the default Source transparency. Setting it to 0 will disable transparency and setting it to 1 will enable it. The default value of -1 indicates that the source's internal transparency will be used.

Example:

{ 
  Screen* S=C->getScreen(); 
  Bitmap* BM=newBitmap(InputStream); 
  S->copy(BM,20,30); 
}

This will copy the entire bitmap to the screen at position 20,30.

Notes:


Direct (buffer) drawing
are operations that draw pixels directly to the drawable buffer.
The drawable must be locked in order to use them, and unlocked at the end.
Here are the routines:

  long lock();

  // Draw a horizontal line at y, from x1 to x2
  long drawHLine(int y, int x1, int x2, ushort color);

  // Draw a vertical line at x, from y1 to y2
  long drawVLine(int x, int y1, int y2, ushort color);

  // Draw any line
  long drawLine(int x1, int y1, int x2, int y2, ushort color);

  // Set an area outside of which nothing can be drawn
  long setClipArea(int x1, int y1, int x2, int y2);

  // Check if a point is within the clipping area.
  long isInClipArea(int x, int y);

  long unlock();

Setting the clip area (which by default is the entire drawable surface) enables drawing to be limited to a certain specified rectangle.


GDI drawing
routines are methods that use the Windows GDI API to draw to the screen.
In order to draw with these, you must call beginDraw() first to create a device context,
and call endDraw() when you're done.
These routines cannot be called while on a locked Drawable.  In addition, functions requiring a lock
cannot be called while a device context exists.

  long setPixel(int x, int y, int Red, int Green, int Blue);
  long setPixel(int x, int y, long Color);
  long getPixel(int x, int y);
  long ellipse(int x1, int y1, int x2, int y2);
  long line(int x1, int y1, int x2, int y2);
  long setColor(int Red, int Green, int Blue);
  long setBKColor(int Red, int Green, int Blue);
  long printXY(int x, int y, int Color, char *str);
  long getTextWidth();
  long getTextHeight();
  long fillRect(int x1, int y1, int x2, int y2);

Using these routines is considered slower, but the extent of this is not definite.

If you know how to use Windows device contexts, you can retrieve the device context after calling beginDraw():
HDC dc=(HDC)yourBitmap->getDeviceContext();
and use it with any of the Win32 sdk functions.  You will still need to call endDraw, when you're done with the device context.


Buffer retrieval
routines are used for direct access to the pixel buffer.  Like direct drawing, these routines require a lock on the object.

IMPORTANT: If you're not absolutely sure you understand the layout of the buffer, don't use these methods at all. Caution must be taken when using these methods, as you may access memory outside the actual surface and cause problems.

First you must understand the layout of the buffer:

Assume we have a 4x4 drawable buffer.   This is an example of a possible buffer layout.
Each box is a pixel (2 bytes), and the memory buffer is a linear array of all boxes.

0,0  1,0  2,0  3,0  Unused  Unused  Unused  Unused  Unused 
0,1  1,1  2,1  3,1  Unused  Unused  Unused  Unused  Unused 
0,2  1,2  2,2  3,2  Unused  Unused  Unused  Unused  Unused 
0,3  1,3  2,3  3,3  Unused  Unused  Unused  Unused  Unused 

The boxes with coordinates are the actual pixels of the drawable object.  All 'Unused' boxes are for system use, and should not be accessed.

The memory layout of the buffer is linear, as the example below shows:

0,0  1,0  2,0  3,0  Unused  Unused  Unused  Unused  Unused  0,1  1,1  2,1 

Each row's memory size is 2*Width + Extra.   This total can be retrieved by calling the getPitch() method, once the object is locked. The pitch is the number of bytes  (not pixels) between the beginning of one row, to the beginning of the next.

You can always use getLine(int) if you don't want to calculate this.   It will return a pointer to the beginning of the specified row.


Using the DirectX interface

If you know how to use DirectX surfaces, you can retrieve the Drawable surface at any time and use the DirectX API to create any special effect you need.  Use (LPDIRECTDRAWSURFACE4)Drawable::getDDrawSurface() to get a pointer to the surface.    You can only acquire this pointer when the Drawable is not locked or has a device context.

In order to keep the libraries internal data structures consistent, you must return the surface to the state it was when you got it.   This means that if you locked it yourself, you must unlock it before continuing.


5. Using User Input

User input is divided into 3 categories.  Keyboard, Mouse, Joystick.

There are 2 kinds of access to the input devices:

Direct access requires to call the update() method to retrieve the current information from the device (polling) .
Event access is different for each device, and is not supported currently for the Joystick.

Keyboard:

Checking if a certain key is pressed is done by using the [] operator:
example:

{ 
  Keyboard& K=(*C->getKeyboard()); 
  if (K[DIK_ESCAPE]) return -1; 
  return 0; 
}

Notice that we're using a reference when getting the keyboard object to avoid copying it (there is only one keyboard).  The [] operator returns 0 if the key is not pressed and 1 if it is. The DIK_* constants can be found in the CON_Keys.h file which contains these constants taken from DirectX <dinput.h>.

In order to get a key press/release event, there is no need to call update() (that will not affect events, though).  A call to Keyboard::getKey(int& ID, int& Pressed) will return a non zero value if a key event has occured.   If so, the ID and Pressed will contain which key it was and whether it was pressed or released.  If the keyboard is to be used (for events) after a long period during which it wasn't used, it is good to call clear() before getting any events.

The Direct and Event access routine can be used together or interleaved or whatever is necessary.

Mouse:

Checking the mouse state by direct access is not really accurate.  This because the internal device reports events in relative coordinates and this may cause the mouse to look jumpy on slow frame rate applications with fast mouse movements.  It is supplied for backwards compatibility.

If the mouse is being used, the event access method for the mouse should be called each frame and the events processed until no events are left

Example:

{ 
  long rc; 
  while ((rc=C->getMouse()->getEvent(0,0,640,480))!=0) 
  { 
    if (rc==1) // mouse moved
    { // do something 
    } 
    if (rc==2) // left button (de)pressed
    { // check state and act. 
      if (C->getMouse()->leftButton())
      { // do something.
      }
    } 
  } 
}

If the mouse is to be used after a long period during which it wasn't used, it is a good idea to call clear() before getting any events.
The coordinates passed to getEvent() specify the bounding box that limits where the mouse can go.

The mouse pointer doesn't appear by default.  Since we're not using the system mouse, you can tell the mouse to draw its pointer by calling: Mouse::render(Screen* S).   The screen must be unlocked, since the pointer is a bitmap that will be copied to the screen.  The mouse pointer should be drawn just before you flip the screen to avoid it being overwritten.

You can also set the mouse pointer bitmap, if you don't like the default one (you probably won't like it), by calling Mouse::setPointer(Bitmap*,int,int) and supplying an alternative bitmap. This bitmap will be copied onto the internal Mouse bitmap so you must delete your mouse pointer bitmap yourself.   The two coordinates define the position within the bitmap of the mouse hotspot.

Joystick:

Since the joystick supports only polling for now, you will need to call update() to get the current state of the joystick and then use one of these methods:

int getX(); 
int getY(); 
int getZ(); 
int button(int Num=0); 
int POV(int Num=0);

The range returned for the axes is usually 0 - 0xffff, but can vary.  You can check the range by asking the user to swing the joystick from one coner to another (as in old DOS games).

The button(int) will return non-zero if the specified button is pressed.

The POV(int) will return the state of a certain point of view control.


6. Using Sound

There are two kinds of sound objects supported: SoundClip & SoundStream.

Sound clips are finite static elements of audio that can be played once, or in a loop, and may even be associated with 3D Objects for 3D sound.   Sound clips are constructed using raw audio data and its parameters, or by using utility functions to load them from a WAV file.   These are the two functions used to build SoundClip objects:

newSoundClip(char*    AudioData, int Size,
             int Freq=44100, int Bits=16, int Channels=2, int HW=SoundAccel);

newSoundClip(istream* AudioData, int Size,
             int Freq=44100, int Bits=16, int Channels=2, int HW=SoundAccel);
 

If you don't want to use raw data but .WAV files, use one of the following functions:

SoundClip* loadWaveFile(istream* is, int HW=SoundAccel);
SoundClip* loadWaveFile(char*  Name, int HW=SoundAccel);

The first loads the .WAV from an open binary stream.  It can be a file stream, or a stream retrieved from the resource stream object.  The second opens a stream on the file with the specified name and then calls the first function.

Sound streams are sounds of undetermined length and are loaded chunk by chunk while playing.  The input stream passed to the SoundStream constructor must remain valid while the sound plays and provides the audio samples during play.  This is useful for background sounds which are continuous.  They can be generated in real time or loaded from a very large file.

You can create a sound stream directly by specifying the data source and the the wave details or by using one of the streamFile functions.

SoundStream* newSoundStream(ResourceStream* RS, istream* AudioData,
                            int Freq=44100, int Bits=16, int Channels=2, int HW=0);
SoundStream* newSoundStream(InputFilter* AudioData,
                            int Freq=44100, int Bits=16, int Channels=2, int HW=0);


For stream large wav files (uncompressed), use the streamWaveFile function:

SoundStream* streamWaveFile(ResourceStream* RS, const char* Name, int HW=0);

For any audio file (even .mp3) use the streamAudioFile function.  The DirectX Media drivers must be installed though.

SoundStream* streamAudioFile(const char* Name, int HW=0);


Playing the sounds is done by calling the method  SoundClip::play(int).   The integer parameter should be non-zero for loop play or zero for one play only.    The SoundClip is not released once it is done playing.  It may be played again.

In order to play the same sound several times in parallel (like bullets shooting from a gun), you must create several instances of the SoundClip object.

It is possible to have a detached sound clip.  This is useful for example when an object explodes and wants to play an explosion clip.  The object is destroyed before the clip has completed play, and cannot release it.  Calling SoundBuffer::destroyOnStop() will cause the soundclip object to delete itself when it is done playing.   This is only works for sound clips which are not played in a loop.

The stop() method can be used to stop playing at any time.

The setPosition() and setDirection() methods are used with 3D Objects, to place the SoundClip in 3D Space.

By default, SoundClips are not 3D sounds.  They will be played just as they were recorded.  A sound can be used as a 3D sound only if it was created with the Sound3D flag in the HW field.  To enable 3D processing on the sound clip, call the set3DMode(int) method with a non-zero parameter.

3D Sounds require a 3D Camera to be active (to serve as the player's ears) in order to be useful.


7. Using File Resources

Games usually need some resources such as bitmaps, sound clips, etc. to be loading from disk.  It is usually good to have the resources packed and compressed in one data file for distribution.   The ResourceStream class provides an interface to pack several data objects in one file.

You can create a ResourceStream with one of these functions:

ResourceStream* newResourceStream(); 
ResourceStream* newResourceStream(ofstream* os); 
ResourceStream* newResourceStream(const char* Name, int Create=0); 

The first (no parameters) creates a transparent interface for actual files on disk.   This is useful during for development until the files are actually packed.

Creating a ResourceStream by passing a binary file output stream, or a name and Create=1, will open the stream for output.   This is used to pack the files into this stream for later usage.   This is usually not done with in the game application but by a separate utility.   Putting data into the stream is done by calling one of the putData() methods. Such a utility application is supplied in the crtrsc.exe provided with the package.

The data put into the stream can be compressed automatically by calling setCompression(1) before putting the data.  Compressed data is decompressed automatically at runtime.  Note: Compressed data items provide streams without seek ability.

Creating a ResourceStream by passing a name and Create=0 will open the stream for input.  You can then access any of the files stored by using the  getData(char* Name, char* cData, int MaxLen=-1) method.   This method requires that you supply a buffer into which the data will be read.

A more useful method is the  getStream(char* Name) method.   It returns an input stream that can be used just like a normal file stream.  The actual size of the data object can be retrieved by the getLength(char* Name) method.

After finishing up with a stream, you must call the freeStream(istream*) method to release the stream.

Some other classes (like texture cache) require you to supply a resource stream for their data source.  You can set a default resource stream that will be released by the system when the application terminates.  Pass a NULL where ever a resource stream is required to access it.   You set the default like this:

setDefaultResourceStream(newResourceStream(......)); 

It's also possible to create a resources file by using the utility program crtrsc.exe
You can use this program in one of two ways:
1.  Create a resource file while specifying all resources in the command line.
    Example:  crtrsc  file.rsc  file1 file2 file3 file4 etc....
2.  Create a resource file using a list file containing the resource names
     Example:  crtrsc file.rsc @listfile.txt
     listfile.txt contains all the names of the resources, one per line.


8. Using Bitmaps

Bitmaps are drawable objects (canvases) just like the screen and can be drawn to as described in the 2D Graphics Section.

Blank Bitmaps can be created using the newBitmap(int Width, int Height, int NoAccel=0) function.  Any of the copy or drawing methods can then be used to change the bitmap data.

Bitmaps can be created as copies of an existing bitmap using the newBitmap(Bitmap& BM) function.

Bitmaps can be loaded from a stream using the newBitmap(istream& is, int NoAccel=0) function.

The data format of the stream can be a standard uncompressed 24 bit windows BMP or in the libCON internal format which is:

Width  (32 bit integer)
Height (32 bit integer)
Bits Per Pixel (32 bit integer) // Should be 24 for maximum compatibility but 16 is supported too.
Bitmap raw data (Width*Height*BPP/8 bytes)

Since 16 bit implementation differs between different display cards, it is recommened that all bitmaps be storedin 24 bit format.  Let the application convert to 16 bit on loading according to the hardware.

For simplicity, there is also a loadBMP(char* Filename) function to load a bitmap directly from a *.BMP file.  This version will load directly from a file (not a resource) and will load compressed bitmaps.


9. Using 3D Meshes

Using a 3D environment requires some setup including initializing a 3D screen and creating a camera.   This is not done automatically since not all games use 3D graphics.  However, if you use the lib3D utility library, you can skip this setup (as detailed immediately below), and replace it by creating a World3D object. You will still need to create/load meshes as described momentarliy.

The two basic elements that must be created are:

Screen3D  - by calling initScreen3D();
Camera     - by calling newCamera();

The camera should be positioned, directed and made active by using these methods:

setPosition()
setDirection()
activate()

A camera's output is by default the entire screen.   To change that, call  Camera::setViewable().


Meshes can then be loaded / created and rendered.

To load a mesh from a file, use:

loadXFile(char* Name, Mesh* O, TextureCache* TC, float Scale=1.0f);

A TextureCache object is not a must and may be NULL.
Currently loading X files from a stream is not supported.  Meshes must be loaded from actual files only.

You can also build your own mesh at run time.  A mesh is created by using the MeshBuilder interface.  A MeshBuilder can be created by using the newMeshBuilder() function.   In order to build the mesh, use the MeshBuilder setup methods:

long addMaterial(const char* Name,
                 const TColor Diffuse, const TColor Specular, const TColor Emissive,
                 const float Power, Texture* texture=NULL);
long addVertex(const Vector3D& p, const fVector2D& t=fVector2D());
long addFace(short a, short b, short c, int MaterialNumber); 

Notice that addMaterial doesn't accept a bitmap for its texture but rather a Texture pointer.  A Texture pointer can be obtained either by loading a texture from a TextureCache object or by using the newTexture() function.

After finishing with adding all data, the createMesh() method should be called to process it and create a Mesh.  After a Mesh has been created, it is ready to be rendered and cannot be changed any more (You cannot add more data to it).  However, it may be passed back to MeshBuilder using MeshBuilder::rebuild(mesh).

Mesh inherits from Transformable to provide positioning and orientation in 3D space.  Prior to rendering, use the member functions of its Transformable for this.  Although Transformable declares a few member variables as public, it is advisable to manipulate it using only the member functions of Transformable -- its member variables will soon be reorganized.

Rendering meshes (after the 3D screen and camera are set) is done by:

Clearing the scene with the Camera::clear() method.  This also orients the 3D listener.
Beginning a scene with the Screen3D::beginScene() method.
Render meshes with the Screen3D::renderObject() method.
And finally, end the scene with the Screen3D::endScene() method.

This renders the meshes to the Screen, but flipping the 2D screen is still required to make it visible.  Use the orientSound() method if you associate a 3D sound with a mesh.  This will place the sound in the mesh's position.

You can either load textures using a TextureCache, in which case a texture can be shared by name, or create a texture manually and be able to modify it and use it any way you like.
Loading textures is done by using the TextureCache class.    You can create your own texture cache by using:  newTextureCache(ResourceStream*, int Destroy=0).  You must release the texture cache when you're done with it.  Setting Destroy to a non-zero value will cause the TextureCache to release the resource stream when it is destroyed.

You can also initialize a system texture cache that will be used as default whenever the parameter passed for it is NULL  (e.g. in loadXFile).  You do that by using the initTextureCache(ResourceStream* RS=NULL, int Destroy=0);  The default resource stream will be used if RS==NULL, but the default must be set up for that.

Once a TextureCache object is created, it will allow loading (using the load() method) of textures and it will store them for later loading so no duplicates are created.   Textures are loaded from Bitmap objects internally.  The stream containing the bitmap can be either in libCON format or standard 24bit BMP format.
It must however have dimensions which are a power of 2 (e.g.  32x32  128x64   256x256)

Creating a texture is done by using this function:

Texture* newTexture(const int Width, const int Height);

The Texture interface can be used like a bitmap and can be drawn onto in similar ways.  Creating a Mesh with such a created texture can be used to create texture animation as can be seen in the 3D video code example.


10. Using the Library's Interfaces.

Since version 0.20, the DLL provides abstract interfaces instead of exported classes.  This gives better protection against inconsistencies, solves allocation problems between processes, and will allow easier future porting to other compilers.

The main changes in code related to this are:

All the creation functions can be found in their appropriate subject header file. (e.g. sound functions in CON_Sound.h).