Continuous shooting with spacebar

Started by Duckbutcher, Sun 16/09/2018 06:53:39

Previous topic - Next topic

Duckbutcher

Hey people, hope you're all well.

I'm playing about with a keyboard controlled top down shooter sort of game. Arrow keys to move and space to fire so far.

I'm using a character (cfire) as the projectile, and have this character binded behind the player character using FOLLOW_EXACTLY.

Upon pressing the space bar, the game checks which direction the character is facing and moves the projectile 50 degrees away from the character in the appropriate direction. There is a short timer which allows the projectile to achieve this distance before respawning behind the player character for the next shot. It works pretty good.

My problem is I have to repeatedly hammer the space bar in order for this to work - holding the spacebar produces some odd effects, ie the projectile slowing to aãâ,¬â,¬snail's pace before stopping suspended in the air. Ideally I don't want to give my players broken wrists, so if there's a simple way to have the projectile fire at a steady rate while holding the spacebar down, I'd be really grateful if you could let me know! I suspect the problem may be something to do with the timer but I'm not sure what to do.

Here's the code, from globalscript function on keypress.

Code: ags


  // FUNCTION KEYS AND SYSTEM SHORTCUTS
  
  if (keycode == eKeySpace){
   cfire.FollowCharacter(null);
   if (player.Loop==2){
   cfire.FaceDirection(eDirectionRight);
   cfire.Move(cego.x+50, cego.y+0, eNoBlock, eAnywhere);
   SetTimer(1, 6); 
   }
   if (player.Loop==1){
   cfire.FaceDirection(eDirectionLeft);
   cfire.Move(cego.x-50, cego.y-0, eNoBlock, eAnywhere);
   SetTimer(1, 8); 
   }
   if (player.Loop==0){
   cfire.FaceDirection(eDirectionDown);
   cfire.Move(cego.x+0, cego.y+50, eNoBlock, eAnywhere);
   SetTimer(1, 8); 
   }
   if (player.Loop==3){
   cfire.FaceDirection(eDirectionUp);
   cfire.Move(cego.x-0, cego.y-50, eNoBlock, eAnywhere);
   SetTimer(1, 8); 
    }
  }


And here is the timer check in global rep- ex.

Code: ags

  if (IsTimerExpired(1)){
   cfire.x=cego.x;
   cfire.y=cego.y;
   cfire.FollowCharacter(cego, FOLLOW_EXACTLY, 1);
   return;
  } 

Slasher

For a start i would increase the bullets speed... and 6 on a timer is slow, 40 is a second,...
I generally go about this scenario differently....

Duckbutcher

I'm open to suggestions, if there's a better way to tackle this situation I'm all ears. I've never attempted to do something like this before and as ags isn't really set up for this kind of game I kind of just threw stuff at the wall to see what would stick..!

Mandle

There's something like "IsKeyPressed"

Khris

Quote from: Duckbutcher on Sun 16/09/2018 08:50:44as ags isn't really set up for this kind of game
Not quite true; the engine is quite flexible and can easily deal with held down keys.
All you need to do is use repeatedly_execute and IsKeyPressed() instead of on_key_press().

Monsieur OUXX

#5
What people said. There's some interesting ideas mixed with some clunky aspects in the solution.
I defintely don't get what FollowCharacter has to do with it.

I'd go like this :
(not compiled, not tested)

Code: ags

#define MAX_PROJECTILES 6 //if you want to be able to fire repeatedly
#define COOLDOWN 30 //we allow the player to shoot once every 30 frames, which is approximately 500ms at 60FPS
#define PROJECTILE_SPEED 1.0 //how much the projectile moves during each frame
#define PROJECTILE_LIFESPAN 240 //A projectile can live up to 240 game cycles, which is 4 seconds at 60FPS

struct ProjectileStruct {
    bool inUse;
    Character* c;
    float x; //coordinates in float to free ourselves from moving the projectiles at speeds that must match exactly screen pixels
    float y;
    float angleInRadians; //Reminder : 360 degrees = 2*Pi
    int age; //how many game cycles have elapsed since it was created
};
ProjectileStruct projectiles[MAX_PROJECTILES];
int coolDown;




void MakeProjectile(int p)
{
  projectiles[p].inUse = true;
  
   int direction = -1;
   float angle = 0.0;
   int xOffset = 0; int yOffset = 0;
   
   switch (player.Loop){
	case 0 : 
		direction = eDirectionDown; xOffset = 0; yOffset = 50; angle = 3.0*Maths.Pi/2.0; // (3/2)Pi = 270 degrees = down
		break;
	case 1 : 
		direction = eDirectionLeft; xOffset = -50; yOffset = 0; angle = 2.0*Maths.Pi; // 2Pi = 180 degrees = left
		break;	
	case 2 : 
		direction = eDirectionRight; xOffset = 50; yOffset = 0; angle = 0.0; // 0 = 0 degrees = right
		break;	
	case 3 : 
		direction = eDirectionUp; xOffset = -50; yOffset = 0; angle = Maths.Pi/2.0; // Pi/2 = 90 degrees = up
		break;	
   }
   projectiles[p].c.Visible = true;
   projectiles[p].c.Move(cego.x+xOffset, cego.y+yOffset, eNoBlock, eAnywhere);
   projectiles[p].c.FaceDirection(direction);
   projectiles[p].x = IntToFloat(projectiles[p].c.x);
   projectiles[p].y = IntToFloat(projectiles[p].c.y);

   projectiles[p].age = 0;
}


//Update projectiles location at each screen refresh
void MoveProjectiles()
{

  for (int p=0; p<MAX_PROJECTILES; p++) {
	if(projectiles[p].inUse) { //in use
	  //Our accurate locations, in float
	  projectiles[p].x+=Maths.Cos(projectiles[p].angle)*PROJECTILE_SPEED;
	  projectiles[p].y+=Maths.Sin(projectiles[p].angle)*PROJECTILE_SPEED;
	  
	  //Our on-screen locations, in pixels (rounded from the float value)
	  projectiles[p].c.x = FloatToInt(projectiles[p].x);
	  projectiles[p].c.y = FloatToInt(projectiles[p].y);
	}
  }

}

//Kill projectiles that have been around for too long
void KillProjectiles()
{

  for (int p=0; p<MAX_PROJECTILES; p++) {
    if (projectiles[p].inUse) {
	  projectiles[p].age++;
	  if (projectiles[p].age > PROJECTILE_LIFESPAN) {
		  projectiles[p].inUse = false;
		  projectiles[p].c.Visible = false;
		  projectiles[p].age = 0;
	  }
	}
  }

}

void ManageShooting()
{
  if (IsKeyPressed(eKeySpace)) {
    if(coolDown==0) {
      coolDown = COOLDOWN; //reset cooldown
      //Find a projectile that's not in use
      for (int i=0; i<MAX_PROJECTILES; i++) {
        if(!projectiles[i].inUse) { //found!
		  MakeProjectile(i);
		  break; //stop looking immediately
		}
      }
    }
  }
}

void repeatedly_execute()
{
  if (coolDown > 0) 
    coolDown--;

  ManageShooting();
  MoveProjectiles();
  KillProjectiles();

}


//Bind your in-game projectiles with some actual characters from your game, that you created in the editor
void InitProjectiles()
{
  projectiles[0].c = cFire0;
  projectiles[1].c = cFire1;
  projectiles[2].c = cFire2;
  projectiles[3].c = cFire3;
  projectiles[4].c = cFire4;
  projectiles[5].c = cFire5;
}

void game_start() 
{
  SetGameSpeed(60); //600FPS is probably better than AGS' weird native 40FPS
  InitProjectiles();
}

 

Duckbutcher

Monsieur, looks like you put in a lot of work there, thanks. And regarding the follow character command, this was how I was masking the projectile - hiding it behind the player character ready to be called to move. I guess it does seem a bit daft but I was making it up as I went along.

Regarding your code, it's way beyond my current level of understanding and while I can understand the general gist, I've put it into my game and got a couple of errors. Float angle = 0 under void makeprojectile throws up a “can't convert int to float” error, and after pulling bits of the code apart to see how they work in isolation I also get an error that “math” is undefined. At the moment everything is at the top of global script, so if it needs to be anywhere specific please let me know.

Thanks for your time, appreciate it.

dayowlron

in line 25 change it to:
Code: ags
 float angle=0.0;

and in lines 30,33, and 39 add an s onto Math to
Code: ags
 ... Maths.Pi 
Pro is the opposite of Con                       Kids of today are so much different
This fact can clearly be seen,                  Don't you know?
If progress means to move forward         Just ask them where they are from
Then what does congress mean?             And they tell you where you can go.  --Nipsey Russell

Monsieur OUXX

OK, if FollowCharacer was used to position the projectile at the same location as the player at the beginning of the shooting, then it makes sense, but I have a tendency not to trust an elaborate function to just teleport my object one time to a given location. I prefer to change .x and .y and be done with it. FollowCharacter might force the projectile to keep following the character or some unforeseen side effects of the same sort.

My code works as follows :

1) A bunch of #define to create some constants (that's some oddity inherited from ye olde C language).

2) Create 6 projectiles that I put in an array

3) the projectiles are "objects" (structs) where I store all the variables needed (x, y, angle, etc.). Each projectile also contains one Character* c, which is an actual AGS Character used to represent the projectile on-screen and move it around, just like you did yourself.

4) At game start, I bind Character* c of each projectile in the array to an actual AGS character that you must have created beforehand in the editor. It's the same idea as your dummy "cFire" character, except now there's 6 of them.

5) At each game loop, we check if the Space key is down and if the cooldown has elapsed (to prevent infinite instant firing). If yes, then we check if one of the 6 characters is "not in use" already, and if yes we place it at the same screen position as the player (+ or - 50 pixels, to make it start from the player's gun location) and give it an angle corresponding to the player's orientation.

6) At each game loop, we update the location of every projectile that is currently in use. that's your typical x = x+cos(angle); y=y+sin(angle)

7) At each game loop, we delete every projectile that is in use but has become too old. It's a way of recycling them to allow new projectiles to be created.
 

Duckbutcher

So I had a bit of a tinker with this again today. Thanks for the corrections dayolron and thanks again Monsieur for the clarification, though I was more apologising for my inevitable questions! That makes everything a lot clearer, appreciated.

I've changed a couple of things, including changing a projectiles[p].c.Visible=true; to projectiles[p].c.on=1; as (I think) characters have to be turned on and off like this rather than with .Visible=.

I've also hit another snag, regarding this bit:

Code: ags

//Update projectiles location at each screen refresh
void MoveProjectiles()
{
 
  for (int p=0; p<MAX_PROJECTILES; p++) {
        if(projectiles[p].inUse) { //in use
          //Our accurate locations, in float
          projectiles[p].x+=Maths.Cos(projectiles[p].angle)*PROJECTILE_SPEED;
          projectiles[p].y+=Maths.Sin(projectiles[p].angle)*PROJECTILE_SPEED;
          
          //Our on-screen locations, in pixels (rounded from the float value)
          projectiles[p].c.x = FloatToInt(projectiles[p].x);
          projectiles[p].c.y = FloatToInt(projectiles[p].y);
        }
  }
 
}


Error is '.angle' is not a public member of 'ProjectileStruct'.

Monsieur OUXX

#10
Quote from: Duckbutcher on Thu 20/09/2018 16:17:09
Error is '.angle' is not a public member of 'ProjectileStruct'.

Well as you can see here :
Code: ags

struct ProjectileStruct
{
   ...
   float angleInRadians;
}


The member is named angleInRadians, not angle. Just change the name accordingly everywhere I made the mistake.

Since we're there, I suggest you change this :
#define PROJECTILE_SPEED 1.0
to this :
#define PROJECTILE_SPEED 5.0

This will make projectiles move 5 pixels during each game loop, instead of only 1 (they would be super slow and probably overlap)
 

SMF spam blocked by CleanTalk