Click here to Skip to main content
Click here to Skip to main content

Sample Pamper Series - Part 4: 2/3D Space Game, Simple Sounds, and Threads

, 4 Apr 2012 CPOL
Rate this:
Please Sign up or sign in to vote.
This article is the final one in the series, and it will give you a 2/3D space game out of what we have learnt from previous articles. We will also apply simple sounds to it played in threads.

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

Screenshot - SB_screen2.gif

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

Screenshot - class1.gif

Screenshot - class2.gif

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; //Mouse coordinates from the client app.

  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.

  //Will get the degree of object i polygon m
  //in the rotated mesh, this though the Object3D class.

  public double GetDegree(int i, int m)
  {
   return ((IObject3D)this.mObjects[i]).GetDegree(m);
  }
  //Will set the object i polygon m. The degree value.

  public void SetDegree(int i, int m, double degree)
  {
   ((IObject3D)this.mObjects[i]).SetDegree(m,degree);
  }
  //Will create the polygon mesh in this object.

  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); //Make it a player.

  }
  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); //Make it an enemy!
  }
  
  //Some other functions
 
  private void MovePlayer(Player obj)
  {
   if(obj.Energy>0)
  {
  //Calculate the power of the speed.

  obj.PaseX = ((double)this.mMouse.X- ((double)obj.ObjPos.X))/200.00;
  obj.PaseY = ((double)this.mMouse.Y- ((double)obj.ObjPos.Y))/200.00;

  //Speed Limit

  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;

  //Counter.

  obj.CountX += obj.PaseX;
  obj.CountY += obj.PaseY;

  //Give new position.

  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;

  //Screen limit detection.

  TestForScreenLimits(ref newpos);

  //Give the thrust we need.

  obj.ObjPos = newpos;
            
  //And rotate the player.

  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));

    //Within range to player!

    if(dist>0 && dist<140 && !obj.InWar && pl.Energy>0)
    {
     //Calculate degrees to target.

     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;

   //Screen limit detection.

   TestForScreenLimits(ref temp);

   obj.ObjPos = temp;

   ColDetection(obj);
  }
 }

 //Astroids kind of thingy

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

//3D horizontal line

 private void HorizontalLine3D(ref Bitmap bm, ref uint[][] zbuffer,
   Poinx3D p1, Poinx3D p2, Color color, double intencity)
 {
  //Delta lengths

  int dx = (int)(p2.X-p1.X);
  int dz = (int)(p2.Z-p1.Z);
            
  //Direction pointers.

  int step_x = 0;
  int step_z = 0;

  //Moving right step +1 else -1

  if(dx>=0)
   step_x = 1;
  else
  {
   step_x = -1;
   dx = -dx;
  }
  if(dz>=0)
   step_z = 1;
  else
  {
   step_z = -1;
   dz = -dz;
  }
            
  //You need this to make the err_term work.
  //Because we are using integers we must multiply with 2
  //otherwise we would just start with error 0.5

  int dx2 = dx*2; //delta X * 2 instead of 0.5
  int dz2 = dz*2; //delta Z * 2 ..
  int err_termXZ = 0;
        
  //Unified things.

  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;
  }
            
  //Step x direction by one until the end of width.

  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);
    }
   }

   //Adjust error_term

   //This if it's time to do so.

   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); //casting hell yeah right!

  normal.sY = ((double)normal.sY / len); //casting hell yeah right!
  normal.sZ = ((double)normal.sZ / len); //casting hell yeah right!


  return normal; //Always return even if ref!

}

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.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Windmiller

Sweden Sweden
Professional programmer, degree in Informatics and Applied Systems Science.

Comments and Discussions

 
QuestionSpaceBox PinmemberSteve Noll4-Apr-12 4:35 
AnswerRe: SpaceBox [modified] PinmemberWindmiller4-Apr-12 13:03 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Terms of Use | Mobile
Web03 | 2.8.141216.1 | Last Updated 5 Apr 2012
Article Copyright 2007 by Windmiller
Everything else Copyright © CodeProject, 1999-2014
Layout: fixed | fluid