Search code examples
actionscript-3actionscriptcollision-detection

Problem with collision detection in AS3?


I'm trying to have a rotatable line, controlled by the arrow keys. When you click the mouse, a ball drops from the cursor, and stops when it hits the line.

However, the balls always stop at the highest point of the line, across a line parallel to the x-axis.

My document class is as follows:

package  
{
    import flash.display.MovieClip;
    import flash.events.*
    import flash.display.Stage
    import ball

    public class Engine extends MovieClip 
    {
        public static var stageRef:Stage

        private static var leftKey:Boolean = false
        private static var rightKey:Boolean = false

        public static var pi = Math.PI 
        public static var lineRotate:Number = 0
        public static var spinRate:Number = 60

        public static var ground:line = new line()

        public function Engine() 
        {
            // constructor code
            stage.addEventListener(Event.ENTER_FRAME, loop)
            stage.addEventListener(KeyboardEvent.KEY_DOWN, keyDownHandler)
            stage.addEventListener(KeyboardEvent.KEY_UP, keyUpHandler)
            stage.addEventListener(MouseEvent.CLICK, addBall)

            stageRef = stage

            ground.x = 300
            ground.y = 200

            stage.addChild(ground)
        }

        private static function keyDownHandler(e:KeyboardEvent)
        {
            if (e.keyCode == 37) //left
            {
                leftKey = true
            }

            if (e.keyCode == 39)
            {
                rightKey = true
            }
        }

        private static function keyUpHandler(e:KeyboardEvent)
        {
            if (e.keyCode == 37) //left
            {
                leftKey = false
            }

            if (e.keyCode == 39) //right
            {
                rightKey = false
            }
        }

        public function loop(e:Event)
        {
            spin()
        }

        public static function addBall(e:MouseEvent) //adds ball
        {
            var tempBall:ball = new ball()

            tempBall.x = e.stageX
            tempBall.y = e.stageY

            stageRef.addChild(tempBall)
        }

        private static function spin() //spins the "ground" line
        {
            if (leftKey) // minus
            {
                lineRotate -= spinRate
            }

            if (rightKey) // plus
            {
                lineRotate += spinRate
            }

            ground.rotation = lineRotate * (pi / 180) //convert to radians
        }
    }
}

The class for the ball is as follows:

package
{
    import flash.display.MovieClip;
    import flash.events.*

    public class ball extends MovieClip 
    {
        public var vX:Number = 0
        public var vY:Number = 2

        private var gravity:Number = 0
        public function ball()
        {
            // constructor code
            addEventListener(Event.ENTER_FRAME, loop)
        }

        public function loop(e:Event)
        {
            this.x += vX
            this.y += vY

            this.vY +=  gravity

            if (this.x > stage.stageWidth || this.x < 0 || this.y < 0 || this.y > stage.stageHeight)
            {
                removeSelf()
            }

            if (Engine.ground.hitTestObject(this))
            {
                trace('yep')
                stopBall()

            }
            else
            {
                trace('nope')
            }
        }

        public function removeSelf()
        {
            removeEventListener(Event.ENTER_FRAME, loop)
            this.parent.removeChild(this)
        }

        public function stopBall()
        {
            gravity = 0
            vY = 0
            vX = 0
        }
    }
}

I've uploaded my .swf to here.


Solution

  • The easiest thing you can do, for balls is a hitTestPoint with the third argument as true, that turns on shape detection.

    ground.hitTestPoint(x,y,true);

    http://livedocs.adobe.com/flash/9.0/ActionScriptLangRefV3/flash/display/DisplayObject.html#hitTestPoint()

    Ok, so, hm, you can check a single point on the ball, like it's bottom point, or you can check an array of points along the bottom of the ball for easier precision... this is the quickest way to do this if you're not planing anything more complex then this. If, however, you want to create a full fleshed game, leave it to a 2d physics library like http://www.box2dflash.org/.

    Forget skinners collision detection for a bigger game(something small like this could survive with it though), as it is bitmap based and would kill performance much more then box2D, it's a nice mask example, but it not a good idea to use for performance reasons.

    I've changed your code a little bit. I've hittested a bunch of points on the bottom part of the ball, bear in mind that the ball's 0,0 inside the movieclip is centered on the balls center.

    package 
    {
        import flash.display.MovieClip;
        import flash.events.*;
        import flash.geom.Point;
    
        public class ball extends MovieClip
        {
            public var vX:Number = 0;
            public var vY:Number = 2;
    
            private var gravity:Number = 0;
            public function ball()
            {
                // constructor code
                addEventListener(Event.ENTER_FRAME, loop);
            }
    
            public function loop(e:Event)
            {
                this.x +=  vX;
                this.y +=  vY;
    
                this.vY +=  gravity;
    
                if (this.x > stage.stageWidth || this.x < 0 || this.y < 0 || this.y > stage.stageHeight)
                {
                    removeSelf();
                }
    
                /*we will now check something like 18 points on the bottom side of the ball for colision
                instead of just the bottom, you can probably guess why... if you cant, replace i on line 
                43 (var angleInRadians...) with 270 to test and then drop balls on a slopped ground surface... of course
                you should definitely juse a physics engine like http://www.box2dflash.org/ for anything more complex.
                */
                for (var i:int = 180; i<=360; i+=10)
                {
                    /*keep in mind that ball.rotation property has 0 at the top of the ball, while here for these we are using the standard
                    Cartesian coordinate system. The effect of this on rotation would be that it is +90 and for X(yes, X) it would be Math.SIN(),
                    not Math.COS()!!!, and for Y it would be Math.sin() with a minus in front because of the flash coordinate system y axis rotation.
                    It's not really related to this, but it's a point that has anoyed me to no end in trig related stuff in flash when I was starting.
                    */
                    var angleInRadians:Number = i / 180 * Math.PI;
                    var posX:Number = this.x + Math.round(Math.cos(angleInRadians) * width / 2);
                    var posY:Number = this.y - Math.round(Math.sin(angleInRadians) * height / 2);//minus because y in flash is upside down
                    Engine.ground.hitTestPoint(posX,posY, true);
    
                    if (Engine.ground.hitTestPoint(posX,posY, true))
                    {
                        stopBall();
                    }
                    else
                    {
                    }
                }
                /*if (Engine.ground.hitTestObject(this))
                            {
                                trace('yep')
                                stopBall()
    
                            }
                            else
                            {
                                trace('nope')
                            }*/
            }
    
            public function removeSelf()
            {
                removeEventListener(Event.ENTER_FRAME, loop);
                this.parent.removeChild(this);
            }
    
            public function stopBall()
            {
                gravity = 0;
                vY = 0;
                vX = 0;
            }
        }
    }
    

    Unrelated to this above, you need to rethink your OOP a little bit, you are doing things a little bit wrong :). A project this small would cope, but anything bigger would give you headaches. Don't take this as an attack, I want to try to guide you to the "right" path and show you the logical fallacies in your code, I am not mentioning this to "pwn" you cause your oop is bad :).

    For example, gravity, inside class ball? Yikes... what if you wanted to have gravity for ninjas, shurikens, ragdolls, etc? Are you going to subclass Ninjas out of the ball class?

    Ninja extends ball? That would be fun.

    Do you think that there might be a better place for that gravity thing? Is "gravity" a property of the ball at all(err, tehnically it is, but not this homogeneous gravity you are putting here, it's more like a permeating, all present thing, cause there is some huge body that we are too close to)? It should be in the Engine...

    Also, you did Engine.ground thing... now, this is a static variable... this is another bad thing, very bad thing :)

    The reason is similar to the previous example, but a little bit turned around. What if you want to reuse ball inside, say Engine2? Or Crytek engine? or UDK? I think it might be sligtely problematic, don't you think? You would have to go in and change the ball class... Imagine if every code you used forced you to do that...

    Basically, you probably could have done something like this:

    var bla:ball = new ball(ground);

    this is better, muuuch better... now we can use it in crytek, udk and Engine2 easily...

    Technically, you want to avoid things like static variables of this kind in your code, the idea is called encapsulation. The idea is that you hide the internal workings of classes from other classes that are unrelated to it or should not know about it. The less you NEED to know, the more portable, reusable (yada yada) your classes are. Engine.ground is a huge encapsulation breaker because ball has absolutely zero need to know about class Engine, it should only be concerned with a reference to the ground itself...

    In time you will learn all of this, patterns in programming, and particularly , which patterns save time, and which are appalling, and why (static variables have their use, but only for ultra simple stuff, avoid Singletons as well if you know what they are).

    http://en.wikipedia.org/wiki/Encapsulation_(object-oriented_programming)

    Sorry for the headache I've caused you...

    Remember, use a physics engine for more complex code, don't reinvent the wheel(reuse everything for your projects that is good for them/sufficient for them)... It's fun to test stuff like this, but other folks have done it better before you, and unless you specifically need to delve into their domain, you don't need to concern yourself with every little detail. You are probably trying to build games/something, not bother with Newtonian dynamics...

    HitTest is just fine for simple stuff though, but this is bordering on simple... :)we are already slightly into physics here, and you might find you need a more robust solution soon...

    So generally, try to make your code "robust" by thinking about encapsulation and dependencies between your classes. If a dependency feels unatural (ie ball depends on Engine) then something is probably wrong, and will break later, will break for another game or engine or... I'll end it here...

    Take care and have fun.