Citrus Engine: Raycast & Spiderman Swing

I’m enjoying diving deeper to Box2D at the moments. And now I want to share about Raycast. As well as how to make a Spiderman, Worm, or Bionic Commando-like rope swing, because it’s one of implementation of Raycast.

spider-man-meme-rope

Raycast
Raycasting is simply create a straight line between two point. Sound pretty simple, but it can be quite useful. Let say you want your AI character have a Line of Sight (LOS) function that will detect what objects are on his sight. Another example if you want to slice an object. Or probably simulate a light ray. I think it can be used on another scenario, that I haven’t discover yet.

I won’t explain about Box2D raycast feature here, as I also quite new to this thing. If you want more detailed explanation, maybe you can google that.

So this is my Ray class that wrapped Box2D raycast function and adds a feature to draw the line on the screen. Box2D debug draw doesn’t render the raycast graphic, so you need to create it by yourself.

package  
{    
    public class Ray     
    {     
        public static const RAY_CAST:String = "ray_cast";     
        public static const RAY_CAST_ONE:String = "ray_cast_one";     
        public static const RAY_CAST_ALL:String = "ray_cast_all";     
      
        public var startPoint:b2Vec2;     
        public var endPoint:b2Vec2;     
        
        public var onContact:Signal;     
        
        public var fraction:Number = 1;     
        
        public var fixture:b2Fixture;     
        public var fixtures:Vector.<b2Fixture>;     
        
        private var sprite:CitrusSprite;     
        private var shape:Shape;     
        
        private var box2D:Box2D;     
        
        private var type:String;     
        
        private var lineGraphic:Boolean = true;     
        
        public function Ray(box2D:Box2D, type:String = RAY_CAST, lineGraphic:Boolean = true)     
        {     
            this.box2D = box2D;     
            
            this.type = type;     
            this.lineGraphic = lineGraphic;     
            
            if (this.lineGraphic)     
            {     
                sprite = new CitrusSprite("raySprite");     
                CitrusEngine.getInstance().state.add(sprite);     
                
                shape = new Shape();     
            }     
            
            startPoint = new b2Vec2();     
            endPoint = new b2Vec2();     
          
            if (this.type == RAY_CAST)     
            {     
                onContact = new Signal(b2Fixture, b2Vec2, b2Vec2);     
            }     
        }     
        
        public function createRay():void     
        {     
            startPoint.x = startPoint.x / box2D.scale;     
            startPoint.y = startPoint.y / box2D.scale;     
            
            endPoint.x = endPoint.x / box2D.scale;     
            endPoint.y = endPoint.y / box2D.scale;     
            
            switch (type)     
            {     
                case RAY_CAST:     
                    box2D.world.RayCast(rayCastHandler, startPoint, endPoint);     
                    break;     
                
                case RAY_CAST_ONE:     
                    fixture = box2D.world.RayCastOne(startPoint, endPoint);     
                    break;     
                
                case RAY_CAST_ALL:     
                    fixtures = box2D.world.RayCastAll(startPoint, endPoint);     
                    break;     
            }     
            
            drawLine();     
        }     
        
        private function rayCastHandler(fixture:b2Fixture, intersection:b2Vec2, normal:b2Vec2, fraction:Number):Number     
        {     
            endPoint.x = intersection.x;     
            endPoint.y = intersection.y;     
            
            onContact.dispatch(fixture, intersection, normal);     
            
            return this.fraction;     
        }     
        
        private function drawLine():void     
        {     
            if (!lineGraphic)     
            {     
                return;     
            }     
            
            shape.graphics.clear();     
            shape.graphics.lineStyle(1);     
            shape.graphics.moveTo(startPoint.x * box2D.scale, startPoint.y * box2D.scale);     
            shape.graphics.lineTo(endPoint.x * box2D.scale, endPoint.y * box2D.scale);     
            
            sprite.view = shape;     
        }     
        
        public function destroy():void     
        {     
            if (type == RAY_CAST)     
            {     
                onContact.removeAll();     
                onContact = null;     
            }     
            
            if (lineGraphic)     
            {     
                shape = null;     
                sprite = null;     
            }     
            
            startPoint = null;     
            endPoint = null;     
        }     
    }     
}

I’m using Starling Graphic Extension here, because by default, Starling doesn’t have a graphics API that can be used to draw vector objects: line, square, circle, etc like Flash.

There are three constant, it will define what type of raycast will be used. RAY_CAST is the default, RAY_CAST_ONE will return first fixture that intersect with the ray, and RAY_CAST_ALL will return all fixtures.
Also there are two point, startPoint and the endPoint.

Fixture variable will hold a reference of the fixture returned if we use RaycastOne. And fixtures variable, a vector that hold reference of fixtures returned by RaycastAll.

Fraction is still a mystery for me. But from what I read somewhere, it’s used by the default Raycast to determine its behaviour. Set it to 0, it will stop at the first target on contact. Set it to 1 it will return all fixtures on contact. Set it to –1 it will ignore everything.

Other variables are self explanatory, I think.

Let’s jump to the createRay() method instead of the constructor, as it’s pretty clear already. In the createRay() method, it convert the startPoint and endPoint to box2D measurement, by simply divide it by box2D scale property. Then we can do the raycasting, based on the raycast type. 
The default raycast will need a callback function, RayCastHandler(). While the others aren’t.

On the rayCastHandler() method, you’ll get information about what fixture that intersect the ray, it’s intersection & normal point, and the fraction. Btw, I still don’t understand what’s fraction function parameter supposed to do. :\

I have a signal here, that will be dispatched when the ray hit something. It will contain all parameter, except fraction.

Okay, we’ve got the raycast wrapper set-up, now we need to implement it on the Hero class. I create a CustomHero class that extends CE default Hero class:

package    
{     
    public class CustomHero extends Hero     
    {     
        private var LOS:Ray;     
        
        public function CustomHero(name:String, params:Object = null)     
        {     
            super(name, params);     
            
            LOS = new Ray(_box2D);     
            LOS.fraction = 0;     
            LOS.onContact.add(LOSContactHandler);     
        }     
        
        override public function update(timeDelta:Number):void     
        {     
            super.update(timeDelta);     
            
            LOS.startPoint.x = x;     
            LOS.startPoint.y = y;     
            
            LOS.endPoint.x = x + 200;     
            LOS.endPoint.y = y;     
            
            if (_inverted)     
            {     
                LOS.endPoint.x = x - 200;     
            }     
            
            LOS.createRay();            
        }     
        
        private function LOSContactHandler(fixture:b2Fixture, intersection:b2Vec2, normal:b2Vec2):void     
        {     
            GameState(_ce.state).setText("target: " + fixture.GetBody().GetUserData().name);     
        }     
    }     
}


Simply create a Ray object called LOS. It will use the default RayCast. And set the fraction to 0, so it will return the first object that intersect with the ray.

Because we want to create a LOS, so it will need stick to the hero all the time, and have a fixed length. On the update() method, set the startPoint to the exact same position as the hero. And the add the length value (200) to the hero x position. Make sure it will also inverted when the hero facing opposite direction. Then call the createRay() function. On the LOSContactHandler(), it’ll just write the object’s name to a TextField.

An the result is something like this:


Move around, and the text field will tell you the object’s name that collide with the ray.
Btw, you can click & hold on the ceiling platform to activate the spidey rope.
How to create that? Just keep reading… Open-mouthed smile

Spiderman Swing


bio
Not Spiderman

The idea just to create a distance joint between the hero to a specified point. To get the target position, we use the raycast function. It will create a ray between hero and the mouse position. If there are a fixture being intersect with the ray, then it’s the target point.

I’ll just paste my GameState code here, with a lot of unnecessary code omitted:

package    
{     
    public class GameState extends StarlingState     
    {     
        private var jointRay:Ray;     
        
        private var jointDef:b2DistanceJointDef;     
        private var joint:b2DistanceJoint;     
        
        private var jointLine:Shape;     
        private var jointSprite:CitrusSprite;     
        
        private var mouseHold:Boolean = false;
         
        public function GameState()     
        {     
            super();     
        }     
        
        override public function initialize():void     
        {     
            super.initialize();
             
            jointRay = new Ray(box2D);     
            jointRay.fraction = 0;     
            jointRay.onContact.add(jointRayContact);         
          
            jointSprite = new CitrusSprite("jointSprite");     
            add(jointSprite);     
            
            jointLine = new Shape();
             
            stage.addEventListener(TouchEvent.TOUCH, click);     
        }     
        
        private function click(event:TouchEvent):void     
        {     
            var touch:Touch = event.getTouch(stage);     
            
            if (touch)     
            {     
                if (touch.phase == TouchPhase.BEGAN)     
                {     
                    var posX:int = event.getTouch(event.currentTarget as DisplayObject).globalX;     
                    var posY:int = event.getTouch(event.currentTarget as DisplayObject).globalY;     
                    var worldPos:Point = ((view as StarlingView).viewRoot as Sprite).globalToLocal(new Point(posX, posY));     
                    
                    var hero:CustomHero = getObjectByName("hero") as CustomHero;     
                    
                    jointRay.startPoint.x = hero.x;     
                    jointRay.startPoint.y = hero.y;     
                    jointRay.endPoint.x = worldPos.x;     
                    jointRay.endPoint.y = worldPos.y;     
                    
                    jointRay.createRay();     
                    
                    mouseHold = true;     
                }     
                else if (touch.phase == TouchPhase.ENDED)     
                {     
                    if (joint)     
                    {     
                        var box2D:Box2D = getObjectByName("box2D") as Box2D;     
                        box2D.world.DestroyJoint(joint);     
                        
                        joint = null;     
                        jointDef = null;     
                        
                        jointLine.graphics.clear();     
                    }     
                    
                    mouseHold = false;     
                }     
            }     
        }     
        
        override public function update(timeDelta:Number):void     
        {     
            super.update(timeDelta);     
            
            if (mouseHold)     
            {     
                if (joint)     
                {                    
                    var box2D:Box2D = getObjectByName("box2D") as Box2D;     
                    
                    joint.SetLength(joint.GetLength() - (5 / box2D.scale));     
                    
                    drawLine();     
                }     
            }     
        }     
        
        private function drawLine():void     
        {     
            var box2D:Box2D = getObjectByName("box2D") as Box2D;     
            
            jointLine.graphics.clear();     
            jointLine.graphics.lineStyle(2, 0xFF0000);     
            jointLine.graphics.moveTo(joint.GetAnchorA().x * box2D.scale, joint.GetAnchorA().y * box2D.scale);     
            jointLine.graphics.lineTo(joint.GetAnchorB().x * box2D.scale, joint.GetAnchorB().y * box2D.scale);     
            
            jointSprite.view = jointLine;     
        }     
        
        private function jointRayContact(fixture:b2Fixture, intersec:b2Vec2, normal:b2Vec2):void     
        {     
            var hero:CustomHero = getObjectByName("hero") as CustomHero;     
            var box2D:Box2D = getObjectByName("box2D") as Box2D;     
            
            if (joint)     
            {     
                box2D.world.DestroyJoint(joint);     
            }     
            
            jointDef = new b2DistanceJointDef();     
            jointDef.Initialize(hero.body, fixture.GetBody(), hero.body.GetWorldCenter(), intersec);     
            
            joint = box2D.world.CreateJoint(jointDef) as b2DistanceJoint;     
        }
    }    
}

Initialize all the necessary objects: Ray, DistanceJointDef, DistanceJoint. And objects for the graphical representation of the hook: Shape & CitrusSprite.
We use default RayCast mode, and fraction of 0. Don’t forget to add touch event to the stage.

Jump to the click() method. To simulate a mouse hold event on Starling we need to detect the phase. Set the mouseHold property to true when it’s in Began phase. Otherwise, set it to false when in Ended phase.

In the Began phase, we create a ray. If the ray intersect with a fixture, then surely it will fire a signal. Catch it with jointRayContact() method and create the DistanceJoint object between the hero to the intersection point.
In the Ended phase, if a joint is exists, we need to destroy them. And also remove the graphical representation.

I also shorten the rope when players holding the mouse button. Just take a look on the update() method. I reduce the joint length 5 pixel per frame. Don’t forget to divide it with box2D scale factor.

DrawLine() method, well, just to draw a red line as a graphical representation for the joint.

Done
Ok, you can download the source code here:

download

Freelance 2D game artist, occassional game developers, lazy blogger, and professional procrasctinator

3 comments:

  1. I frikken love your tutorial, even though I don't 100% understand everything since Im not really a programmer. But i want to learn. Thanks for this

    You are awesome dude. You are awesome'.

    ReplyDelete
  2. Thanks for the great post. It is very useful and informative.
    Web CodeMan

    ReplyDelete