In this tutorial, you will learn how to cast a field of view of a turret guarding a specific location. When an enemy is within the turret's field of view, the turret will shoot at them. Vector math will be used to help in implementing this field of view.
Final Result Preview
Let's take a look at the final result we will be working towards. Click on the turret at the bottom of stage to begin the simulation.
Step 1: Field of View
I'm sure most readers have used cameras. Every camera has a view angle, defined by the type of lens attached. There are narrow and wide view angles. View angles constrain the field of view into a sector. From a top-down position, they look like the diagram below. If you take a picture, eveything within the grayed area will be captured.
The turret's field of view in our simulation is like that of a camera. If there's an enemy within its field of view, a guard will respond (sound an alarm, take aim, shoot, etc).
Step 2: Basic Concept
The diagram above shows the field of view of turret. Usually the angle of view will be equal on both the left and the right. The radius will also be consistent throughout the sector. So to check whether an enemy is within a turret's field of view, these two mathematical conditions can be used:
- Distance between turret and enemy is less than radius.
- Angle from turret's line of sight to enemy is less than 30°.
Step 3: Define Field of View Using Vector
We shall use vector math to help us. In this case, the vectors in consideration are vLine2
and vLine3
. We can:
- Compare magnitudes of vLine2 and vLine3 to validate Condition 1 from Step 2.
- Compare angle sandwiched between vLine2 and vLine3 to validate Condition 2 from Step 2.
Using the formula of dot product between Vectors, we can find the angle sandwiched between two vectors. I have included a Flash presentation above to facilitate your understanding. Click on the buttons at the bottom to scroll through the frames.
Here is the Actionscript in Vector2D
(which I've used in previous tutorials, like this one) that does the job. Note that line 257 helps to determine whether the angle is on the negative or positive side. However, this is not going to help us much here as direction is not important. More explanations on this topic in the next part of this series.
/** * Method to obtain the smaller angle, in radian, sandwiched from current vector to input vector * @param vector2 A vector to bound the angle * @return Angle in radian, positive is clockwise, negative is anti-clockwise */ public function angleBetween(vector2:Vector2D):Number { //get normalised vectors var norm1:Vector2D = this.normalise(); var norm2:Vector2D = vector2.normalise(); //dot product of vectors to find angle var product:Number = norm1.dotProduct(norm2); product = Math.min(1, product); var angle:Number = Math.acos(product); //sides of angle if (this.vectorProduct(vector2) < 0) angle *= -1 return angle; }
Step 4: Implementation
I have included a presentation below that implements the concept of field of view. Feel free to click and drag the bigger gray circles around. Observe area covered by the field of view indicated by darker dots.
Step 5: Important ActionScript
If you'd like to see the ActionScript for the presentation above, feel free to open up AppFan.as
from the source download - it's commented to facilitate understanding. I shall only include here the important snippet that checks the conditions.
I've highlighted the conditional statement that each of the little dots on the stage is evaluated against it to see if its within the highlighted area.
//Calculate the magnitude and angle var vLine2:Vector2D = new Vector2D(b2.x - b1.x, b2.y - b1.y); var vLine3:Vector2D = new Vector2D(b3.x - b1.x, b3.y - b1.y); var ang:Number = Math.abs(vLine2.angleBetween(vLine3)) //Eliminate directional feature of angle var mag:Number = vLine2.getMagnitude(); for each (var item:Ball in sp) { var vParticle1:Vector2D = new Vector2D(item.x - b1.x, item.y - b1.y); //Checking if falls within sector //Condition: Magnitude less than mag, angle between particle ang vLine2 less than ang if(Math.abs(vLine2.angleBetween(vParticle1)) <ang && mag> vParticle1.getMagnitude()){ item.col = 0x000000; } //if outside of segment, original color else item.col = 0xCCCCCC; item.draw(); }
Step 6: Variations
To add variation to the use of field of view, we can implement far and near attenuation. In fact, we just went through far attenuation. For far attenuation, enemies that are further away (further than the far attenuation distance) cannot be seen by the turret. For near attenuation, enemies that are too close (less than near attenuation) cannot be seen by turret. This may sound irrational, but imagine the turret sitting on a high cliff while the enemy sneaks right under the cliff.
Okay, perhaps you are not convinced, so here is another example of different use. A guard carries a short sword, a bow and arrows. When an enemy is too far off, he may just want to keep a close eye on him. When the enemy comes within shooting range, he shoots arrows. When the enemy comes too close, he fights with short sword. You may even implement different types of guards: bowmen and swordsmen. Enemies within shooting range are dealt with by bowmen while those at at close range are dealt with by swordsmen.
By understanding the conditions introduced in Step 2, variations can be introduced into our simulation or game.
Step 7: Define Field of View Conditions
I've included a Flash presentation below to showcase different cases and their corresponding set of conditions. Click on the buttons to check out the different cases.
Step 8: Programming Field of View Conditions
The following is a snippet of code for implementing the conditions as laid out in Step 7. You may want to view the whole source code in Region2.as
to check out the whole implementation.
//Checking done every frame private function move(e:MouseEvent):void { //Calculate Vector from guard to enemy var g_e:Vector2D = new Vector2D(enemy.x - guard.x, enemy.y - guard.y); var angle:Number = r3.angleBetween(g_e); //Conditions var withinSector:Boolean = Math2.degreeOf(Math.abs(angle)) < sector; var withinR3:Boolean = g_e.getMagnitude() < r3.getMagnitude(); var withinR2:Boolean = g_e.getMagnitude() < r2.getMagnitude(); var withinR1:Boolean = g_e.getMagnitude() < r1.getMagnitude(); //Difference example cases if (example == 0) { if (withinSector && withinR3) { t1.text = "Within FOV" } else t1.text = "Beyond FOV" } else if (example == 1) { if (withinSector && withinR3 && !withinR1) { t1.text = "In between \nfar and near attenuation" } else t1.text = "Beyond FOV" } else if (example == 2) { if (withinSector) { if (withinR1) t1.text ="Sword attack" else if (withinR2) t1.text = "Arrow shoot" else if (withinR3) t1.text = "Keep observe" } else t1.text = "Beyond FOV" } } //Swapping example cases in response to changes in context menu private function swap(e:ContextMenuEvent):void { //swap example if (e.target.caption == "Basic FOV") example = 0; else if (e.target.caption == "Far/Near Attenuation") example = 1; else if (e.target.caption == "Observe/Arrow/Sword") example = 2; //Redraw region indicating detection area drawRegion(); }
Step 9: Implementation
Here's an implementation of ideas explained in Step 6. Click on the black circle and drag it aroud the stage to check whether it's sitting within the visible region. Right-click on stage to pop context menu out, then select from "Basic FOV", "Far/Near Attenuation" and "Observe/Arrow/Sword" to check out the different examples.
Step 10: The Scenario
Now to put into context whatever we have learnt, we are going to create a simulation. Here's the scenario:
A turret is stationed at one end of the stage. Its role is to eliminate as many troops that invade its space as possible. Of course, turret will have to see those troops (within field of view) in order to shoot lasers - and bye-bye troops. Since it can only shoot a beam of laser at any instance, it's going to choose the closest enemy in sight.
On the other hand, troops will try to successfully cross to the other side. They will have to cross a river and, as they do, they will slow down. Now these troops are going to respawn on top of stage whenever they die or go out of stage.
Step 11: Basic Setup
The implementation of this example will be hard-coded. The basic setup is as below. On initialisation we will draw the graphics of elements (river, troops, turret) and position then nicely. On clicking on the purple turret, animation will start. On every frame we will see them animate according to the behaviours of each element.
public function Scene1() { makeTroops(); makeRiver(); makeTurret(); turret.addEventListener(MouseEvent.MOUSE_DOWN, start); function start ():void { stage.addEventListener(Event.ENTER_FRAME, move); } } private function move(e:Event):void { behaviourTroops(); behaviourTurret(); }
Below are the variables of this class.
private var river:Sprite; private var troops:Vector.<Ball>; private var troopVelo:Vector.<Vector2D>; private var turret:Sprite; private var fieldOfView:Sprite; private var lineOfSight:Vector2D = new Vector2D (0, -300); //line of sight facing north private var sectorOfSight:Number = 20 //Actually half of sector, in degrees
Step 12: Draw and Position River
Drawing and positioning river. Pretty simple.
private function makeRiver():void { river = new Sprite; addChild(river); //Specify the location & draw graphics of river with (river) { x = 0; y = 150; graphics.beginFill(0x22BBDD, 0.2); graphics.drawRect(0, 0, 500, 50); graphics.endFill(); } }
Step 13: Draw and Position Troops
Drawing troops will be simple. However, I wanted a "V" formation on troops. So I position the troop at the bottom of "V" first, followed by troops on both sides of its wing. You may want to adjust its center position through center
and the spacing between troops through xApart
and yApart
. Note that troops
and its corresponding troopVelo
share the same index. All troops are heading south.
private function makeTroops():void { troops = new Vector.<Ball>; //Initiate troops troopVelo = new Vector.<Vector2D>; //initiate velocity //local variables var center:Vector2D = new Vector2D(stage.stageWidth * 0.5, 150); var xApart:int = 20; var yApart:int = 15; //Locating troops & velocities var a:Ball = new Ball; stage.addChild(a); troops.push(a); a.x = center.x; a.y = center.y; //troops heading south var aV:Vector2D = new Vector2D(0, 1); troopVelo.push(aV); for (var i:int = 1; i < 11; i++) { var b:Ball = new Ball; stage.addChild(b); troops.push(b); b.x = center.x + i * xApart; b.y = center.y - i * yApart; var bV:Vector2D = new Vector2D(0, 1); troopVelo.push(bV); var c:Ball = new Ball; stage.addChild(c); troops.push(c); c.x = center.x - i * xApart; c.y = center.y - i * yApart; var cV:Vector2D = new Vector2D(0, 1); troopVelo.push(cV); } }
Step 14: Draw and Position Turret
Here, we shall draw, position, and orient the turret and its field of view to face north towards the enemy. Note that the line of sight is facing north.
private function makeTurret():void { //instantiate, locate, orient turret turret = new Sprite; stage.addChild(turret); turret.x = stage.stageWidth * 0.5, turret.y = stage.stageHeight; turret.rotation = -90; turretRot = 2; //rotation speed //Draw turret graphics var w:int = 30; var h:int = 10; turret.graphics.beginFill(0x9911AA); turret.graphics.lineStyle(2); turret.graphics.moveTo( 0, -h / 2); turret.graphics.lineTo(w, -h / 2); turret.graphics.lineTo(w, h / 2); turret.graphics.lineTo(0, h / 2); turret.graphics.lineTo(0, -h / 2); turret.graphics.endFill(); //Setting data for field of view's graphics var point1:Vector2D = new Vector2D(0, 0); var point2:Vector2D = new Vector2D(1, 0); var point3:Vector2D = new Vector2D(0, 0); point1.polar(lineOfSight.getMagnitude(), Math2.radianOf(sectorOfSight)); point2.setMagnitude(lineOfSight.getMagnitude()/Math.cos(Math2.radianOf(sectorOfSight))); point3.polar(lineOfSight.getMagnitude(), Math2.radianOf(-sectorOfSight)); //instantiate, locate, orient field of view fieldOfView = new Sprite; addChild(fieldOfView); fieldOfView.x = turret.x; fieldOfView.y = turret.y; fieldOfView.rotation = -90; //draw turret's field of view fieldOfView.graphics.beginFill(0xff9933, 0.1); fieldOfView.graphics.lineStyle(1); fieldOfView.graphics.moveTo(0, 0); fieldOfView.graphics.lineTo(point1.x, point1.y); fieldOfView.graphics.curveTo(point2.x, point2.y, point3.x, point3.y); fieldOfView.graphics.lineTo(0, 0); fieldOfView.graphics.endFill(); }
A little detail here on the drawing the line of sight. I've included image below for clarification:
Step 15: Troops' Behaviour
Troops will be animated across time. Here's their behaviour. Note that the last two lines of code are commented. If you would like dead troops to be removed from animation, you may uncomment them.
//troops' behaviour private function behaviourTroops():void { //for each troop for (var i:int = 0; i < troops.length; i++) { //If troop reach bottom of screen, respawn on top of screen if (troops[i].y > stage.stageHeight) { troops[i].y = 0; troops[i].x = Math.random() * (stage.stageWidth - 100) + 100; } //if wading through river, slow down //else normal speed if (river.hitTestObject(troops[i])) troops[i].y += troopVelo[i].y*0.3; else troops[i].y += troopVelo[i].y //If troop is dead ( alpha < 0.05 ), respawn on top of screen if (troops[i].alpha < 0.05) { troops[i].y = 0; troops[i].x = Math.random() * (stage.stageWidth - 100) + 100; troops[i].col = 0xCCCCCC; troops[i].draw(); troops[i].alpha = 1; //stage.removeChild(troops[i]); troops.splice(i, 1); //troopVelo.splice(i, 1); } } }
Step 16: Turret's Behaviour
The turret will guard its post by panning its field of view around, but within certain angles. Here, I've defined the panning angle to be between -135 and -45 (using Flash angles). If there's an enemy within sight, it will attack it. But if there's more than one enemy, it's going to choose the closest to attack.
//turret's behaviour private function behaviourTurret():void { //rotate turret within boundaries of -135 & -45 if (turret.rotation > -45) turretRot = -2 else if (turret.rotation < -135) turretRot = 2 //shoot closest enemy within sight graphics.clear(); if (enemyWithinSight() != null) { //closest enemy in sight var target:Ball = enemyWithinSight(); target.col = 0; target.draw(); //turns to black & target.alpha -= 0.2; //health deteriorates //orient turret towards enemy var turret2Target:Vector2D = new Vector2D(target.x - turret.x, target.y - turret.y); turret.rotation = Math2.degreeOf(turret2Target.getAngle()); //draw laser path to enemy graphics.lineStyle(2); graphics.moveTo(turret.x, turret.y); graphics.lineTo(target.x, target.y); } //no enemy within sight, continue scanning else { turret.rotation += turretRot } //turn field of view and line of sight of turret according to turret's rotation fieldOfView.rotation = turret.rotation; lineOfSight.setAngle(Math2.radianOf(turret.rotation)); }
Step 17: Getting Closest Enemy
The turret will locate the closest enemy in its field of view and react by shooting a laser at it. To see how it finds the closest enemy, check out the ActionScript implementation below.
//return the closest enemy within sight private function enemyWithinSight():Ball { var closestEnemy:Ball = null; var closestDistance:Number = lineOfSight.getMagnitude(); for each (var item:Ball in troops) { var turret2Item:Vector2D = new Vector2D(item.x - turret.x, item.y - turret.y); //check if enemy is within sight //1. Within sector of view //2. Within range of view //3. Closer than current closest enemy var c1:Boolean = Math.abs(lineOfSight.angleBetween(turret2Item)) < Math2.radianOf(sectorOfSight) ; var c2:Boolean = turret2Item.getMagnitude() < lineOfSight.getMagnitude(); var c3:Boolean = turret2Item.getMagnitude() < closestDistance; //if all conditions fulfilled, update closestEnemy if (c1 && c2&& c3){ closestDistance = turret2Item.getMagnitude(); closestEnemy = item; } } return closestEnemy; }
Step 18: Launching Simulation
You may now press Ctrl + Enter in FlashDevelop and observe this simulation. Click the turret to start the demo below.
Step 19: A Step Further
We can make use of this understanding to:
- Implement a field of view for enemies.
- Implement more turrets.
- Introduce variation to the field of view as explained in Step 9.
...and so on.
Hopefully, this will spark some ideas and perhaps help in your next simulation or game.
Conclusion
Thanks for reading. As usual, do drop a comment to let me know if this has been helpful to you. I'll be writing the next tutorial to check out how enemies can stay out of turret's field of view by "hiding" behind obstacles. Stay tuned.
Comments