Introduction
This is the fourth and final part of the Sample Pamper series where we are going to put all things together in a simple game called Space Box. All the things we have learnt this far will be applied to the game, with rotations, lights, and polygon painting. This article will give you a clue of how to implement these things in objects, and here, I'll give you a hint to how to do this. This game is based on the old fashion game "Asteroids", and you will see why when you play it; if you already know how to do this, then you can just have fun playing it, so let's go.
This article will describe…
Requirement
You will need Visual Studio .NET installed to be able to run and try the source code for this article.
Back to top?
The game
In this picture, you can see three enemies, each in the shape of a box; the player is the circular object. The orange text "SHIELD" is a shield you can activate with the right mouse button, but only if you're fast enough to get one when the text appears for a few seconds. This shield will kill an enemy colliding with it and then disappear. Then, you must find a new one to be able to kill another enemy. You can also shoot the enemies with a little laser ball. In this picture, you can see some circles around one of the enemies, and this is because the player has just shot him. The blue line drawn from the player object to the mouse pointer displays which way the thrust for the object will go. The effect is like if there was a rubber band between the player object and the mouse pointer.
Back to top?
Class diagram
Back to top?
Using the code
The downloadable file is quite big (around 700 KB) because of the sound files included, but I've made them as small as possible. I've put everything in objects with interfaces, but there are things left out like some functions and objects. You can test your own skills and rearrange them, or just play the game like it is, it's up to you. From now, I will explain all the parts in this package. First, I'll start off by showing you the class diagrams over the interfaces and objects, so that you can see the connection between them.
To use this package, we must type the following lines in the game application:
using System.Threading;
using Engine3D;
using Engine3DVP;
The first line is there because we are using threads to play sounds simultaneously in the game, in a simple way. Engine3D
is the namespace where we will find the engine, player, enemy, and others that we will need. Engine3DVP
is the value pack including all the interfaces and structures needed for the game. We are not using these interfaces to 100%, but we are using them, you should use the interfaces in the parameters in the functions all the way. Now, in the game client application, we have our sound engine, which we will use in the game. Here's the code snippet for that part.
Back to top?
Sound engine
public class Sound
{
[Flags]
public enum SoundFlags : int
{
SND_SYNC = 0x0000,
SND_ASYNC = 0x0001,
SND_NODEFAULT = 0x0002,
SND_MEMORY = 0x0004,
SND_LOOP = 0x0008,
SND_NOSTOP = 0x0010,
SND_NOWAIT = 0x00002000,
SND_ALIAS = 0x00010000,
SND_ALIAS_ID = 0x00110000,
SND_FILENAME = 0x00020000,
SND_RESOURCE = 0x00040004
}
[DllImport("winmm.dll")]
public static extern bool PlaySound( string szSound,
IntPtr hMod, SoundFlags flags );
[DllImport("winmm.dll")]
public static extern long mciSendString(string strCommand,
string strReturn, int iReturnLength, IntPtr hwndCallback);
private bool mLoop;
private string mSound;
private string mName;
private void ThreadPlay()
{
mciSendString("open \""+
this.mSound+"\" type mpegvideo
alias "+this.mName,null,0,IntPtr.Zero);
if(this.mLoop)
mciSendString("play "+
this.mName+" repeat",null,0,IntPtr.Zero);
else
mciSendString("play "+this.mName,null,0,IntPtr.Zero);
}
public void PlayMulti( string strFileName, bool loop, string name)
{
this.mName = name;
this.mLoop = loop;
this.mSound = strFileName;
Thread mMThread = new Thread(new ThreadStart(ThreadPlay));
mMThread.Start();
mMThread.Join();
this.ThreadPlay();
}
public void PlaySingle( string strFileName, bool loop)
{
PlaySound(strFileName,IntPtr.Zero, SoundFlags.SND_ASYNC);
}
}
This object has a PlaySingle
function and one for multi sound play, and it leaves all the created threads to the Garbage Collector, so it will leave some memory held for a while before being released. Not the best way to do it, but it is manageable. It plays Wave and MP3 files, we will use them both in this game. Now, we need to create an instance of the engine in the client game application.
private Engine3DVP.IEngine3D eng =
new Engine3D.Engine3D(SCREEN_WIDTH,SCREEN_HEIGHT);
Unfortunately, we have the same name of the namespace as the Engine3D
object, this should never be done, but we are doing it here anyway. This will create an engine to handle a screen object, the game screen. Now, I'll show you Engine3D
so that you can see what it does in detail.
Back to top?
Game engine
using Engine3DVP;
namespace Engine3D
{
public class Engine3D: IEngine3D
{
private int SCREEN_HEIGHT;
private int SCREEN_WIDTH;
private Point mMouse;
private ArrayList mObjects;
public Engine3D(int screen_width, int screen_height)
{
}
public int Energy(int i)
{
return ((IObject3D)this.mObjects[i]).Energy;
}
The function Energy
will give you the amount of energy the object has left to live.
public double GetDegree(int i, int m)
{
return ((IObject3D)this.mObjects[i]).GetDegree(m);
}
public void SetDegree(int i, int m, double degree)
{
((IObject3D)this.mObjects[i]).SetDegree(m,degree);
}
public void NewPlayer(ref ArrayList mesh, Poinx3D place,
double rotX, double rotY, int energy)
{
IObject3D temp = new Player(place, rotX, rotY, energy);
temp.SetMeshPoints(ref mesh);
this.mObjects.Add((IPlayer)temp);
}
public void NewEnemy(ref ArrayList mesh, Poinx3D place,
double rotX, double rotY, double paseX, double paseY, int energy)
{
IObject3D temp = new Enemy(place, rotX, rotY, paseX, paseY, energy);
temp.SetMeshPoints(ref mesh);
this.mObjects.Add((IEnemy)temp);
}
private void MovePlayer(Player obj)
{
if(obj.Energy>0)
{
obj.PaseX = ((double)this.mMouse.X- ((double)obj.ObjPos.X))/200.00;
obj.PaseY = ((double)this.mMouse.Y- ((double)obj.ObjPos.Y))/200.00;
if(obj.CountX>25)
obj.CountX=25;
else if(obj.CountX<-25)
obj.CountX=-25;
if(obj.CountY>25)
obj.CountY=25;
else if(obj.CountY<-25)
obj.CountY=-25;
obj.CountX += obj.PaseX;
obj.CountY += obj.PaseY;
Poinx3D newpos = new Poinx3D();
newpos.X = (int)obj.CountX+obj.ObjPos.X;
newpos.Y = (int)obj.CountY+obj.ObjPos.Y;
newpos.Z = obj.ObjPos.Z;
TestForScreenLimits(ref newpos);
obj.ObjPos = newpos;
obj.SetRotX(obj.PaseY/40.0);
obj.SetRotY(obj.PaseX/40.0);
ColDetection(obj);
}
}
A good idea is to change all the values like /40 to constants instead.
private void TestEnemyPlayer(ref Enemy obj)
{
for(int i=0;i<this.mobjects.count;i++)
{
if(this.mObjects[i] is Player)
{
IObject3D pl = (Player)this.mObjects[i];
double distx = (obj.ObjPos.X-pl.ObjPos.X);
double disty = (obj.ObjPos.Y-pl.ObjPos.Y);
double dist = Math.Sqrt(Math.Abs(distx)*Math.Abs(distx)+
Math.Abs(disty)*Math.Abs(disty));
if(dist>0 && dist<140 && !obj.InWar && pl.Energy>0)
{
double cc4 = -Math.Cos((Math.Acos(distx/dist)))*10;
double cr4 = -Math.Sin((Math.Asin(disty/dist)))*10;
obj.PaseX = cc4;
obj.PaseY = cr4;
obj.InWar = true;
}
else if(dist>=80 && obj.InWar)
obj.InWar = false;
}
}
}
This function above will test if an enemy is close enough to attack a player object.
private void MoveEnemy(Enemy obj)
{
if(!obj.IsDead)
{
TestEnemyPlayer(ref obj);
Poinx3D temp;
temp = obj.ObjPos;
temp.X += (int)(obj.PaseX);
temp.Y += (int)(obj.PaseY);
temp.Z = obj.ObjPos.Z;
TestForScreenLimits(ref temp);
obj.ObjPos = temp;
ColDetection(obj);
}
}
private void TestForScreenLimits(ref Poinx3D obj)
{
if(obj.X<-50)
obj.X = SCREEN_WIDTH;
if(obj.X>SCREEN_WIDTH)
obj.X = -50;
if(obj.Y<-50)
obj.Y = SCREEN_HEIGHT;
if(obj.Y>SCREEN_HEIGHT)
obj.Y = -50;
}
TestForScreenLimits
will make an object moving off screen in one way enter from the other, like in the old game Asteroids (I should say the old classic game Asteroids).
private void HorizontalLine3D(ref Bitmap bm, ref uint[][] zbuffer,
Poinx3D p1, Poinx3D p2, Color color, double intencity)
{
int dx = (int)(p2.X-p1.X);
int dz = (int)(p2.Z-p1.Z);
int step_x = 0;
int step_z = 0;
if(dx>=0)
step_x = 1;
else
{
step_x = -1;
dx = -dx;
}
if(dz>=0)
step_z = 1;
else
{
step_z = -1;
dz = -dz;
}
int dx2 = dx*2;
int dz2 = dz*2;
int err_termXZ = 0;
int uni1D = 0, uni2D2 = 0;
int inc11 = 0, inc12 = 0, inc2 = 0;
int step1D2 = 0, step2D = 0;
int loopTo = 0, direction = 0;
if(dx>=dz)
{
err_termXZ = dz2 - dx;
direction = 0;
loopTo = dx;
uni1D = dx2;
inc11 = (int)p1.Y;
step1D2 = step_z;
inc12 = (int)p1.Z;
uni2D2 = dz2;
inc2 = (int)p1.X;
step2D = step_x;
}
else if(dz>dx)
{
err_termXZ = dx2 - dz;
direction = 2;
loopTo = dz;
uni1D = dz2;
inc11 = (int)p1.Y;
inc12 = (int)p1.X;
step1D2 = step_x;
uni2D2 = dx2;
inc2 = (int)p1.Z;
step2D = step_z;
}
for(int i=0;i<=loopTo;i++)
{
Color cl = color;
if(direction==0&&inc2>0&&inc2<screen_width&&inc11>0&&
inc11<screen_height)
{
if(inc12>zbuffer[inc2][inc11])
{
bm.SetPixel(inc2,inc11,cl);
zbuffer[inc2][inc11] = Convert.ToUInt32(inc12);
}
}
if(direction==2&&inc12>0&&inc12<screen_width&&inc11>0&&
inc11<screen_height)
{
if(inc2>zbuffer[inc12][inc11])
{
bm.SetPixel(inc12,inc11,cl);
zbuffer[inc12][inc11]=Convert.ToUInt32(inc2);
}
}
if(err_termXZ>=0)
{
err_termXZ -= uni1D;
inc12 += step1D2;
}
err_termXZ += uni2D2;
inc2 += step2D;
}
}
Back to top?
That's all about the calculation functions. The following functions are there to help the above functions do their work:
public Segment3D Normalize(ref Segment3D normal)
{
double len = Math.Sqrt(normal.sX*normal.sX+normal.sY*normal.sY+
normal.sZ*normal.sZ);
normal.sX = ((double)normal.sX / len);
normal.sY = ((double)normal.sY / len);
normal.sZ = ((double)normal.sZ / len);
return normal;
}
public Segment3D BuildSegment(Poinx3D start, Poinx3D end, ref Segment3D segm)
{
segm.sX = end.X-start.X;
segm.sY = end.Y-start.Y;
segm.sZ = end.Z-start.Z;
return segm;
}
BuildSegment
is just that, it will build a segment.
public double DotNormalized(Segment3D segm1, Segment3D segm2)
{
double x = segm1.sX * segm2.sX + segm1.sY * segm2.sY + segm1.sZ * segm2.sZ;
return x;
}
DotNormalized
will give the dot product out of two normalized segments.
public Segment3D ExctractNormal(Segment3D segm1,
Segment3D segm2, ref Segment3D normal)
{
normal.sX = ((segm2.sY*segm1.sZ)-(segm2.sZ*segm1.sY));
normal.sY = ((segm2.sZ*segm1.sX)-(segm2.sX*segm1.sZ));
normal.sZ = ((segm2.sX*segm1.sY)-(segm2.sY*segm1.sX));
return normal;
}
ExctractNormal
will give you the normal out of two segments in a surface and return the normal one. That's it. I've shown you the most important stuff in this game. Download it and play with it, change it but only for learning purposes. Do you want to use it for other reasons? You'll have to tell me by giving me a call in this article forum.
Back to top?
Points of interest
This is just an example of what you can accomplish out of the information you got from the earlier articles in this series. Displaying graphics should be done in other ways than manipulating with a Bitmap
, like we are here. It's not perfect, but alive.
History
- Version 1.0, uploaded 8 November, 2007 - A version included for the article, for C#.
- Version *, changed 14 November, 2007 - Made some changes to the article.
- Version 1.0, uploaded 17 November, 2007 - Same as before, but available for Visual Studio 2005 .NET.
- Version *, changed 25 November, 2007 - Made some new changes to the article.
Back to top?
References
Links
Back to top?
Disclaimer
This software is provided 'as-is' without any express or implied warranty. In no event will the authors be held liable for any damages arising from the use of this software. Permission is granted to anyone to use this software for learning purpose only. And, never could you claim that it is yours.
Professional programmer, degree in Informatics and Applied Systems Science.