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.
In order to create a basic game environment, you need to provide an console initialization routine, and provide a loop routine. These routines will determine the behaviour of the program.
The first function is the initialization routine, which will set graphics mode and acceleration:
int initConsole(int& Width, int& Height, int& FullScreen, int& NoAccel, Screen* S);Notice that the parameters of this function are reference parameters and are to be filled by the routine. The routine must return 0 if the desired mode is available and the program should continue. If this routine returns a non-zero value, the program will terminate immediately.
Here is an example:
{ Width=640; Height=480; FullScreen=1; NoAccel=0; 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; NoAccel=0; 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>)
The second routine required, the game loop routine, will be called once per frame, to do the changes in the game state and draw the frame:
int action(Console* C);The parameter it recieves is the pointer to the console 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 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.
Here's an example of an action routine that just draws 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 available.
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:
The library defines several concepts which relate to intuitive objects
a programmer can relate to:
'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 stuff onto the next frame, and
then can be flipped to display that frame.
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.
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 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:
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:
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 the programmer to limit drawing to a certain area.
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 the drawable is locked, and 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 still need
to call endDraw, when you're done with the device context.
Buffer retrieval routines are used to give direct access to the pixel buffer, to the programmer. 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.
User input is divided into 3 categories. Keyboard, Mouse, Joystick.
There are 2 kinds of access to the input devices:
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 (We only have one keyboard)
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 time it wasn't, it is good to call clear() before getting any events.
The Direct and Event access routine can be used together or interleaved or what ever is necessary.
Mouse:
Checking the mouse state, by direct access is not really accurate, since
the internal device reports events in relative coordinates and using this
may cause the mouse to look jumpy on slow frame rate applications with
fast mouse movements.
It is supplied for backwards compatibility.
The event access method should be called and processed until no events
are left, each frame, if the mouse is being used.
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 time it wasn't, it is good to call clear() before getting any events.
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. It is good to draw the mouse 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 where in the bitmap is 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);
The values range returned for the axes is usually 0 - 0xffff, but can vary. You can check it once for range, by asking the user to wiggle the joystick to the corners (like old DOS games)
The button(int) will return whether the specified button is
pressed or not.
There are two kinds of sound objects supported: SoundClip & SoundStream.
Sound clips are finite elements of audio, that can be played once, in a loop, and 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 constructors 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);
SoundClip* loadWaveFile(istream* is, int HW=SoundAccel);
SoundClip* loadWaveFile(char* Name, int HW=SoundAccel);
The first one 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 one opens a stream to a file with the specified name, and then calls the first function.
Sound streams are sounds of undetermined length, which are loaded chunk by chunk while playing. The input stream passed to the SoundStream constructor must remain valid while the sound plays, and will provide the audio samples during play. They are usually useful for background sounds which are continuous, and can be generated in real time or loaded from a very large file.
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. You may play it 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 of course useful for sound clips with non loop play only.
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.
SoundClips are by default not 3D sounds. This means that 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. In order to enable 3D processing on the sound clip, call the set3DMode(int) method. Specify a non-zero value to enable the 3D processing.
3D Sounds require a 3D Camera to be active (to serve as the player's
ears) in order to be useful.
Games usually need some resources such as bitmaps, sound clips, etc. to be available for loading on file. It is usually good to have the resources packed in one data file, for distribution. The ResourceStream class provides an interface to pack several data objects in one file.
You can creating a ResourceStream with one of these functions:
ResourceStream* newResourceStream();
ResourceStream* newResourceStream(ofstream* os);
ResourceStream* newResourceStream(const char* Name, int Create=0);
The first one with no parameters, creates a transparent interface for actual files on disk. This is good for development, until the files are actually packed.
Creating a ResourceStream by passing a binary 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 in the game application, but by a separate utility. Putting data into the stream is done by calling one of the putData() methods.
The data put into the stream can be compressed automatically, by calling setCompression(1) before putting data. Compressed data is decompressed automatically in runtime. Note: Compressed data items will provide streams with no 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 the 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 also be retrieved by the: getLength(char* Name) method.
After a stream is done with, 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, and then pass a NULL where ever a resource stream is required. 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.
Bitmaps are drawable objects just like the screen, and can be drawn to, just like described in the 2D Graphics Section.
Bitmaps can be created blank, by using the newBitmap(int Width, int Height, int NoAccel=0) function You can then use any of the copy or drawing methods to change the bitmap data.
Bitmaps can be created as copies of an existing bitmap, by using the newBitmap(Bitmap& BM) function.
Bitmaps can be loaded from a stream by 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 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, its best to store all bitmaps in 24 bit format, and let the application convert to 16 bit on loading according to the implementation.
For simplicity, there is a loadBMP(char* Filename) function
to load a bitmap directly from a *.BMP file. Currently, it isn't
possible to use a stream for this, just a plain file.
It is recommended to eventually convert the bitmap, so that it can
be packed and compressed along with the rest of the data.
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 the creation of all this setup, and replace it by creating a World3D object. You will still need to create/load meshes as described below.
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 created and rendered.
An empty mesh is created by using the newMesh() function. In order to build the mesh, you can either use the mesh setup methods:
long addMaterial(char* Name, ushort Diffuse, ushort Specular, ushort
Emissive, float Power, void* Texture);
long addVertex(float x, float y, float z, float u=0.0f, float v=0.0f);
long addFace(short a, short b, short c, int MaterialNumber);
Notice that addMaterial doesn't accept a bitmap for its texture, but a texture pointer, which can be obtained by loading a texture from a TextureCache object.
In this case, after finishing to add all data, the: finalize() method should be called, to process all that data. Once an object is finalized, it is ready to be rendered, and cannot be changed any more (You cannot add more data to it).
Or you can use a utility function that loads an object from a file:
loadXFile(char* Name, Mesh* O, TextureCache* TC, float Scale=1.0f);
This function requires a created empty Mesh, which it will
fill with data.
A TextureCache object is not a must, and may be NULL.
Currently Microsoft doesn't support loading X files from a stream, so the mesh will be loaded from an actual file only.
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, by calling the Camera::clear() method.
This also orients the 3D listener.
Beginning a scene, by calling the Screen3D::beginScene() method.
Rendering any meshes by calling the Screen3D::renderObject()
method.
Ending a scene, by calling the Screen3D::endScene() method.
This renders the meshes to the Screen, but flipping the 2D screen is still required to make it visible. It is a good idea to use the orientSound() method if you associate a sound with a mesh. This will place the sound in the mesh's position.
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 instance to release the resource stream when it is destroyed.
You can also initialize a system texture cache that will be used as default whenever it's needed and the parameter passed 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) on textures, and 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)
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 allows future porting to other compilers easier.
The main changes in code that this causes are: