LibGDX Reconstruction Flappy Bird-Collision Detection and Detail Processing

Original Link: https://my.oschina.net/u/2432369/blog/610409
  Source link for this chapter: Libgdx rebuilds FlappyBird Password: twy2
The previous chapter describes the process of creating physical simulation for BOX2D. In this chapter, we will continue to complete the remainder of BOX2D, collision detection.Because BOX2D helps us complete all the physical simulation processes, including collision detection, which greatly reduces the difficulty of our project, we do not need to understand how collision detection works or even know any collision detection algorithm to complete collision detection and notify us.Let's add a callback function for collision detection and the corresponding logic code for FlappyBird.
collision detection
Open the WorldController class and add the following code to it:
...
 public class WorldController extends InputAdapter implements Disposable {   
    ...
    private void initWorld() {
    	if(world != null) world.dispose();
    	world = new World(new Vector2(0, -110.8f), false);
    	
    	// Add collision detection listeners for the world
    	world.setContactListener(new BirdContactListener());
    }
    ...
    private void collisionDetection(AbstractGameObject a, AbstractGameObject b) {
    	
    }
    
    // Collision Detection Monitor
    private class BirdContactListener implements ContactListener {

		@Override public void beginContact(Contact arg0) {
			AbstractGameObject a = (AbstractGameObject)arg0.getFixtureA().getBody().getUserData();
			AbstractGameObject b = (AbstractGameObject)arg0.getFixtureB().getBody().getUserData();
			collisionDetection(a, b);
		}
		@Override public void endContact(Contact arg0) {}
		@Override public void postSolve(Contact arg0, ContactImpulse arg1) {}
		@Override public void preSolve(Contact arg0, Manifold arg1) {}
    }


}
First, a BirdContactListener class is defined, which implements four methods of the ContactListener interface, beginContact() and endContact() called at the start (overlap) and end of the collision, respectively.The preSolve() method is called after collision detection and before the collision is resolved. You can implement this method instead of the built-in collision handling in BOX2D. The last postSolve() method is called after the collision occurs, where the collision impulse is obtained.There is only one method, beginContact(), which first obtains the Fiture of two colliding objects through Contact and then obtains the Body object, because the setUserData() method is called when creating the Body to treat the game object itself as user data, so we can get the game object directly from getUserData().Finally, the WorldController() private method collisionDetection() is called to handle the collision event.
Create a BirdContactListener object in the initialization function init() and call World.setContactListener() to set up a collision listener for the world.
From the code added above, it can be inferred that the collisionDetection() method will be called once a collision occurs. Now we need to consider what to do with collisionDetection?We know that FlappyBird games are very simple and will end when a collision occurs, so here we should inform WorldController that a collision has occurred with Bird and that the game needs to end immediately.WorldController should stop the BOX2D simulation as soon as it receives the collision message, and Bird should start to finish the animation as soon as it receives the message.
Next, first modify the Bird object:
...
public class Bird extends AbstractGameObject {
        ...
	protected static final float DIED_DROP_DRAG = 0.5f; 		// Falling speed after death
	...
	private boolean contacted;
	private boolean flashed;
	...
	// Initialization
	public void init(int selected) {
		contacted = false;
		flashed = false;
		...
	}
        ...
        // Notify collision events
        public void contact() {
           contacted = true;
        }
  
        public boolean isContact() {
           return contacted;
        }
        ...
	@Override
	public void render(SpriteBatch batch) {
		// Stop playing frame animation if a collision occurs
		if(!contacted) {
			animDuration += Gdx.graphics.getDeltaTime();
			currentFrame = birdAnimation.getKeyFrame(animDuration);
		}
		// A collision occurs but the game is not over yet and the bird is animated to the ground
		else {
			position.y -= DIED_DROP_DRAG;
			rotation -= BIRD_FLAP_ANGLE_POWER;
			position.y = Math.max(position.y, Land.LAND_HEIGHT - Constants.VIEWPORT_HEIGHT / 2 + dimension.y / 2);
			rotation = Math.max(rotation, BIRD_MAX_DROP_ANGLE);		
		}
		
		batch.draw(currentFrame.getTexture(), position.x - dimension.x / 2, position.y - dimension.y / 2,
			dimension.x / 2, dimension.y / 2, dimension.x, dimension.y, scale.x, scale.y, rotation,
			currentFrame.getRegionX(), currentFrame.getRegionY(), currentFrame.getRegionWidth(),
			currentFrame.getRegionHeight(), false, false);
		
		if (contacted && !flashed) {
			flashed = true;
			float w = Gdx.graphics.getWidth();
			float h = Gdx.graphics.getHeight();
			batch.draw(Assets.instance.decoration.white, -w / 2, -h / 2, w, h);
		}
		
	}
}
First, a DIED_DROP_DRAG is added to indicate the falling speed of the bird after the collision, then two boolean type variables are added, where contacted indicates whether the bird has collided or not, and flashed indicates whether the flicker after the collision has been completed.Later, we added two methods to set and get the contacted variable.The biggest change is the render() method, which can be understood by decomposing the code. If no collision occurs, everything works. If a collision occurs, the last animation frame obtained is not updated. Then the y-coordinate of the bird is updated according to the DIED_DROP_DRAG descent speed, and the bird is rotated according to BIRD_FLAP_ANGLE_POWER.The speed updates the bird's rotation angle, and finally defines the minimum y-coordinate and rotation values.The render () method also finishes the flickering process based on contacted and flashed. The flickering principle is very simple. Once a collision occurs, we can fill a frame of the scene with a white picture.
Next, modify the WorldController:
...
  public class WorldController extends InputAdapter implements Disposable {  
    ...
    public void update(float deltaTime) {
    	if(bird.isContact()) return;
    	...
    }  
    ...
    private void collisionDetection(AbstractGameObject a, AbstractGameObject b) {
    	if (a instanceof Bird) {
		((Bird) a).contact();
	} else if (b instanceof Bird) {
		((Bird) b).contact();							
	}	
    	Gdx.app.debug(TAG, "Player Character Contected!");
    }
    ...
  }
First, in the update() method, test if a bird object has a collision, and if it does, it is not new.The collisionDetection() method is then modified to notify the Bird object of any collision.
Game over
Based on the previous analysis, we can be sure that when Bird collides, the game does not end immediately, but there are some animations (if flickering, landing) before the end.Next we'll add an end process to the game.
First we need to determine the conditions for the end of the game.According to the previous analysis, when a Bird object collides, it needs to complete the drop animation to end the game, so the end of the game is marked by the bird has completed the drop animation, that is, the y-axis coordinate of the bird has reached the ground and the rotation angle rotation equals -90 degrees, so add a method for Bird to determine if the game is over isGameOver():
public boolean isGameOver() {
		return contacted && 
			  (position.y <= Land.LAND_HEIGHT - 
			  Constants.VIEWPORT_HEIGHT / 2 + dimension.y / 2) && 
			  (rotation == BIRD_MAX_DROP_ANGLE);
	}
Next, add a boolean type variable, isGameOver, to WorldController and determine if the game is over in update():
...
 public class WorldController extends InputAdapter implements Disposable {  
    ...
    public boolean isGameOver;
    
    public WorldController() { 
    	init();
    }  
      
    private void init() { 
        ...
    	isStart = false;
    	isGameOver = false;
    	...
    }  
    ...
    public void update(float deltaTime) {
    	if(!isGameOver) {
    		isGameOver = bird.isGameOver();
    	}
    	if(bird.isContact()) return;
    	...
    }
    ...
}
Now that the game is over, the game can be restarted.The following modifications to the WorldController.touchDown() method allow the game to start over:
@Override
    public boolean touchDown(int screenX, int screenY, int pointer, int button) {
    	if (button == Buttons.LEFT) {
			if(!isStart) {
				isStart = true;
				bird.beginToSimulate(world);
				land.beginToSimulate(world);
			}
			
			if(isGameOver) {
				init();
			}
			bird.setJumping();
		}
		return true;
    }
The above code determines whether the game ends when the mouse is pressed or a touch screen event occurs, and if it ends, calls init() to start over.However, in order for the above changes to start properly, we must make major changes:
First add the init() method to AbstractGameObject and move all the contents of the constructor into the method:
public AbstractGameObject() {
	}
	
	public void init() {
		position = new Vector2();
		dimension = new Vector2(1, 1);
		origin = new Vector2();
		scale = new Vector2(1, 1);
		rotation = 0;
		body = null;
	}
Next, modify the init() method of the three objects to guarantee the init() method calls to the parent class:
public Bird() {
                init((int) (Math.random() * 3));
	}

	// Initialization
	public void init(int selected) {
                super.init();
                 ...
       }
public Land() {
                ...
                init();
	}
	
	public void init() {
                super.init();
                ...
	}
public Pipe() {
                ...
                init();
	}
	
	public void init() {
                super.init();
                ...
	}
Now we can test the application:
Now that we have completed a complete process of the game and can restart it, there are still many missing GUI s and start and end information.These are described in the next chapter.Now let's work on a detail, because every time a Pipe is created during the run, and the Pipe object contains a Body object, so if we don't release the corresponding Body object in time when a Pipe object is not needed, a long time will greatly reduce the efficiency of BOX2D, soWe need to modify the Pipes and Pipe classes to run better and smoothly.
Detail handling
After the above analysis, we modified the Pipes class:
...
public class Pipes extends AbstractGameObject {
	...
        private void testPipeNumberIsTooLarge(int amount) {
                if (pipes != null && pipes.size > amount) {
                         pipes.get(0).destroy();
                         pipes.removeIndex(0);
                }
       }
	...
	public class Pipe extends AbstractGameObject {
		...
                @Override
                public void beginToSimulate(World world) {
                   // down
                   BodyDef bodyDef = new BodyDef();
                   bodyDef.type = BodyType.KinematicBody;
                   bodyDef.position.set(position);
                
                   body = world.createBody(bodyDef);

                   PolygonShape shape = new PolygonShape();
                   shape.setAsBox(dimension.x / 2, dnPipeHeight / 2,
                   new Vector2(dimension.x / 2, dnPipeHeight / 2), 0);
                   shape.setRadius(-0.1f);

                   FixtureDef fixtureDefDown = new FixtureDef();
                   fixtureDefDown.shape = shape;

                   body.createFixture(fixtureDefDown);
                   body.setLinearVelocity(PIPE_VELOCITY, 0);

                   // up
                   float height = dimension.y - dnPipeHeight - CHANNEL_HEIGHT;
                   shape.setAsBox(dimension.x / 2, height / 2, 
                        new Vector2(dimension.x / 2, dnPipeHeight + CHANNEL_HEIGHT + height / 2), 0);
                   shape.setRadius(-0.1f);
                   FixtureDef fixtureDefUp = new FixtureDef();
                   fixtureDefUp.shape = shape;
                   body.createFixture(fixtureDefUp);
                }
			
                public void destroy() {
                    for(Fixture fixture : body.getFixtureList()) {
                        body.destroyFixture(fixture);
                    }
                    Pipes.this.word.destroyBody(body);
                }
		...
	}
}
First, look at the Pipe internal class. We modified the beginToSimulate() implementation. Previously, we maintained two Body objects for each Pipe. This was all right, but the Body object of BOX2D supports multiple Fixtue features, so we don't need to create two Bodies at all, because we need to do so laterTo release Body and Fixtue objects, creating a Body object is more manageable.Next we added the destroy() method to Pipe, where we destroyed all the Fixture objects and the Body objects themselves.
Next, let's look at testPipeNumberIsTooLarge() of the Pipes object, in which we test that if the number of Pipe objects is too large, destroy the corresponding Body and Fixture objects of a Pipe object.Next let's test the application:
Did you find that the game is much smoother now?If you think Bird is falling too slowly, it's less difficult to modify the WorldController.initWorld() method to increase the acceleration of gravity of the world object.This is world = new World(new Vector2(0, -110.8f), false) and it can be adjusted here.
In this chapter we create collision detection logic and deal with some details. In the next chapter we will add GUI information such as score, frame rate, buttons, etc. to the game.

Reprinted at: https://my.oschina.net/u/2432369/blog/610409

Tags: less

Posted on Tue, 10 Sep 2019 15:05:43 -0700 by mjm