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.
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:
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.
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.
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.
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.
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.
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.
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).