Trying to simulate the flamethrower from GTA2

Started by
12 comments, last by Norman Barrows 10 years, 5 months ago

Hello everyone,

I have got a simple game running on android - I have a character drawn in Paint.NET and some zombies as well. Here is a screenshot:
9gj5GUM.png

As you can see, there are two "thumb pads" which I use to control the character - left one is for moving and the right one is for rotating and shooting.

So far, I have implemented only a gun for the player to shoot with. However, I'd also like to add a flamethrower. I remembered that the flamethrower in GTA2 looked fluid and decided to try and copy it. For those of you who do not know how the flamethrower in GTA2 works here is a 33 second video video:

I really like the fluidity of the flames and how they bend, for example at 0:27 when the player starts rotating.

My initial idea was to create a simple fire sprite (16x16) which I spawn from the top of the gun of the player and move in the appropriate direction. However, I could not achieve the fluidity I wanted.

This leads to my question - does someone know how I can replicate the flamethrower from GTA2? What information I would need to store to be able to do all the calculations, etc.

Advertisement

I am not sure what is the problem with the flame you implemented, you should post a picture...

I will assume you got a straight line, even when the player turns.

If that is the case, then what you need to do is:

Your flame thrower should be a queue of flames.

Every frame, create a new flame, and delete the ones at the end of the queue (the oldest flames).

When you create a new flame, store that flame's angle.

That way, if you are turning, each flame should be at a slightly different direction.

Also, you might want to draw the flames differently, depending on where they are in the queue. The "older" flames should be bigger, because fireballs expand over time.

My Oculus Rift Game: RaiderV

My Android VR games: Time-Rider& Dozer Driver

My browser game: Vitrage - A game of stained glass

My android games : Enemies of the Crown & Killer Bees

The way I have implemented the flamethrower is this:

Every update I add a new flame to the end of the player's gun. I use an ArrayList to store the flames. Every flame has an angle (the same one as the player) and a direction (going away from the player). When the flame has traveled a set distance, I remove it from the array list. Here is a screenshot:

XhkmKQR.png

Also, the flame sprite:

yhtGIRD.png


I like the queue idea. However, what I'm really aiming at is to achieve the "bending" that the flames from gta2 do when the player rotates (seen at 00:27 in the video) I have no idea how I have to manipulate the flames to get such an effect.

Do you mean that the near part of the flame in GTA is straighter, then the far part of the flame?

If so, make your flame decelerate as it get farther from your gun.

I will assume you are currently multiplying your flame's direction by the time elapsed.

Change it so that the more time elapsed, the farther & slower your flame should be.

instead of multiplying by time, multiply by some root of time.

Instead of:


Position = FlameDirection * Time

Do either:


Position = FlameDirection * sqrt(Time)

or:


Position = FlameDirection * Math.pow( Time , float_number_smaller_than_one)

The smaller float_number_smaller_than_one is, the more "bending" you will have.

My Oculus Rift Game: RaiderV

My Android VR games: Time-Rider& Dozer Driver

My browser game: Vitrage - A game of stained glass

My android games : Enemies of the Crown & Killer Bees

What I mean is that in GTA2 when the player is stationary the endpoint of the flame is a given distance away from the player. However, when the player starts rotating, the endpoint of the flame is closer to the player. After the player stops rotating, the flame "straightens".

The way I'm calculating the positions of each flame object is by multiplying the speed of the flame by the sin() and cos() of the angle it is moving in and adding the result to the Y and X coordinates respectively.

1. Their player is walking as he is spinning. Is your player advancing while spinning?

2. In the GTA demo video, the player is not the origin of the flame. The end of the gun is the origin of the flame. If you look at your example, and move your player "back" a little bit you will see that the player get's closer to rotated flames.

3. You are not multiplying by speed. You are multiplying by speed * time. If you were multiplying by speed, your flames would not move (Speed is constant).

So, for simplicty: let's take just the Cos component of your flame. The same will work for cos. (This would be easier if you publish some code).

I assume what you are doing now is:


FlameX = FlameOriginX + Cos( FlameDirection ) * Speed * FlameTime

Where FlameTime is how long ago this flame was created, or it's index in the array, or whatever you use to measure when this particular flame should die.

You need to do two things:

1. Make the origin far away from your player:


FlameX = FlameOriginX + Cos( FlameDirection ) * LengthOfGun + Cos( FlameDirection ) * Speed *FlameTime

2. Then add deceleration:


FlameX = FlameOriginX + Cos( FlameDirection ) * LengthOfGun + Cos( FlameDirection ) * Speed * Sqrt(FlameTime)

Do the same for Y and Sin.

EDIT: You can simplify algebrically a little bit:


FlameX = FlameOriginX + Cos( FlameDirection ) * (  LengthOfGun + Speed * Sqrt(FlameTime) )

My Oculus Rift Game: RaiderV

My Android VR games: Time-Rider& Dozer Driver

My browser game: Vitrage - A game of stained glass

My android games : Enemies of the Crown & Killer Bees

I have downloaded GTA2 specifically to test this. If the player is not moving, only rotating, the endpoint of the flame gets closer to the player. So, whether the player is moving in any direction is not significant.

As for the formulas you posted - I do not really understand the need to multiply by the time.

Let's look at the X coordinate of a flame that is moving at an angle of 60 degrees with a speed of 5. The X coordinate should be increased by the cos(angle) * 5 every frame, or in other words the X coordinate should be increased by 5 / 2 pixels each update.

The Y coordinate should be increased by sin(angle) * 5 or by 5 * sqrt(3) / 2 each frame.

When you look at the increase in X and Y, the overall increase in distance is:
sqrt(25/4 + 75/4) = 10/2 = 5

EDIT: After looking at what you posted, it is the same thing. However, you increase the X coordinate with cos(angle) * speed every millisecond (for example) why I do once every 17ms (60fps)

I see what you are doing here...

In my example I do not change the coordinate of the flames,

I calculate them from scratch every frame.

You are using a discrete system, while I am using a continuous one.

It doesn't matter though, using your discrete system we can achieve the same result:

1. Set the initial coordinate for the flame to be farther away from your "man". It should be exactly "gun-length" far from your man in the direction it is pointing.

2. To achieve deceleration:

For every frame decrease the speed of your particle.

You don't store speed for every particle?

Have no fear, you can extrapolate the speed by how much time your particle has existed: How long does it have until it needs to be destroyed? That is your time.

The example you posted is bad. When you are saying: "The Y coordinate should be increased by 'constant' " you are note changing your speed, and therefore not decelerating.

PS: Time is not 1ms, it is in whatever unit you decide to measure it. In most of my games, I measure time in logic-frames which are 20ms (50fps).

My Oculus Rift Game: RaiderV

My Android VR games: Time-Rider& Dozer Driver

My browser game: Vitrage - A game of stained glass

My android games : Enemies of the Crown & Killer Bees


//---------------------------------------------------------------------------


#pragma hdrstop

#include "WEAPON_FLAMETHROWER.h"
//#include "FC_OPENGL.h"

//---------------------------------------------------------------------------

#pragma package(smart_init)

void __fastcall  drawbillboardF(float cx, float cy, float cz, float size)
{
double m_PickInfo_ModelView[16];// of ;

  t3dpoint Pos;
glGetDoublev(GL_MODELVIEW_MATRIX, m_PickInfo_ModelView);

BillboardX.x = m_PickInfo_ModelView[0];
BillboardX.y = m_PickInfo_ModelView[4];
BillboardX.z = m_PickInfo_ModelView[8];


BillboardY.x =	m_PickInfo_ModelView[1];
BillboardY.y =	m_PickInfo_ModelView[5];
BillboardY.z =	m_PickInfo_ModelView[9];



glBegin(GL_QUADS);

glTexCoord2f(0.0, 1.0);

Pos.x = cx-BillboardX.x*size+BillboardY.x*size;
Pos.y = cy-BillboardX.y*size+BillboardY.y*size;
Pos.z = cz-BillboardX.z*size+BillboardY.z*size;

glVertex3f(Pos.x,Pos.y,Pos.z);
glTexCoord2f(1.0, 1.0);

Pos.x = cx+BillboardX.x*size+BillboardY.x*size;
Pos.y = cy+BillboardX.y*size+BillboardY.y*size;
Pos.z = cz+BillboardX.z*size+BillboardY.z*size;

glVertex3f(Pos.x,Pos.y,Pos.z);

glTexCoord2f(1.0, 0.0);
Pos.x = cx+BillboardX.x*size-BillboardY.x*size;
Pos.y = cy+BillboardX.y*size-BillboardY.y*size;
Pos.z = cz+BillboardX.z*size-BillboardY.z*size;
glVertex3f(Pos.x,Pos.y,Pos.z);

glTexCoord2f(0.0, 0.0);
Pos.x = cx-BillboardX.x*size-BillboardY.x*size;
Pos.y = cy-BillboardX.y*size-BillboardY.y*size;
Pos.z = cz-BillboardX.z*size-BillboardY.z*size;
glVertex3f(Pos.x,Pos.y,Pos.z);

glEnd();

}



void __fastcall  tflamethrower::DRAW()
{
int i;
float d; t3dpoint v;


make_next_frame();
//glpushmatrix();
glColor4f(1,1,1,0.7);
glDepthMask(GL_FALSE);

glEnable(GL_BLEND);
//
glBlendFunc(GL_ONE,GL_ONE);
//glEnable(GL_TEXTURE_2D);
for (i = 0; i < 129; i++)
if (particles[i].draw == true)
{
//	                                                                 glbindtexture(GL_TEXTURE_2D,sprite.animtextures[particles[i].frame]);


// particles[i].size = 300.0f;
drawbillboardF(particles[i].pos.x,particles[i].pos.y,particles[i].pos.z,particles[i].size);
 //glrotate(-angle,1,0,0);
//  glmatrixmode(GL_TEXTURE);glrotate(-angle,0,1,0); glmatrixmode(GL_MODELVIEW);
//glpopmatrix();

}


//glDisable(GL_TEXTURE_2D);
glDisable(GL_BLEND);
glDepthMask(GL_TRUE);
}


  __fastcall tflamethrower::tflamethrower()
  {
   sprite = new tglanimtexture;
  }
  __fastcall tflamethrower::~tflamethrower()
  {
   sprite->free();
   delete sprite;
  }

void __fastcall  tflamethrower::load(AnsiString filename)
{
 sprite->loadtex(filename);
}


void __fastcall  tflamethrower::make_next_frame()
{
try {
int i;
t3dpoint k;

for (i = 0; i < 129; i++)
if (particles[i].draw == true)
{

k.x = 100000000.0f*(sin(particles[i].glop*imopi)*cos(particles[i].heading*imopi));
k.z = 100000000.0f*(cos(particles[i].glop*imopi)*cos(particles[i].heading*imopi));
k.y = 100000000.0f*(sin(particles[i].heading*imopi));
 particles[i].size        = (float(GetTickCount())-particles[i].time)/100.0f;
  k=Normalize(k);


   if (NORMAL_TIME == 0) {        //czyli jak false
	   particles[i].pos = vectors_frontadd(particles[i].startpos,vector_multiple(k,power*(GetTickCount()-particles[i].time)/1000.0f));
   } else {

 particles[i].pos = vectors_frontadd(particles[i].pos,vector_multiple(k,2.00*power*(GetTickCount()-particles[i].time)/1000.0f)); //end else
		   }
particles[i].time = GetTickCount();
//					 particles[i].rotate = particles[i].rotate +1;
/*if (GetTickCount()-particles[i].frametime >= sprite.speed ) {
	  particles[i].frametime = GetTickCount();

//particles[i].frame = particles[i].frame + 1;       odkomentowac
//if (particles[i].frame > high(sprite.animtextures)  )then begin particles[i].frame := 0; particles[i].draw := false; end; odkomenotwac

}

  */

checkparticle(i);
}

	 } catch (...) {

	 }
}
                                     //tutaj jak wielkosc przekracza maks wielkosc to usuwamy z rysowania particle
void __fastcall tflamethrower::checkparticle(int index)
{
//if ( particles[index].size >=  5000.0f ) particles[index].draw = false;

if (n3ddistance(particles[index].startpos,particles[index].pos) > 1000.0f)       
                               particles[index].draw = false;

}

void __fastcall tflamethrower::throw_next_free(t3dpoint pos, float  glop, float heading)
{
int i;
bool found;

if ( (GetTickCount()-shot_time) < SHOT_SPEED ) return;
shot_time = GetTickCount();
already_shot = true;
found = false;
for (i = 0; i < 129; i++)
if (particles[i].draw == false)
{
found = true;
last_fired_particle = i; break;
}

if (found == false){ last_fired_particle = 0; }

 particles[last_fired_particle].draw = true;
 particles[last_fired_particle].size        = 300;
 particles[last_fired_particle].startpos    = pos;
 particles[last_fired_particle].pos    = pos;
 particles[last_fired_particle].frame       = 0;
 particles[last_fired_particle].time        = GetTickCount();
 particles[last_fired_particle].glop        = glop;
 particles[last_fired_particle].heading     = heading;
 particles[last_fired_particle].frametime   = GetTickCount();
// particles[last_fired_particle].hitpos      = shot_pos;
// particles[last_fired_particle].hitindex    = shot_index;
 particles[last_fired_particle].rotate      = random(359);
 particles[last_fired_particle].rot_time    = GetTickCount();
 particles[last_fired_particle].stan = 1;


}

void __fastcall tflamethrower::reset()
{
	int i;
//RYSOWALEM_swiatlo[0] = false;
//RYSOWALEM_swiatlo[1] = false;
//RYSOWALEM_swiatlo[2] = false;
for (i = 0; i < 129; i++) particles[i].draw = false;
}

void __fastcall tflamethrower::setallinpos(t3dpoint pos)
{
int i;
for (i = 0; i < 129; i++) particles[i].startpos = pos;



}


//---------------------------------------------------------------------------

#ifndef WEAPON_FLAMETHROWERH
#define WEAPON_FLAMETHROWERH
//---------------------------------------------------------------------------
							 #include "DxcMath.h"
							 #include "glAnimTexture.h"

//const
//MAX_FLAME_SPRITE_SIZE = 5000;
   t3dpoint BillboardX;// : t3dpoint;
t3dpoint BillboardY;// : t3dpoint;
struct tflame_part {
float 			size;
t3dpoint 		pos;
t3dpoint 		startpos;
bool 			draw;
int 	frame;
int 	time;
int 	frametime;
float glop;
float heading;
float rotate;
int 		rot_time;
t3dpoint 	hitpos;
int 		stan;
bool 		stanC;
int 		hitindex;
t3dpoint odbity_wektor;
};

class tflamethrower {
public:
int NORMAL_TIME;
tglanimtexture * sprite;
float power;
tflame_part particles[129];
int last_fired_particle;
 int SHOT_SPEED;
 bool already_shot;
 int shot_time;
//RYSOWALEM_swiatlo : array[0..2] of boolean;

  __fastcall tflamethrower();
  __fastcall ~tflamethrower();
void __fastcall reset();
void __fastcall setallinpos(t3dpoint pos);
void __fastcall throw_next_free(t3dpoint pos, float  glop, float heading);
void __fastcall checkparticle(int index);
void __fastcall load(AnsiString filename);
void __fastcall DRAW();
private:
void __fastcall make_next_frame();



};

#endif

here is a sample what i did, uses time, works as intended.


//---------------------------------------------------------------------------


#pragma hdrstop

#include "WEAPON_FLAMETHROWER.h"
//#include "FC_OPENGL.h"

//---------------------------------------------------------------------------

#pragma package(smart_init)

void __fastcall  drawbillboardF(float cx, float cy, float cz, float size)
{
double m_PickInfo_ModelView[16];// of ;

  t3dpoint Pos;
glGetDoublev(GL_MODELVIEW_MATRIX, m_PickInfo_ModelView);

BillboardX.x = m_PickInfo_ModelView[0];
BillboardX.y = m_PickInfo_ModelView[4];
BillboardX.z = m_PickInfo_ModelView[8];


BillboardY.x =	m_PickInfo_ModelView[1];
BillboardY.y =	m_PickInfo_ModelView[5];
BillboardY.z =	m_PickInfo_ModelView[9];



glBegin(GL_QUADS);

glTexCoord2f(0.0, 1.0);

Pos.x = cx-BillboardX.x*size+BillboardY.x*size;
Pos.y = cy-BillboardX.y*size+BillboardY.y*size;
Pos.z = cz-BillboardX.z*size+BillboardY.z*size;

glVertex3f(Pos.x,Pos.y,Pos.z);
glTexCoord2f(1.0, 1.0);

Pos.x = cx+BillboardX.x*size+BillboardY.x*size;
Pos.y = cy+BillboardX.y*size+BillboardY.y*size;
Pos.z = cz+BillboardX.z*size+BillboardY.z*size;

glVertex3f(Pos.x,Pos.y,Pos.z);

glTexCoord2f(1.0, 0.0);
Pos.x = cx+BillboardX.x*size-BillboardY.x*size;
Pos.y = cy+BillboardX.y*size-BillboardY.y*size;
Pos.z = cz+BillboardX.z*size-BillboardY.z*size;
glVertex3f(Pos.x,Pos.y,Pos.z);

glTexCoord2f(0.0, 0.0);
Pos.x = cx-BillboardX.x*size-BillboardY.x*size;
Pos.y = cy-BillboardX.y*size-BillboardY.y*size;
Pos.z = cz-BillboardX.z*size-BillboardY.z*size;
glVertex3f(Pos.x,Pos.y,Pos.z);

glEnd();

}



void __fastcall  tflamethrower::DRAW()
{
int i;
float d; t3dpoint v;


make_next_frame();
//glpushmatrix();
glColor4f(1,1,1,0.7);
glDepthMask(GL_FALSE);

glEnable(GL_BLEND);
//
glBlendFunc(GL_ONE,GL_ONE);
//glEnable(GL_TEXTURE_2D);
for (i = 0; i < 129; i++)
if (particles[i].draw == true)
{
//	                                                                 glbindtexture(GL_TEXTURE_2D,sprite.animtextures[particles[i].frame]);


// particles[i].size = 300.0f;
drawbillboardF(particles[i].pos.x,particles[i].pos.y,particles[i].pos.z,particles[i].size);
 //glrotate(-angle,1,0,0);
//  glmatrixmode(GL_TEXTURE);glrotate(-angle,0,1,0); glmatrixmode(GL_MODELVIEW);
//glpopmatrix();

}


//glDisable(GL_TEXTURE_2D);
glDisable(GL_BLEND);
glDepthMask(GL_TRUE);
}


  __fastcall tflamethrower::tflamethrower()
  {
   sprite = new tglanimtexture;
  }
  __fastcall tflamethrower::~tflamethrower()
  {
   sprite->free();
   delete sprite;
  }

void __fastcall  tflamethrower::load(AnsiString filename)
{
 sprite->loadtex(filename);
}


void __fastcall  tflamethrower::make_next_frame()
{
try {
int i;
t3dpoint k;

for (i = 0; i < 129; i++)
if (particles[i].draw == true)
{

k.x = 100000000.0f*(sin(particles[i].glop*imopi)*cos(particles[i].heading*imopi));
k.z = 100000000.0f*(cos(particles[i].glop*imopi)*cos(particles[i].heading*imopi));
k.y = 100000000.0f*(sin(particles[i].heading*imopi));
 particles[i].size        = (float(GetTickCount())-particles[i].time)/100.0f;
  k=Normalize(k);


   if (NORMAL_TIME == 0) {        //czyli jak false
	   particles[i].pos = vectors_frontadd(particles[i].startpos,vector_multiple(k,power*(GetTickCount()-particles[i].time)/1000.0f));
   } else {

 particles[i].pos = vectors_frontadd(particles[i].pos,vector_multiple(k,2.00*power*(GetTickCount()-particles[i].time)/1000.0f)); //end else
		   }
particles[i].time = GetTickCount();
//					 particles[i].rotate = particles[i].rotate +1;
/*if (GetTickCount()-particles[i].frametime >= sprite.speed ) {
	  particles[i].frametime = GetTickCount();

//particles[i].frame = particles[i].frame + 1;       odkomentowac
//if (particles[i].frame > high(sprite.animtextures)  )then begin particles[i].frame := 0; particles[i].draw := false; end; odkomenotwac

}

  */

checkparticle(i);
}

	 } catch (...) {

	 }
}
                                     //tutaj jak wielkosc przekracza maks wielkosc to usuwamy z rysowania particle
void __fastcall tflamethrower::checkparticle(int index)
{
//if ( particles[index].size >=  5000.0f ) particles[index].draw = false;

if (n3ddistance(particles[index].startpos,particles[index].pos) > 1000.0f)       
                               particles[index].draw = false;

}

void __fastcall tflamethrower::throw_next_free(t3dpoint pos, float  glop, float heading)
{
int i;
bool found;

if ( (GetTickCount()-shot_time) < SHOT_SPEED ) return;
shot_time = GetTickCount();
already_shot = true;
found = false;
for (i = 0; i < 129; i++)
if (particles[i].draw == false)
{
found = true;
last_fired_particle = i; break;
}

if (found == false){ last_fired_particle = 0; }

 particles[last_fired_particle].draw = true;
 particles[last_fired_particle].size        = 300;
 particles[last_fired_particle].startpos    = pos;
 particles[last_fired_particle].pos    = pos;
 particles[last_fired_particle].frame       = 0;
 particles[last_fired_particle].time        = GetTickCount();
 particles[last_fired_particle].glop        = glop;
 particles[last_fired_particle].heading     = heading;
 particles[last_fired_particle].frametime   = GetTickCount();
// particles[last_fired_particle].hitpos      = shot_pos;
// particles[last_fired_particle].hitindex    = shot_index;
 particles[last_fired_particle].rotate      = random(359);
 particles[last_fired_particle].rot_time    = GetTickCount();
 particles[last_fired_particle].stan = 1;


}

void __fastcall tflamethrower::reset()
{
	int i;
//RYSOWALEM_swiatlo[0] = false;
//RYSOWALEM_swiatlo[1] = false;
//RYSOWALEM_swiatlo[2] = false;
for (i = 0; i < 129; i++) particles[i].draw = false;
}

void __fastcall tflamethrower::setallinpos(t3dpoint pos)
{
int i;
for (i = 0; i < 129; i++) particles[i].startpos = pos;



}


//---------------------------------------------------------------------------

#ifndef WEAPON_FLAMETHROWERH
#define WEAPON_FLAMETHROWERH
//---------------------------------------------------------------------------
							 #include "DxcMath.h"
							 #include "glAnimTexture.h"

//const
//MAX_FLAME_SPRITE_SIZE = 5000;
   t3dpoint BillboardX;// : t3dpoint;
t3dpoint BillboardY;// : t3dpoint;
struct tflame_part {
float 			size;
t3dpoint 		pos;
t3dpoint 		startpos;
bool 			draw;
int 	frame;
int 	time;
int 	frametime;
float glop;
float heading;
float rotate;
int 		rot_time;
t3dpoint 	hitpos;
int 		stan;
bool 		stanC;
int 		hitindex;
t3dpoint odbity_wektor;
};

class tflamethrower {
public:
int NORMAL_TIME;
tglanimtexture * sprite;
float power;
tflame_part particles[129];
int last_fired_particle;
 int SHOT_SPEED;
 bool already_shot;
 int shot_time;
//RYSOWALEM_swiatlo : array[0..2] of boolean;

  __fastcall tflamethrower();
  __fastcall ~tflamethrower();
void __fastcall reset();
void __fastcall setallinpos(t3dpoint pos);
void __fastcall throw_next_free(t3dpoint pos, float  glop, float heading);
void __fastcall checkparticle(int index);
void __fastcall load(AnsiString filename);
void __fastcall DRAW();
private:
void __fastcall make_next_frame();



};

#endif

here is a sample what i did, uses time, works as intended.

Thank you for writing all of that! However, I have still not started using OpenGL (I'm drawing on Android using a Canvas object) so I find it hard to understand what you are doing. Can you outline the logic behind your code so that I can try and implement it in my project?

This topic is closed to new replies.

Advertisement