block by joyrexus 11220109

Bullet trajectories

Full Screen

Click or tap to fire an artillery shell. Hold to fire multiple shots.

The trajectory is calculated and plotted based on the firing angle, gravity, and initial bullet velocity.

Notes

All code here is taken straight from the Game Mechanic Explorer, a collection of concrete examples for various game mechanics, algorithms, and effects. The examples are all implemented in JavaScript using the Phaser game framework, but the concepts and methods are general and can be adapted to any engine.

index.html

<!DOCTYPE html>
<meta charset="utf-8">
<script src="//cdnjs.cloudflare.com/ajax/libs/phaser/2.0.3/phaser.min.js"></script>
<body>
  <div id="game" style="overflow: hidden;"></div>
<script>
  // A GAME MECHANIC EXPLORER DEMO
  // //gamemechanicexplorer.com/#bullets-5
  var GameState = function(game) {};

  // Load images and sounds
  GameState.prototype.preload = function() {
      this.game.load.image('bullet', 'bullet.png');
      this.game.load.image('ground', 'ground.png');
      this.game.load.spritesheet('explosion', 'explosion.png', 128, 128);
  };

  // Setup the example
  GameState.prototype.create = function() {
      // Set stage background color
      this.game.stage.backgroundColor = 0x4488cc;

      // Define constants
      this.SHOT_DELAY = 300; // milliseconds (10 bullets/3 seconds)
      this.BULLET_SPEED = 800; // pixels/second
      this.NUMBER_OF_BULLETS = 20;
      this.GRAVITY = 980; // pixels/second/second

      // Create an object representing our gun
      this.gun = this.game.add.sprite(50, this.game.height - 64, 'bullet');

      // Set the pivot point to the center of the gun
      this.gun.anchor.setTo(0.5, 0.5);

      // Create an object pool of bullets
      this.bulletPool = this.game.add.group();
      for(var i = 0; i < this.NUMBER_OF_BULLETS; i++) {
          // Create each bullet and add it to the group.
          var bullet = this.game.add.sprite(0, 0, 'bullet');
          this.bulletPool.add(bullet);

          // Set its pivot point to the center of the bullet
          bullet.anchor.setTo(0.5, 0.5);

          // Enable physics on the bullet
          this.game.physics.enable(bullet, Phaser.Physics.ARCADE);

          // Set its initial state to "dead".
          bullet.kill();
      }

      // Turn on gravity
      game.physics.arcade.gravity.y = this.GRAVITY;

      // Create some ground
      this.ground = this.game.add.group();
      for(var x = 0; x < this.game.width; x += 32) {
          // Add the ground blocks, enable physics on each, make them immovable
          var groundBlock = this.game.add.sprite(x, this.game.height - 32, 'ground');
          this.game.physics.enable(groundBlock, Phaser.Physics.ARCADE);
          groundBlock.body.immovable = true;
          groundBlock.body.allowGravity = false;
          this.ground.add(groundBlock);
      }

      // Create a group for explosions
      this.explosionGroup = this.game.add.group();

      // Setup a canvas to draw the trajectory on the screen
      this.bitmap = this.game.add.bitmapData(this.game.width, this.game.height);
      this.bitmap.context.fillStyle = 'rgb(255, 255, 255)';
      this.bitmap.context.strokeStyle = 'rgb(255, 255, 255)';
      this.game.add.image(0, 0, this.bitmap);

      // Simulate a pointer click/tap input at the center of the stage
      // when the example begins running.
      this.game.input.activePointer.x = this.game.width/2;
      this.game.input.activePointer.y = this.game.height/2 - 100;

      // Show FPS
      this.game.time.advancedTiming = true;
      this.fpsText = this.game.add.text(
          20, 20, '', { font: '16px Arial', fill: '#ffffff' }
      );
  };

  GameState.prototype.drawTrajectory = function() {
      // Clear the bitmap
      this.bitmap.context.clearRect(0, 0, this.game.width, this.game.height);

      // Set fill style to white
      this.bitmap.context.fillStyle = 'rgba(255, 255, 255, 0.5)';

      // Calculate a time offset. This offset is used to alter the starting
      // time of the draw loop so that the dots are offset a little bit each
      // frame. It gives the trajectory a "marching ants" style animation.
      var MARCH_SPEED = 40; // Smaller is faster
      this.timeOffset = this.timeOffset + 1 || 0;
      this.timeOffset = this.timeOffset % MARCH_SPEED;

      // Just a variable to make the trajectory match the actual track a little better.
      // The mismatch is probably due to rounding or the physics engine making approximations.
      var correctionFactor = 0.99;

      // Draw the trajectory
      // //en.wikipedia.org/wiki/Trajectory_of_a_projectile#Angle_required_to_hit_coordinate_.28x.2Cy.29
      var theta = -this.gun.rotation;
      var x = 0, y = 0;
      for(var t = 0 + this.timeOffset/(1000*MARCH_SPEED/60); t < 3; t += 0.03) {
          x = this.BULLET_SPEED * t * Math.cos(theta) * correctionFactor;
          y = this.BULLET_SPEED * t * Math.sin(theta) * correctionFactor - 0.5 * this.GRAVITY * t * t;
          this.bitmap.context.fillRect(x + this.gun.x, this.gun.y - y, 3, 3);
          if (y < -15) break;
      }

      this.bitmap.dirty = true;
  };

  GameState.prototype.shootBullet = function() {
      // Enforce a short delay between shots by recording
      // the time that each bullet is shot and testing if
      // the amount of time since the last shot is more than
      // the required delay.
      if (this.lastBulletShotAt === undefined) this.lastBulletShotAt = 0;
      if (this.game.time.now - this.lastBulletShotAt < this.SHOT_DELAY) return;
      this.lastBulletShotAt = this.game.time.now;

      // Get a dead bullet from the pool
      var bullet = this.bulletPool.getFirstDead();

      // If there aren't any bullets available then don't shoot
      if (bullet === null || bullet === undefined) return;

      // Revive the bullet
      // This makes the bullet "alive"
      bullet.revive();

      // Bullets should kill themselves when they leave the world.
      // Phaser takes care of this for me by setting this flag
      // but you can do it yourself by killing the bullet if
      // its x,y coordinates are outside of the world.
      bullet.checkWorldBounds = true;
      bullet.outOfBoundsKill = true;

      // Set the bullet position to the gun position.
      bullet.reset(this.gun.x, this.gun.y);
      bullet.rotation = this.gun.rotation;

      // Shoot it in the right direction
      bullet.body.velocity.x = Math.cos(bullet.rotation) * this.BULLET_SPEED;
      bullet.body.velocity.y = Math.sin(bullet.rotation) * this.BULLET_SPEED;
  };

  // The update() method is called every frame
  GameState.prototype.update = function() {
      if (this.game.time.fps !== 0) {
          this.fpsText.setText(this.game.time.fps + ' FPS');
      }

      // Draw the trajectory every frame
      this.drawTrajectory();

      // Check if bullets have collided with the ground
      this.game.physics.arcade.collide(this.bulletPool, this.ground, function(bullet, ground) {
          // Create an explosion
          this.getExplosion(bullet.x, bullet.y);

          // Kill the bullet
          bullet.kill();
      }, null, this);

      // Rotate all living bullets to match their trajectory
      this.bulletPool.forEachAlive(function(bullet) {
          bullet.rotation = Math.atan2(bullet.body.velocity.y, bullet.body.velocity.x);
      }, this);

      // Aim the gun at the pointer.
      // All this function does is calculate the angle using
      // Math.atan2(yPointer-yGun, xPointer-xGun)
      this.gun.rotation = this.game.physics.arcade.angleToPointer(this.gun);

      // Shoot a bullet
      if (this.game.input.activePointer.isDown) {
          this.shootBullet();
      }
  };

  // Try to get a used explosion from the explosionGroup.
  // If an explosion is not available, create a new one and add it to the group.
  // Setup new explosions so that they animate and kill themselves when the
  // animation is complete.
  GameState.prototype.getExplosion = function(x, y) {
      // Get the first dead explosion from the explosionGroup
      var explosion = this.explosionGroup.getFirstDead();

      // If there aren't any available, create a new one
      if (explosion === null) {
          explosion = this.game.add.sprite(0, 0, 'explosion');
          explosion.anchor.setTo(0.5, 0.5);

          // Add an animation for the explosion that kills the sprite when the
          // animation is complete
          var animation = explosion.animations.add('boom', [0,1,2,3], 60, false);
          animation.killOnComplete = true;

          // Add the explosion sprite to the group
          this.explosionGroup.add(explosion);
      }

      // Revive the explosion (set it's alive property to true)
      // You can also define a onRevived event handler in your explosion objects
      // to do stuff when they are revived.
      explosion.revive();

      // Move the explosion to the given coordinates
      explosion.x = x;
      explosion.y = y;

      // Set rotation of the explosion at random for a little variety
      explosion.angle = this.game.rnd.integerInRange(0, 360);

      // Play the animation
      explosion.animations.play('boom');

      // Return the explosion itself in case we want to do anything else with it
      return explosion;
  };

  var game = new Phaser.Game(960, 500, Phaser.AUTO, 'game');
  game.state.add('game', GameState, true);

  window.focus();
</script>