Simple Platform Game Basics Part 3
Better bullet handling, better graphics, more animations.


In this part we use the same principle we did for the ghost and body,
but this time use the idea also with the land.
Two .png files are used in the process.
One is just black and transparent with the basic outline of the level.
the other is the actual graphic that we want for the land.

The plain black one will be the 'land' basicEnemyObject that will cause hits,
and the graphical image will not cause hits, just move with the other image.

A Partition is used to greatly increase the amount of bullets that can be on screen at the same time.
However we also employ a technique that causes each bullet out of view to be removed.
The two strategies together make for rather nice bullet handling.

In this example there are also random enemies that spawn from the left side.
They are instances of the basicEnemyProjectile class.
Whenn added to the stage, they automatically start moving in a given direction.


ManualSamusRunningWithAPartition.as
package Tutorials
{
	
	import com.actiontad.basicGameEvents.LoopEvent;
	import com.actiontad.basicGameEvents.GameObjectEvents;
	import com.actiontad.basicGameEvents.HitEvent;
	import com.actiontad.basicGameObjects.basicGameObjectExtension;
	import com.actiontad.basicGameObjects.IEnemy;
	import com.actiontad.basicGameObjects.LoopEventSubscriber;
	import com.actiontad.basicGameObjects.SimplePlatformEngine;
	import com.actiontad.basicGameObjects.SimplePlatformWalker;
	import com.actiontad.basicGameObjects.basicEnemyObject;
	import com.actiontad.basicGameObjects.basicEnemyProjectile;
	import com.actiontad.basicGameObjects.basicBullet;
	import com.actiontad.basicGameObjects.Partition;
	import com.actiontad.gameUtils.TextSplashScreen;
	
	import flash.display.Shape;
	import flash.text.TextField;
	import flash.text.TextFormat;
	import flash.geom.Point;
	import flash.events.Event;
	import flash.display.DisplayObject;
	
	/**
	 * 
	 *  Simple Platform Basics Part 3 - Manual Samus Running and Shooting  With A Partition 
	 *   And some enemy circles too. And cooler bullet handling. And more artwork.
	 *   Simple rectangle based platform game basics with com.actiontad.basicGameObjects
	 * @author (t)ad - You are not to use this tutorial, as is, for a commercial game. Please use your own images.
	 * 
	 * 
	 *  This tutorial shows what is technically possible using the animations object of a IWithAnimations implementor.
	 *  Like the other SimplePlatformEngine tutorials there is a "ghost" SimplePlatformWalker that acts as the basis for HitEvents.
	 * 
	 *  However in this tutorial a LESWithAnimations (LoopEventSubscriber implementing IWithAnimations) is used for the main body of Samus.
	 *  And we use individual images and the animations object to manually define Samus' running animation.
	 *
	 * 
	 *  Using individual images is useful if the Flash IDE is not available, but it should be considered a last resort.
	 *  When possible try to use a WithAnimationsSubscriber instead of the animations object of a IWithAnimations implementor.
	 * 
	 *  This class includes a Partition and added methodology for more efficient bullet handling.
	 * 
	 *  And some enemies to shoot at. And a few more animations for Samus. And she can surf on the bullets too.
	 * 
	 *  Also in this part, we have created a custom class named SamusBody that extends LESWithAnimations.
	 *  Instead of all those lines of Embed taking up space in this class.
	 * 
	 * 
	 *  Also, there is a png image that follows the land. And acts as the real ground that we see.
	 *  We use the same principle as we did for samus and samusBody.
	 *  Using land as the ghost (causer of hits) and rGround for the body (does not cause hits)
	 * 
	 *  The real ground image could go inside the container at the same x and y as land,
	 *  but in this case we haave put it outside the container and created a basic smoothing function for its motion.
	 * 
	 * 
	 *  This class would be considered a main engine of a game.
	 * 
	 *  You could have a different engine for each level, or one engine that turns into each level.
	 *  I prefer the later, but it's up to you.
	 * 
	 *  In the next part of this tutorial on basic platform games we'll see how to use a ScreenOrganizer class
	 *  to add things like a title screen, and to organize the different classes that will make up a game.
	 *  
	 *  In this part we have increased the frame rate to 40.
	 * 
	 *  To have LoopEventSubscribers go as fast as the frame rate, their looperDelay should be the rounded millisecond value of the frame rate.
	 * 
	 *  So, if the frame rate is 30, in milliseconds that is 33.3 (1000/30)  and 33.3 is the default for all LoopEventSubscribers.
	 * 
	 *  In this case the frame rate is 40, so (1000/40) is 25, and should be the interval by which looperDelays are adjusted.
	 *  And therfore the base looperDelay rate is 25.
	 * 
	 */
	[SWF(frameRate = '40', backgroundColor = '0xFFFFFF', width = '650', height = '350')]
	public class ManualSamusRunningWithAPartition extends SimplePlatformEngine 
	{
		[Embed(source = "images/levelSpace.png")]
		private var levelSpace:Class;
		
		[Embed(source = "images/realGround.png")]
		private var realGround:Class;
		
		[Embed(source = "images/morningMountain.jpg")]
		private var backgroundImage:Class;
		
		private var theBackground1:DisplayObject;
		private var theBackground2:DisplayObject;
		private var backGroundSubscriber:LoopEventSubscriber;
		
		private var samus:SimplePlatformWalker;
		private var samusBody:SamusBody;//this time we've made our own class for the body.
		private var land:basicEnemyObject; //its a basicEnemyObject so bullets will die when they hit it.
		private var bulletPar:Partition;
		private var enemyInt:int = 0;
		private var txSplashScreen:TextSplashScreen;//quick show text field.
		//just set wordShowTime and words to have some text appear for some time.
		
		private var scoreField:TextField;
		private var enemiesDestroyed:int = 0;
		private var deathCount:int = 0;
		private var gotYellowThing:String = "no";
		private var strengthBar:Shape;
		
		private var rGround:DisplayObject;//the real image used for the ground
		private var groundPoints:Point;//these vars are used in smoothing the animation of the real ground
		private var blankGroundPoints:Point = new Point();
		private var lastGroundPoint:int = 0;

		public function ManualSamusRunningWithAPartition() {
			super(null, 25, 1000, 40);
			samus = new SimplePlatformWalker();
			samus.friction = .8;
			samus.gravity = 1.5;
			samus.maxVelocity = 6;
			samus.strength = 50;
			samus.deathFunction = neverReallyDie;
			samus.looperDelay = 25;
			with (samus.graphics) {
				beginFill(0xffffff, .001);
				//size of each image is 45, 54 - but image is centered, so offset width for collision illusion.
				drawRect(0, 0, 30, 54);
			}
			
			//this simple command is possible because of the subclass relationship.
			samus.addEventListener(GameObjectEvents.KEY + 32 + GameObjectEvents.UP, fireBullet);
			//however the event for the key down truly comes from the stage. 
			//samus contains a key object which is an instance of the ComboKeys class.
			
			//a Partition with 3 as the precisionOffset, 0 would be the most accurate, but invokes a pixel perfect test.
			bulletPar = new Partition(3);
			bulletPar.looperDelay = 25;
			
			samusBody = new SamusBody();//see SamusBody below
			samusBody.looperDelay = 25;
			samusBody.addEventListener(GameObjectEvents.LOOP, followSamus);
			
			land = new basicEnemyObject();
			land.neverDiesCompletely = true;
			
			land.addChild(new levelSpace());//this time the land is a png image.
			land.looperDelay = 25;
			
			var thingToGet:basicGameObjectExtension = new basicGameObjectExtension();
			with (thingToGet.graphics) {
				beginFill(0xFFFF00);
				drawCircle(0, 0, 5);
			}
			
			thingToGet.addEventListener(HitEvent.HIT, collectThing);
			container.addChild(samus);
			container.addChild(samusBody);
			land.x = -500;
			land.y = 90;
			//the bulletPar has no width or height defined, it's just ready to hold bullets
			container.addChild(bulletPar);
			container.addChild(land);
			land.visible = false;
			//the land is a basicEnemyObject and will cause hits.
			//It is a 8 kb png file, just black and transparent.
			//Much like the land in part 2, just black.
			
			//The following png file (realGround) is the real land image we want shown.
			//It will go in the container, on top of the 'land'
			//Since it will not cause hits, we don't have to worry about it slowing performance in that way.
			rGround = addChild(new realGround()); rGround.cacheAsBitmap = true;
			groundPoints = land.localToGlobal(blankGroundPoints);
			rGround.x = groundPoints.x; rGround.y = groundPoints.y;
			
			thingToGet.x = -500 + 50;
			thingToGet.y = 80;
			container.addChild(thingToGet);
			
			scoreField = new TextField();
			scoreField.textColor = 0x080808;
			scoreField.width = 200;
			scoreField.text = 
			"# enemies killed: 0 \n # times died: 0 \n reached top: no \n strength: ";
			scoreField.y = scoreField.x = 10;
			strengthBar = new Shape();
			with (strengthBar.graphics) {
				beginFill(0xFF0000);
				drawRect(0, 0, 50, 10);
			}
			addChild(scoreField);
			strengthBar.x = 60;
			strengthBar.y = scoreField.y + scoreField.textHeight - 9;
			addChild(strengthBar);
			scoreField.addEventListener(Event.ENTER_FRAME, keepScore);
			
			//do not remove/overrride the react listener, doing so will not work in all situations.
			//see the notes in the SimplePlatformWalker class.
			//samus.removeEventListener(HitEvent.HIT, samus.react); 
			
			//You can add other handlers to the HitEvent.
		    samus.addEventListener(HitEvent.HIT, overseeHitReaction); 
			
			samus.addEventListener(GameObjectEvents.KEY + 82 + GameObjectEvents.UP, resetSamusPosition);
			
			looperDelay = 25; hitCheckDispatcher.momentInterval = 25;
		}
		
		private function keepScore(e:Event):void {
			scoreField.text = "# enemies killed: " + enemiesDestroyed +
				" \n # times died: " + deathCount + " \n reached top: " + gotYellowThing +
				" \n strength: ";
		}
		
		private function overseeHitReaction(e:HitEvent):void {
			var potentialEnemy:basicEnemyProjectile = e.hitSpecs.theHitter as basicEnemyProjectile;
		    if (potentialEnemy) {
				samus.currentAnimation = "hurt"+samus.facing;
				samus.strength -= 2;
				if (samus.strength <= 0) { samus.die(false, 0); } //deathFunction happens, if true, she would be removed also.
				with (strengthBar.graphics) {
					clear();
					beginFill(0xFF0000);
					drawRect(0, 0, samus.strength, 10);
				}
			}
		} 
		private function resetSamusPosition(e:Event):void {
			samus.x = 10;
			samus.y = land.y + 200;
			txSplashScreen.wordShowTime = this.innerDispatcher.frameOffsetCalc(8);
			txSplashScreen.words = 
			"Press space to fire bullets, arrows to move.\nIn this example you can surf on the bullets. \n" +
			"Press r to reset Samus' position. \n"+
			"There is something at the top of the left wall.";
		}
		/**
		 *  In this case, this function happens on added to stage.
		 * @param	e
		 */
		override public function subscribe(e:Event = null):void {
			super.subscribe(e);
			
			theBackground1 = new backgroundImage();//embedded above
			theBackground2 = new backgroundImage();
			addChildAt(theBackground1, 0);
			theBackground2.x = theBackground1.width;
			theBackground1.y = theBackground2.y = 0;
			addChildAt(theBackground2, 0);
			
			//Sometimes the background movement will perform better on its own separate loop.
			//In this case it was ok to just let it tap into the global loop as well, 
			//the performance is actually better.
			
			backGroundSubscriber = new LoopEventSubscriber(this.innerDispatcher.theDispatcher);
			backGroundSubscriber.looperDelay = 25;
			backGroundSubscriber.addEventListener(GameObjectEvents.LOOP, moveBackground);
			backGroundSubscriber.addEventListener(GameObjectEvents.LOOP, smoothRealGroundMotion);

			var txfrmt:TextFormat = new TextFormat("Arial", 15, 0x090909, true, false, false, null, null, "center");
			txSplashScreen = new TextSplashScreen(txfrmt, "center", null, 650, 350);
			txSplashScreen.looperDelay = 25;
			addChild(txSplashScreen);
			txSplashScreen.wordShowTime = this.innerDispatcher.frameOffsetCalc(10);//10 seconds, adjusted for real swf frame rate.
			txSplashScreen.words = 
			"Press space to fire bullets, arrows to move.\nIn this example you can surf on the bullets. \n" +
			"Press r to reset Samus' position. \n"+
			"There is something at the top of the left wall.";
			
			backGroundSubscriber.addEventListener(GameObjectEvents.LOOP, createRandomEnemy);
		}
		private function smoothRealGroundMotion(e:Event):void {
			groundPoints = land.localToGlobal(blankGroundPoints);
			if (groundPoints.x > lastGroundPoint + 2 || groundPoints.x < lastGroundPoint - 2) {
				rGround.x = groundPoints.x;
				lastGroundPoint = groundPoints.x;
			} 
			rGround.y = groundPoints.y;
		}
		
		private function neverReallyDie():void {
			txSplashScreen.wordShowTime = innerDispatcher.frameOffsetCalc(5); samus.strength = 50;
			txSplashScreen.words = "Samus should have died, but well, ...this is just an example. \nStrength back up!";
			deathCount++;
		}
		private var bp:Point = new Point();
		private function createRandomEnemy(e:Event):void {
			var mi:int = this.innerDispatcher.momentInterval; 
			var p:Point = samus.localToGlobal(bp);
			enemyInt += mi; var howFast:int = p.y <= 135?80:160;//when samus' y is higher than 135 spawn enemies faster.
			if (enemyInt % (mi*howFast) == 0) {//spawn an enemy about every 4 seconds (40 * 25 = 1000)
				var enemy:basicEnemyProjectile = 
					new basicEnemyProjectile(basicBullet, null, "left", false, true);//the last param is what we needed to change
				enemy.strength = 15;
				enemy.x = -350;
				enemy.y = samus.y;
				enemy.looperDelay = 25;
				enemy.direction = Math.random() * 5 <= 3?"right":"left";
				enemy.addEventListener(HitEvent.HIT, redCircleEnemyHitHandler);//adding this listener removes the default hitHandler method.
				enemy.addEventListener(GameObjectEvents.LOOP, checkEnemyPosition);
				enemy.velocityX = (Math.random() * 7) + 2;
				with (enemy.graphics) { clear(); beginFill(0xFF0000); drawCircle(0, 0, 10); }
				container.addChild(enemy);
			}
		}
		private function checkEnemyPosition(e:Event):void {
			if (e.target.x >= container.width) { //if you surf up high, the enemies will be able to pass the right wall
				e.target.removeEventListener(GameObjectEvents.LOOP, checkEnemyPosition);
				e.target.die(true, 1);
			}
		}
		private function redCircleEnemyHitHandler(e:HitEvent):void {
			e.target.strength -= 1;
			if (e.target.strength <= 0) {
				e.target.removeEventListener(HitEvent.HIT, redCircleEnemyHitHandler);
				if (e.hitSpecs.theHitter as Partition) { enemiesDestroyed++; }
				e.target.die(true, 1); return;
			}
			if (e.hitSpecs.theHitter.neverDiesCompletely == true && e.hitSpecs.theHitter as Partition == null) {
				e.target.removeEventListener(HitEvent.HIT, redCircleEnemyHitHandler);
				e.target.die(true, 1);
			} 
		}
		
		/**
		 *  Because at the very bottom of samus there is fallSpace (from SimplePlatformWalker),
		 *  technically she never hits what she lands on top of.
		 *  To account for that we check to see if what is hitting the bullet is an IEnemy or another bullet.
		 *  If not, we can assume it is the fallSpace of samus, and/or therefore samus.
		 * @param	e
		 */
		private function letSamusSurfBulletsABit(e:HitEvent):void {
			var isSamus:Boolean = e.hitSpecs.theHitter as IEnemy == null && e.hitSpecs.theHitter as basicBullet == null;
			if (isSamus && samus.y < e.target.y - (samus.height/2)) {
				samus.velocityX += 5 * (samus.facing == samus.RIGHT?1: -1);//5 is the velocityX of a bullet
				samus.currentAnimation = "surf" + samus.facing;
				return;
			} 
		}

		private function followSamus(e:Event):void {
			samusBody.currentAnimation = samus.currentAnimation;//Assign samus animations to samusBody, 
			//a BUCOUnderGravity, which is what a SimplePlatformWalker (samus) is, changes basic currentAnimation values automatically.
			
			samusBody.x = samus.x - 22.5 + 15;//half width of samusBody + half width of samus
			samusBody.y = samus.y + 6.5;//offset for y because images are centered in 45 by 54 rectangles
		}
		/**
		 * Because of the partition we don't add the bullets to the container, they just go in the partition.
		 * Using a partition increases bullet firing performance by leaps and bounds!
		 * 
		 *  And by just setting some different properties for each bullet, we make them more efficient as well.
		 * 
		 *  Even with the new call remaining this is a whole different game,in terms or performance, as opposed to part 2.
		 * 
		 *  Recycling bullets is how to illiminate the new calls, you will see an example of that in the basic ship shooter tutorial.
* * @param e */ private function fireBullet(e:Event):void { //By setting the last param in this basicBullet call to true and passing samus as the first param, //we tell the bullet to die if it is outside of the range of samus. //See basicBullet for more about this technique. var bullet:basicBullet = bulletPar.addChild(new basicBullet(samus, null, null, samus.facing, true, true)) as basicBullet; //In this class we are also going to give samus some extra ability so she can surf on the bullets. bullet.addEventListener(HitEvent.HIT, letSamusSurfBulletsABit);//Calling this removes the handler below, see BasicGameObject. bullet.addEventListener(HitEvent.HIT, bullet.hitHandler);//We want the default handler too, so we add it back bullet.rateOfDeath = 1; bullet.strength = 17; bullet.looperDelay = 25; bullet.velocityX = 5 + ( (samus.velocityX) * (samus.facing == samus.RIGHT?1: -1)); bullet.x = -container.x + 325 - (samus.facing == "left"?5:-30);// half stage width, and then, offsets for left and right bullet.y = -container.y + 175 + 27;//half stage height, half samus height } private function collectThing(e:HitEvent):void { if (e.hitSpecs.theHitter == samus) { txSplashScreen.wordShowTime = innerDispatcher.frameOffsetCalc(7); txSplashScreen.words = "Nice! You got the ..yellow thing!"; gotYellowThing = "yes"; e.target.die(true, 1); } } private function moveBackground(e:Event):void { var spee:int = 2; if (samus.directionX == 1 ) { theBackground1.x -= spee; theBackground2.x -= spee; } if (samus.directionX == -1 ) { theBackground1.x += spee; theBackground2.x += spee; } if (samus.directionY == 1 && theBackground1.y > -(theBackground1.height - 350) ) { theBackground1.y -= spee; theBackground2.y -= spee; } if (samus.directionY == -1 && theBackground1.y < 0 ) { theBackground1.y += spee; theBackground2.y += spee; } if (theBackground1.x <= -(theBackground1.width)) { theBackground1.x = (theBackground1.width-1); } if (theBackground2.x <= -(theBackground1.width)) { theBackground2.x = (theBackground1.width-1); } if (theBackground1.x > 0) { theBackground2.x = theBackground1.x - (theBackground1.width-1); } if (theBackground2.x > 0) { theBackground1.x = theBackground2.x - (theBackground1.width-1); } } } }



SamusBody.as


package Tutorials
{
	import com.actiontad.basicGameObjects.LESWithAnimations;
	
	/**
	 * 
	 * @author  (t)ad 
	 * 				Note:
	 * 				This file is not to be reused with these images.
	 * 				The images are not even included for download with this tutorial.
	 * 				Please use your own images. This is just an example.
	 * 
	 * 				The images used are 45 by 54 rectangles with the images of make-shift Samus centered in the middle.
	 * 				To prove a point to myself, most of them were cut and cropped in windows paint.
	 * 				While I don't recommend it, the nature of AS3, and the ease of using the animations object gives one such flexibility.
	 *  
	 * 				The WithAnimationsSubscriber is to be used instead of the following system, whenever possible.
	 */
	public class SamusBody extends LESWithAnimations 
	{
		
		[Embed(source = "images/SnormalR.png")]
		private var SnormalR:Class;
		[Embed(source = "images/SnormalL.png")]
		private var SnormalL:Class;
		
		[Embed(source = "images/S1.png")]
		private var S1:Class;
		[Embed(source = "images/S2.png")]
		private var S2:Class;
		[Embed(source = "images/S3.png")]
		private var S3:Class;
		[Embed(source = "images/S4.png")]
		private var S4:Class;
		[Embed(source = "images/S5.png")]
		private var S5:Class;
		[Embed(source = "images/S6.png")]
		private var S6:Class;
		[Embed(source = "images/S7.png")]
		private var S7:Class;
		[Embed(source = "images/S8.png")]
		private var S8:Class;
		[Embed(source = "images/S9.png")]
		private var S9:Class;
		[Embed(source = "images/S10.png")]
		private var S10:Class;
		
		[Embed(source = "images/S1L.png")]
		private var S1L:Class;
		[Embed(source = "images/S2L.png")]
		private var S2L:Class;
		[Embed(source = "images/S3L.png")]
		private var S3L:Class;
		[Embed(source = "images/S4L.png")]
		private var S4L:Class;
		[Embed(source = "images/S5L.png")]
		private var S5L:Class;
		[Embed(source = "images/S6L.png")]
		private var S6L:Class;
		[Embed(source = "images/S7L.png")]
		private var S7L:Class;
		[Embed(source = "images/S8L.png")]
		private var S8L:Class;
		[Embed(source = "images/S9L.png")]
		private var S9L:Class;
		[Embed(source = "images/S10L.png")]
		private var S10L:Class;
		
		[Embed(source = "images/SHurtR.png")]
		private var HurtRight:Class;
		[Embed(source = "images/SHurtL.png")]
		private var HurtLeft:Class;
		
		[Embed(source = "images/SJumpL.png")]
		private var JumpLeft:Class;
		[Embed(source = "images/SJumpR.png")]
		private var JumpRight:Class;
		
		[Embed(source = "images/SFallL.png")]
		private var FallLeft:Class;
		[Embed(source = "images/SFallR.png")]
		private var FallRight:Class;
		
		[Embed(source = "images/SSurfR.png")]
		private var SurfRight:Class;
		
		[Embed(source = "images/SSurfL.png")]
		private var SurfLeft:Class;
		
		
		public function SamusBody() {
			//manual define the normal animations for when not in motion, left and right
			this.animations.normalleft = [(new SnormalL())];
			this.animations.normalright = [(new SnormalR())];
			
			//manual define each run animation for left and right
			this.animations.runright = [(new S1()), (new S2()), (new S3()), (new S4()), (new S5()), 
												(new S6()), (new S7()), (new S8()), (new S9()), (new S10())];
			
			this.animations.runleft = [(new S1L()), (new S2L()), (new S3L()), (new S4L()), (new S5L()), 
												(new S6L()), (new S7L()), (new S8L()), (new S9L()), (new S10L())];
			
			this.animations.hurtleft = [(new HurtLeft())];
			this.animations.hurtright = [(new HurtRight())];
			
			this.animations.jumpleft = [(new JumpLeft())];
			this.animations.jumpright = [(new JumpRight())];
			
			this.animations.fallleft = [(new FallLeft())];
			this.animations.fallright = [(new FallRight())];
			
			this.animations.surfright = [(new SurfRight())];
			this.animations.surfleft = [(new SurfLeft())];
			
		}
	}
}



The Result


See the example in its own window here.
It is important to note that to get such good performance your games must be embeded with wmode "direct"


Next: Part 4a

Part 4 begins an overview of using the ScreenOrganizer class to bring together the engine of the game with
other classes like the title screens class and the class for the main application starting point.

part 1 part 2 part 4a part 4b part 4c