Search code examples
actionscript-3game-physics

if a ball is hitting a box from every side, how to know which side hittest is true


i am creating a brick breaker game in which ball is hitting a square object. i want to change the direction of ball when he hit square object. square is of 15 px. and ball is of 10 px. for example

if hits on the right side, Speed in x direction will reverse if hits on the left side, Speed in x direction will reverse if hits on the up side, Speed in y direction will reverse if hits on the down side , Speed in y direction will reverse.

I tried it hard but found nothing. any help will be appreciated. here is the code:

      import flash.events.*;
      import flash.display.*;

       stop();

// speed of ball in x and y

       var speedx : Number = 10;
       var speedy : Number = 10;

// start running

       function begin_code (event:MouseEvent):void{

       addEventListener(Event.ENTER_FRAME,move_ball);

       stage.addEventListener(MouseEvent.MOUSE_DOWN,by_key);
       addEventListener(Event.ENTER_FRAME,ht_mc);
       start_game.alpha=0;
       }
       start_game.addEventListener(MouseEvent.CLICK, begin_code) ; 

//start ball moving

  function move_ball(e:Event):void{

Ball.x += speedx;
Ball.y += speedy;
Ball.rotation +=speedx;

var ballposition : Number = Ball.x -Hitbar.x;
var ballhitpercent : Number = (ballposition /(Hitbar.width-Ball.width));

if(Ball.x <= x1.x+Ball.width/2){
    speedx = speedx *-1;
}
 if(Ball.x >= x2.x-Ball.width/2){
    speedx = speedx *-1;
}
 if(Ball.y <=(55)){

    speedy = speedy *-1;

}
else if (Ball.y >= stage.stageHeight-Ball.height){ 
speedy = speedy *-1;
trace("hit y");

}
//start ball angle
else if(Ball.hitTestObject(Hitbar)){ 
    speedx = ballhitpercent*10;
    speedy = speedy *-1;    
}
 }

//start hitbar moving

// code for keys!!!!!!!!!

    var distance : Number = 0;

    function by_key(e:MouseEvent):void{

    if (mouseX>Hitbar.x){

    distance = (mouseX-Hitbar.x);
    Hitbar.x += distance;
}

else if (mouseX<Hitbar.x){
distance = (Hitbar.x-mouseX);
Hitbar.x -= distance;
}
}

//restart

  function by_key_up(e:MouseEvent):void{
  } 

  stage.addEventListener(MouseEvent.MOUSE_UP, by_key_up);

  function pLimiter(e:Event):void{
  Hitbar.y = 406.2;

  if (Hitbar.hitTestObject(x1)){
     Hitbar.x = 32.6;
}

   if (Hitbar.hitTestObject(x2)){
     Hitbar.x = 287.55;
}   
}

  addEventListener(Event.ENTER_FRAME, pLimiter);

// Bricks set hit

    var pickup:Array =new Array();

    for (var i = 0;i<numChildren;i++){
if(getChildAt(i) is abc){
    pickup.push(getChildAt(i));
}
}

   function hittest_box(e:Event):void{
for (var f = 0;f<pickup.length;f++){
if (Ball.hitTestObject(pickup[f])){
if(pickup[f].parent) {
    pickup[f].parent.removeChild(pickup[f]);
}
    }   
   }
   }
    function ht_mc(e:Event):void{
     for (var j = 0;j<pickup.length;j++){
    var ball_pos_x: Number = ( Ball.x - pickup[j].x); 
    var ball_pos_y: Number = ( Ball.y - pickup[j].y); 

     if( ball_pos_x < 11.5 && ball_pos_x > -11.5){
             speedy = speedy *-1;
             trace("box y");
    addEventListener(Event.ENTER_FRAME,hittest_box);

    }
    else if ( ball_pos_y < 11.5 && ball_pos_y > -11.5){
             speedx = speedx *-1;
              trace("boxx");
   addEventListener(Event.ENTER_FRAME,hittest_box);
    }
    }
    }

Solution

  • edit

    To turn my two point example into a one-ball-moving version, the main thing you need to do is to declare the p1 then move the ball and then declare p2.

    p1.x = ball.x;
    p1.y = ball.y;
    moveBall();
    p2.x = ball.x;
    p2.y = ball.y;
    

    Now you have p1 and p2. Run the calculations as below to find which brick is hit and which side was hit.

    A few more calculations will be needed (and you'll have to loop through this whole thing for every collision that occurs in between frames). But I can't post all that here.

    Original Answer

    swf

    My answer is meant to be able to be used in any situation in which you have a point object that is moving and needs to detect when it collides with sides of a square. Simple hitTest solutions are inadequate because a point object moving fast enough could "jump over" a corner. Additionally, if the walls of the object are infinitely thin, there couldn't easily be a "collision", so what we really want to calculate is whether or not the point passed through a wall during the last tick. Also, if the ball is going fast enough to jump over an entire brick, you'll get collision with bricks that are surrounded by other bricks. This method fixes that. Here we go...

    edge detection

    My illustration shows the basic method I'm using. It breaks down like this:

    1. define end points for each line segment (corners of block and 2 positions of ball)
    2. check first if ball bounding box and block overlap with simple hit test type of conditional statement
    3. if they do, determine which edge the ball passed through by comparing the 3 angles as shown in the diagram (angle 1 just has to be between angle 2 and 3)
    4. then determine which of the points that make up the lines that the ball passed through during the last frame transition and determine which of these points is nearest the where the ball was on the previous frame (this will be a point contained in the line that the ball will hit!)
    5. lastly, determine which line contains that point

    There could certainly be some optimizations added, but this works like a charm for what it is.

    This code will add a bunch of LineTestBlock objects to the stage as well as 2 Point objects associated with circles. Move the Points such that 1 will represent the first frame location of the ball, and the second Point can be dragged to a place representing where the ball will be on the 2nd frame. You will see that wherever the line between these passes through, the closest edge of a block will light up.

    And here is the code. It is a main document class named BlockDoc and a LineTestBlock class.

    package  {
    
    import flash.display.MovieClip;
    import flash.geom.Point;
    import flash.events.Event;
    import flash.display.Sprite;
    import flash.events.MouseEvent;
    import flash.text.TextField;
    import flash.text.TextFormat;
    import flash.geom.Rectangle;
    import flash.display.Shape;
    
    
    public class BlockDoc extends MovieClip {
    
        // this holds the circle handles (the actual sprite objects)
        var cArray = new Array();
        // this holds all the bricks
        var brickArray = new Array();
        //ball position in frame 1
        //these are just point objects. p1 will need to represent where the ball was on the last frame
        var p1:Point = new Point();
        // and p2 will need to be where the ball is "going" to be on the next frame
        // although, if it hits something, the ball will not end up p2 on the next screen update, obviously
        var p2:Point = new Point();
        // just an array used for putting the 1 and 2 on  the handles
        // you won't eventually need this, of course
        var pointNameArray:Array = new Array();
    
        // this is the Sprite object that holds the rectangle 
        // which describes the bounding box made by the ball in the two frames                  
        var recASprite:Sprite = new Sprite();
    
        // rec is the Sprite that holds the bricks, or you could just put the bricks on the stage.  I like having them in a container so I can remove them all or move them all or change them all just by changing the container
        var rec:Sprite = new Sprite();
        // lines is the Shape object that all the bricks edge lines are drawn to
        var lines:Shape = new Shape();
        // mainLine is the Shape object that the line connecting the two ball positions is drawn to
        var mainLine:Shape = new Shape();
        // you won't need either of these lines in your version
    
        public function BlockDoc() {
    
            // just a background so you can see the alpha better. 
            // you won't need this in yours
            var bkgnd:Sprite = new Sprite();
            bkgnd.graphics.lineStyle();
            bkgnd.graphics.beginFill(0x0);
            bkgnd.graphics.drawRect(0,0,stage.stageWidth,stage.stageHeight);
            bkgnd.graphics.endFill();
            addChild(bkgnd);
    
    
            makeBricks();   
            createArray();
            addChild(mainLine);
            addChild(lines);
            createHandles(); // creates the circle handles
            addEventListener(Event.ENTER_FRAME,gameTick);
        }
    
        private function drawLine(container:Shape,p1:Point,p2:Point,color:uint=0xffbbee):void{
            container.graphics.clear();
            container.graphics.lineStyle(2,color);
            container.graphics.moveTo(p1.x,p1.y);
            container.graphics.lineTo(p2.x,p2.y);
        }
    
        private function makeBricks():void{ 
    
            // creates rectangle object defined by frame 1 and 2 of ball position
            updateBallBoundingBox(recASprite,p1,p2);
            addChild(rec); // the parent for all bounding boxes
            makeSimpleRectangle(recASprite); // this is the bounding box for the 2 positions of the ball
            // these are the bricks
            for (var i:int = 0; i < 5; i++){
                for (var j: int = 0; j < 3; j++){
                    var b: LineTestBlock = new LineTestBlock(50,25);
                    rec.addChild(b);
                    b.alpha = .5;
                    brickArray.push(b);
                    b.x = 100 * i + 50;
                    b.y = 80 * j +100;  
    
                    // top left corner of brick
                    b._p1.x = b.x;
                    b._p1.y = b.y;
    
                    // top right corner of brick
                    b._p2.x = b.x + b.width;
                    b._p2.y = b.y;
    
                    // bottom right corner of brick
                    b._p3.x = b.x + b.width;
                    b._p3.y = b.y + b.height;
    
                    // bottom left corner of brick
                    b._p4.x = b.x;
                    b._p4.y = b.y + b.height;
                }
            }
        }
    
        private function makeSimpleRectangle(rect:Sprite,color:uint = 0x0):void{
    
            rect.graphics.lineStyle(0,0x0,0);
            rect.graphics.beginFill(color,.5);
            rect.graphics.drawRect(0,0,50,50);
            rect.graphics.endFill();
        }
    
        private function updateBallBoundingBox(rec:Sprite,p1:Point,p2:Point):void{
            rec.x = Math.min(p2.x,p1.x);
            rec.y = Math.min(p2.y,p1.y);
            rec.width = Math.max(p2.x,p1.x)-rec.x;
            rec.height = Math.max(p2.y,p1.y)-rec.y;
        }
    
        private function checkIntersection(A1:Point,A2:Point,B1:Point,B2:Point):Boolean{
    
            // adjusts the current "bounding box" using the position 1 and position 2 of the ball
            updateBallBoundingBox(recASprite,A1,A2);
    
            // if bounding boxes hit
            // define the angles to be used (further optimization can easily be done here by
            // only checking the 2 possible sides of contact, or using a look up table 
            // instead of using Math.atan2)
    
            // angle between position 1 and 2 of the ball
            var A1_A2_r:Number = Math.atan2(A2.y - A1.y,A2.x - A1.x);
            // angle between position 1 and one corner
            var A1_B1_r:Number = Math.atan2(B1.y - A1.y,B1.x - A1.x);
            // angle between position 1 and the second corner
            var A1_B2_r:Number = Math.atan2(B2.y - A1.y,B2.x - A1.x);
            // and if angle from position 1 to position 2 is between angle from one corner to the other corner...
            if (A1_A2_r > A1_B1_r && A1_A2_r < A1_B2_r){
                // return true
                return true;
            } else {
                return false;
            }
        }
    
        private function gameTick(e:Event):void{
            // p1 and p2 are the point objects that describe frame 1 and frame 2 of the moving ball
                   // for actual moving ball:
                   //p1.x = Ball.x; 
                   //p1.y = Ball.y;
                   //move_ball();
                   //p2.x = Ball.x;
                   //p2.y = Ball.y;
                   //or use this for experimenting with the handles
            //p1.x = cArray[0].x;
            //p1.y = cArray[0].y;
            //p2.x = cArray[1].x;
            //p2.y = cArray[1].y;
            // draw line between position 1 and 2
            drawLine(mainLine,p1,p2);
            checkCollision();
        }
    
        private function checkCollision():void{
    
            // clears previously drawn lines on bricks
            lines.graphics.clear();
            var xDir:int;
            var yDir:int;
            // determine direction of ball (you probably already have this as a variable somewhere so probably don't need this
            if (p1.x < p2.x){
                xDir = 1;
            } else if (p1.x > p2.x){
                xDir = -1;
            } else {
                xDir = 0;
            }
            if (p1.y < p2.y){
                yDir = 1;
            } else if (p1.y > p2.y){
                yDir = -1;
            } else {
                yDir = 0;
            }   
    
            // this array will hold all the Point objects involved in true collision
            var arr:Array = new Array();
            var arrLines:Array = new Array();
            var nearestDistance:Number = 2000; // change this to something sensible (maybe the speed of the ball...);
            var nearestPoint:Point = new Point(); // change this to something sensible
            // first check bounding boxes       
            for (var i:int = 0; i < brickArray.length; i++){
                var b:LineTestBlock = brickArray[i];                
                var a:Sprite = recASprite;
                updateBallBoundingBox(a,p2,p1);
                // this is just a bounding box hittest, and hitTestObject used to work, but I broke something, so I have to do it like this now
                if (a.x+a.width > b.x && a.y + a.height > b.y && a.x < b.x+b.width && a.y<b.y+b.height){
    
                    // now that we know the rectangles overlap, we check the two sides that are hittable
                    // instead of xDir and yDir just use your variable or property that holds the x and y speeds;
                    // I use xDir and yDir because my example doesn't actually use motion.
                    if (xDir > 0){
                        if (checkIntersection(p1,p2,b._p1,b._p4)){
                            // ball hits left wall of brick
                            //drawLine(lines,b._p1,b._p4,0x00ffee);
                            // instead of drawLine above, put in your function for collision of left wall
                            arr.push(b._p1,b._p4);
                            arrLines.push({p1:b._p1,p2:b._p4,brick:brickArray[i]});
                            // that last array there simply holds an object with 3 properties 
                            // for use later in comparing what should actually receive the hit.
                        }
                        if (yDir > 0) {
                            if (checkIntersection(p2,p1,b._p1,b._p2)){
                                // ball hits top wall of brick
                                arr.push(b._p1,b._p2);
                                arrLines.push({p1:b._p1,p2:b._p2,brick:brickArray[i]});
                            }
                        } else if (yDir < 0){
                            if(checkIntersection(p2,p1,b._p3,b._p4)){
                                // ball hits bottom wall of brick
                                arr.push(b._p3,b._p4);  
                                arrLines.push({p1:b._p3,p2:b._p4,brick:brickArray[i]});                                
                            }
                        }
                    } else if (xDir < 0){
                        if(checkIntersection(p2,p1,b._p2,b._p3)){
                            // ball hits right edge of brick
                            arr.push(b._p2,b._p3);
                            arrLines.push({p1:b._p2,p2:b._p3,brick:brickArray[i]});
                        } 
                        if (yDir > 0) {
                            if (checkIntersection(p2,p1,b._p1,b._p2)){
                                // ball hits top edge of brick
                                arr.push(b._p1,b._p2);
                                arrLines.push({p1:b._p1,p2:b._p2,brick:brickArray[i]});
                            }
                        } else if (yDir < 0){
                            if(checkIntersection(p2,p1,b._p3,b._p4)){
                                // ball hits bottom edge of brick
                                arr.push(b._p3,b._p4);
                                arrLines.push({p1:b._p3,p2:b._p4,brick:brickArray[i]}); 
                            }
                        }
                    }
                }
            }
            for (var i:int = 0; i < brickArray.length; i++){
                brickArray[i].alpha = 0.3;
            }
            for (var i:int = 0; i < arr.length; i++){
                var d:Number = dist(arr[i],p1);
                if(d < nearestDistance){
                    nearestDistance = d;
                    nearestPoint = arr[i];
                }
            }
            for (var j:int = 0; j < arrLines.length; j++){
                if (arrLines[j].p1 == nearestPoint){
                    drawLine(lines,nearestPoint,arrLines[j].p2);//THIS IS THE EDGE THAT GETS HIT!!
                    arrLines[j].brick.alpha = 0.7; // THIS IS THE BRICK THAT GETS HIT!!
                } else if (arrLines[j].p2 == nearestPoint){
                    drawLine(lines,nearestPoint,arrLines[j].p1); //THIS IS THE EDGE THAT GETS HIT!!
                    arrLines[j].brick.alpha = 0.7; // THIS IS THE BRICK THAT GETS HIT!!
                }
            }           
        }
    
        private function dist(p1:Point, p2:Point):Number{
            var dx:Number = p1.x - p2.x;
            var dy:Number = p1.y - p2.y;
            return Math.sqrt(dx * dx + dy * dy);
        }
    
        private function createHandles():void{
    
            // these are the circle handles for moving the balls around
            for (var i:int = 0; i < 2; i++){
                var c:Sprite = new Sprite();
                var tf:TextField = new TextField();
    
                c.graphics.lineStyle();
                c.graphics.beginFill(0x5555ff+0x5555*i);
                c.graphics.drawCircle(0,0,10);
                c.graphics.endFill();
                addChild(c);
                c.addChild(tf);
                c.x = 50*i+50;
                c.y = 50;
                c.addEventListener(MouseEvent.MOUSE_DOWN,dragObject);
                c.addEventListener(MouseEvent.MOUSE_UP,dragStopObject);
                cArray.push(c);
    
                tf.height = 20;
                tf.width = 10;
                tf.text = pointNameArray[i];
                tf.x = -5;
                tf.y = -10;
                tf.mouseEnabled = false;
            }
        }
    
        private function createArray():void{
            // just used for putting the fancy 1 and 2 on the handles
            pointNameArray = ["1","2"];
        }
    
        private function radsFromPoints(p1:Point,p2:Point):Number{
            return Math.atan2(p1.y - p2.y, p1.x - p2.y);
        }
    
        private function dragObject(me:MouseEvent):void{
            me.target.startDrag(true);
        }
    
        private function dragStopObject(me:MouseEvent):void{
            me.target.stopDrag();
        }
    }   
    }
    

    and the LineTestBlock class:

    package  {
    import flash.display.*;
    import flash.text.*;
    import flash.geom.Point;
    
    public class LineTestBlock extends Sprite{
    
        var _p1:Point = new Point();
        var _p2:Point = new Point();
        var _p3:Point = new Point();
        var _p4:Point = new Point();
    
        public function LineTestBlock(w:Number,h:Number) {
    
            var s:Sprite = new Sprite();
            s.graphics.lineStyle();
            s.graphics.beginFill(0xff5599,0.5);
            s.graphics.drawRect(0,0,w,h);
            s.graphics.endFill();
            addChild(s);
        }       
    }
    }
    

    Obviously the stuff about lines and alpha are just for display purposes. You'll be able to erase a bunch of this code and will need to replace some of it with actual bounce functions.

    One thing to keep in mind is how you deal with the bouncing off of the ball. You can't simply reverse the x or y speed of the ball, as you might think because then it is inside of a brick, or on the opposite side of the brick trying to come back out, which will work, but could look funny and actually allow the ball access to bricks that should be off limits because the are surrounded by other bricks. But I've already answered the question at hand, so I'll stop there.