Author Topic: [Tutorial]Dynamic vision system  (Read 1673 times)

Ohmnivore

  • Member
  • **
  • Posts: 45
  • Karma: +0/-0
    • View Profile
[Tutorial]Dynamic vision system
« on: Wed, Nov 13, 2013 »
I just wrote a tutorial which you can find here: http://ohmnivore.elementfx.com/216/flixel-vision-system-part-1/#.UoQwYfmPSSo
But since I don't want to come across as a link spammer I have carefully converted it to the following post. I hope it will help other people so they won't need to start from point zero like I had to.

While working on my kick-ass stealth-tactical platformer game (built on Flixel and its supa cool power tools), I came across the need for a limited vision system. In other words, enemies need to have a limited vision cone. I also needed a very fast method that could support the multitude of enemies in a platformer. Because games are worth a thousand words, hereís a demo of the final vision system.
<a href="http://ohmnivore.elementfx.com/wp-content/uploads/2013/11/Z211.swf" target="_blank" rel="noopener noreferrer" class="bbc_link bbc_flash_disabled new_win">http://ohmnivore.elementfx.com/wp-content/uploads/2013/11/Z211.swf</a>
Sweet eh? Okay, now letís get to business. First off, the vision cone can be modeled into 3 parts: The circle, the upper delimiting line and the lower defining line.

For the sake of your own sanity letís assume the view origins from a single point, and not two distinct eyes. It still looks good anyways. The last assumption is that the player is a circle. Youíll understand later why. But for now, try to fit your character into a circle and mark its radius.

Not so bad, right? The characterís sprite is 24*24 pixels. If there are any imprecisions theyíre pretty much off by a pixel or two, so donít obsess over it. What we need is cheap, fast vision checking.

Back to circles. Circles are awesome. Circle-circle collision is like the best thing ever. All you need to do is check if the distance between both centers is smaller than the sum of both radiuses.

Okay, now let me outline the general steps for checking collision between the vision cone and the playerís circle.

1) Check if both circles collide. Circle-circle collision.

2) Check if the playerís circleís center is between the two delimiting lines.

3) Check if the playerís circleís centerís distance to either one of the lines is smaller than the playerís circleís radius. In short, circle-line collision.

4) Check if the line from the observerís eye towards the player collides with the tilemap, in which case the view is obstructed.

The player is visible if 1,4, and 2 or 3 return true.

Take note that these steps go from least computationally expensive to most expensive, this is plain common sense.

So hereís the class I used for the game. It also handles drawing! However I write code like crap when Iím in a hurry, so itís not neat. It does need a bit of adjustment for use in another game, but most stuff to change should be picked up by the compiler should you try to adapt it. If there is a demand for an in-depth explanation of the 4 vision system steps, Iíll try to expand on this tutorial as much as I can. Also I pretty much omitted the drawing part which is crucial after all. Cheers!
Code: [Select]
package 
{
import flash.display.Sprite;
import flash.geom.Point;
import org.flixel.FlxPoint;
import org.flixel.FlxSprite;
import org.flixel.FlxTilemap;
import flash.display.Shape;
import flash.display.BitmapData;
import flash.display.Bitmap;

public class Vision extends FlxSprite
{

public var state:Level;
public var radius:Number;
public var fov:Number;
public var zangle:Number;
public var parent;
public var xoffset:Number;
public var yoffset:Number;
public var view:Sprite;
public var vheight:int;
public var deg_to_rad:Number = 0.0174532925;

public function Vision(State:Level, Radius:Number, FOV:Number, Angle:Number, Parent, Xoffset:int, Yoffset:int)
{
state = State;
radius = Radius;
fov = FOV;
zangle = Angle;
parent = Parent;
x = parent.x;
y = parent.y;
xoffset = Xoffset;
yoffset = Yoffset;

view = new Sprite();
vheight = int(Math.tan(fov / 2 * deg_to_rad) * radius) * 2;
//trace("vheight", vheight);
view.graphics.lineStyle(1, 0xff910000);
view.graphics.beginFill(0xFF0000,0.35);
var finishp:Point = draw_arc(view, parent.width/2, vheight/2, parent.visionlength, -fov/2, fov/2, 1);
view.graphics.lineTo(parent.width/2, vheight/2);
view.graphics.lineTo(finishp.x, finishp.y);

height = vheight;
width =  parent.visionlength*1.5;


var b:BitmapData = new BitmapData( parent.visionlength*1.5, vheight, true, 0x00000000);
b.draw(view,null,null,null,null,true);
pixels = b;
state.views.add(this);
origin = new FlxPoint(xoffset, vheight / 2);

//fov += 90;
}

override public function update():void
{
var parentradius:Number = parent.visionlength * parent.vision;
if (parentradius != radius)
{
scale = new FlxPoint(parentradius/radius,parentradius/radius);
radius = parent.visionlength * parent.vision;
}
x = parent.x;
y = parent.y - vheight/2 + yoffset;
}

public function draw_arc(movieclip,center_x,center_y,radius,angle_from,angle_to,precision):Point {
            var angle_diff = angle_to - angle_from;
var deg_to_rad=0.0174532925;
            var steps=Math.round(angle_diff*precision);
            var angle=angle_from;
            var px=center_x+radius*Math.cos(angle*deg_to_rad);
            var py = center_y + radius * Math.sin(angle * deg_to_rad);
var initp;
//movieclip.graphics.lineTo(center_x, center_y);
            movieclip.graphics.moveTo(px, py);
            for (var i:int = 1; i <= steps; i++) {
                angle=angle_from+angle_diff/steps*i;
                movieclip.graphics.lineTo(center_x + radius * Math.cos(angle * deg_to_rad), center_y + radius * Math.sin(angle * deg_to_rad));
if (i == 1) initp = new Point(center_x + radius * Math.cos(angle * deg_to_rad), center_y + radius * Math.sin(angle * deg_to_rad));
            }
return initp;
}

public function checkIfSee():Boolean
{
if (checkCircle())
{
var deg_to_rad=0.0174532925;
var playerpos:FlxPoint = state.player.getMidpoint();
var relativep:FlxPoint = new FlxPoint((playerpos.x - (x)), (playerpos.y - (y + vheight/2)));
var theta:Number = (zangle) * deg_to_rad;
var rotatedp:Point = new Point(0,0);
rotatedp.x = Math.cos(theta) * relativep.x - Math.sin(theta) * relativep.y;
rotatedp.y = Math.sin(theta) * relativep.x + Math.cos(theta) * relativep.y;

if (inBetween(rotatedp))
{
if (checkRay()) return true;
}

if (touchingSight(rotatedp))
{
if (checkRay()) return true;
}
}

return false;
}

public function checkRay():Boolean
{
var playerpos:FlxPoint = state.player.getMidpoint();
if (state.collidemap.ray(new FlxPoint(x, y + vheight/2), new FlxPoint(playerpos.x, playerpos.y), null, 1)) return true;
else return false;
}

public function checkCircle():Boolean
{
var playerpos:FlxPoint = state.player.getMidpoint();
var eyepos:FlxPoint = new FlxPoint(x + xoffset, y + yoffset);
if (PtoPdist2(eyepos, playerpos) < (state.player.RADIUS + radius)*(state.player.RADIUS + radius))
{
//trace("Alert!");
return true;
}
else
{
return false;
}
}

static public function PtoPdist2(p1:FlxPoint, p2:FlxPoint):Number
{
return ((p2.x - p1.x) * (p2.x - p1.x) + (p2.y - p1.y) * (p2.y - p1.y));
}

public function inBetween(rotatedp:Point):Boolean
{
if ((rotatedp.y < Math.tan(fov * deg_to_rad / 2) * rotatedp.x) && (rotatedp.y > -1 * Math.tan(fov* deg_to_rad / 2) * rotatedp.x))
{
return true;
}

return false;
}

public function touchingSight(rotatedp:Point):Boolean
{
var point1:Point = new Point(radius, radius*(Math.tan(fov * deg_to_rad/ 2)));
var point2:Point = new Point(radius, radius*( -1 * Math.tan(fov * deg_to_rad/ 2)));

var dist1:Number = segmentDistToPoint(new Point(0, 0), point1, rotatedp);
//trace("dist1: ", dist1);
if (dist1 < state.player.RADIUS)
{
//trace("dist1: ", dist1);
return true;
}

var dist2:Number = segmentDistToPoint(new Point(0, 0), point2, rotatedp);
if (dist2 < state.player.RADIUS)
{
//trace("dist2: ", dist2);
return true;
}

return false;
}

public static function segmentDistToPoint(segA:Point, segB:Point, p:Point):Number
{
var p2:Point = new Point(segB.x - segA.x, segB.y - segA.y);
var something:Number = p2.x*p2.x + p2.y*p2.y;
var u:Number = ((p.x - segA.x) * p2.x + (p.y - segA.y) * p2.y) / something;

if (u > 1)
u = 1;
else if (u < 0)
u = 0;

var x:Number = segA.x + u * p2.x;
var y:Number = segA.y + u * p2.y;

var dx:Number = x - p.x;
var dy:Number = y - p.y;

var dist:Number = Math.sqrt(dx*dx + dy*dy);

return dist;
}
}

}
« Last Edit: Wed, Nov 13, 2013 by Ohmnivore »

paala

  • Contributor
  • ****
  • Posts: 250
  • Karma: +0/-1
    • View Profile
Re: [Tutorial]Dynamic vision system
« Reply #1 on: Tue, Dec 17, 2013 »
Great tutorial for a stealth game.