I'm reading a book about game development: Beginning Java Game Development with LibGDX.
There was a class named CheesePlease3 that I copied from the book, it presented the Stage class, and Actor class, I had to make a subclass from the Actor class, named BaseActor.
I did everything correctly, basically copy-pasted the whole thing, and it doesn't draw any object.
So my question is why? What is wrong?
Maybe the code itself a little bit long but easy to read.
Here is the CheesePlease3 class:
public class CheesePlease3 extends Game {
public Stage mainStage;
private BaseActor mouse;
private BaseActor cheese;
private BaseActor floor;
private BaseActor winText;
#Override
public void create () {
mainStage = new Stage();
floor = new BaseActor();
floor.setTexture(new Texture("floor.png"));
floor.setPosition(0, 0);
mainStage.addActor(floor);
cheese = new BaseActor();
cheese.setTexture(new Texture("cheese.png"));
cheese.setPosition(300, 300);
mainStage.addActor(cheese);
mouse = new BaseActor();
mouse.setTexture(new Texture("mouse.png"));
mouse.setPosition(200, 200);
mainStage.addActor(mouse);
winText = new BaseActor();
winText.setTexture(new Texture("youWon.png"));
winText.setPosition(150, 150);
winText.setVisible(false);
mainStage.addActor(winText);
}
#Override
public void render(){
// process input
mouse.velocityX = 0;
mouse.velocityY = 0;
if (Gdx.input.isKeyPressed(Input.Keys.LEFT))
mouse.velocityX -= 100;
if (Gdx.input.isKeyPressed(Input.Keys.RIGHT))
mouse.velocityX += 100;
if (Gdx.input.isKeyPressed(Input.Keys.UP))
mouse.velocityY -= 100;
if (Gdx.input.isKeyPressed(Input.Keys.DOWN))
mouse.velocityY += 100;
// update
float dt = Gdx.graphics.getDeltaTime();
mainStage.act(dt);
// check win condition: Mouse must be overlapping cheese
Rectangle mouseRectangle = mouse.getBoundingRectangle();
Rectangle cheeseRectangle = cheese.getBoundingRectangle();
if (mouseRectangle.contains(cheeseRectangle))
winText.setVisible(true);
// draw graphics
Gdx.gl.glClearColor(0.8f, 0.8f, 1, 1);
Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);
mainStage.draw();
}
And here is the BaseActor class:
public class BaseActor extends Actor {
public TextureRegion region;
public Rectangle boundary;
public float velocityX;
public float velocityY;
public BaseActor(){
super();
region = new TextureRegion();
boundary = new Rectangle();
velocityX = 0;
velocityY = 0;
}
public void setTexture (Texture t){
int w = t.getWidth();
int h = t.getHeight();
setWidth(w);
setHeight(h);
region.setRegion(t);
}
public Rectangle getBoundingRectangle(){
return boundary.set(getX(), getY(), getWidth(), getHeight());
}
#Override
public void act (float dt){
super.act(dt);
moveBy(velocityX * dt, velocityY * dt);
}
public void drawBatch (Batch batch, float parentAlpha){
Color c = getColor();
batch.setColor(c.r, c.g, c.b, c.a);
if (isVisible())
batch.draw(region, getX(), getY(), getOriginX(), getOriginY(),
getWidth(), getHeight(), getScaleX(), getScaleY(), getRotation());
}
And here is the DesktopLauncher:
public static void main (String[] arg) {
LwjglApplicationConfiguration config = new LwjglApplicationConfiguration();
new LwjglApplication(new CheesePlease3(), config);
config.title = "Mouse - Cheese";
}
The BaseActor has to override the draw method.
I assume the drawBatch method should be renamed to draw.
Ps.:
The movement for up and down is inverted.
My sprite moves too slowly. Basically I want to apply less force to move my player. Current this:
getBody().applyForceToCenter(new Vector2(-10000000f,0f), true);
is the force needed to make it move a tiny bit.
I know the reason why I am not able to move it is since I haven't scaled the sprite (64x64) it weights more than 400kg. What should be the correct scale?
This is my game screen.
public class GameScreen implements Screen {
//Reference to our Game, used to set Screens
private Logang game;
//basic playscreen variables
private OrthographicCamera gamecam;
private Viewport gamePort;
//Box2d variables
private World world;
private Box2DDebugRenderer b2dr;
boolean drawn = true;
private Player p;
private int pX = 100, pY = 300;
public GameScreen(Logang game) {
this.game = game;
//create cam used to follow mario through cam world
gamecam = new OrthographicCamera();
gamePort = new ScalingViewport(Scaling.stretch, Logang.GWIDTH, Logang.GHEIGHT, gamecam);
gamePort.apply();
gamecam.position.set(gamecam.viewportWidth / 2, gamecam.viewportHeight / 2, 0);
gamecam.update();
Box2D.init();
//create our Box2D world, setting no gravity in X, -10 gravity in Y, and allow bodies to sleep
world = new World(new Vector2(0, Logang.GRAVITY), true);
//allows for debug lines of our box2d world.
b2dr = new Box2DDebugRenderer();
//create a FitViewport to maintain virtual aspect ratio despite screen size
p = new Player(new Sprite(new Texture("hud_p3.png")), world, pX, pY, 1);
//initially set our gamcam to be centered correctly at the start of of map
line();
}
#Override
public void show() {
}
public void update(float dt) {
//handle user input first
p.update(dt);
//update our gamecam with correct coordinates after changes
}
#Override
public void render(float delta) {
//separate our update logic from render
update(delta);
//Clear the game screen with Black
Gdx.gl.glClearColor(0, 0, 0, 1);
Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);
world.step(1f / 60f, 6, 2);
gamecam.position.set(p.getSprite().getX(),Logang.GHEIGHT / 2, 0); // x and y could be changed by Keyboard input for example
gamecam.update();
game.getBatch().setProjectionMatrix(gamecam.combined);
//renderer our Box2DDebugLines
b2dr.render(world, gamecam.combined);
System.out.println("Player x: " + p.getSprite().getX() + " Camera X: " + gamecam.position.x + " Body X: " + p.getBody().getPosition().x);
//System.out.println("Player y: " + p.getSprite().getY() + " Camera Y: " + gamecam.position.y + " Body Y: " + p.getBody().getPosition().y);
game.getBatch().begin();
if (p.getBody() != null)
p.render(game.getBatch());
EntityManager.renderTerra(game.getBatch(), delta);
game.getBatch().end();
}
public void line() {
Texture tmp = new Texture("hud_p3.png");
tmp.setWrap(Texture.TextureWrap.MirroredRepeat, Texture.TextureWrap.MirroredRepeat);
for (int i = 0; i < 50; i++) {
EntityManager.add(new Ground(new Sprite(tmp), world, (int)(i * Logang.TILE), 1, 2));
}
// EntityManager.changeSize(((Logang.TILE) * 5),Logang.TILE);
}
#Override
public void resize(int width, int height) {
//updated our game viewport
gamePort.update(width, height);
gamecam.position.set(gamecam.viewportWidth / 2, gamecam.viewportHeight / 2, 0);
}
public World getWorld() {
return world;
}
#Override
public void pause() {
}
#Override
public void resume() {
}
#Override
public void hide() {
}
#Override
public void dispose() {
world.dispose();
b2dr.dispose();
}
And this is my entity class
private World world;
private Sprite sprite;
private Body body;
private int tipo;
public Entity(Sprite sprite, World world, int x, int y, int tipo) {
this.sprite = sprite;
this.world = world;
getSprite().setPosition(x, y);
sprite.setSize(Logang.TILE, Logang.TILE);
sprite.setOriginCenter();
define(tipo);
this.tipo = tipo;
}
public void update(float dt){
if(Gdx.input.isKeyPressed(Input.Keys.LEFT)){
getBody().applyForceToCenter(new Vector2(-10000000f,0f), true);
}
if(Gdx.input.isKeyPressed(Input.Keys.RIGHT)){
getBody().applyForceToCenter(new Vector2(10000000f,0f), true);
}
if(Gdx.input.isKeyPressed(Input.Keys.SPACE)){
//getBody().applyLinearImpulse(0f,-Logang.GRAVITY,
getBody().getPosition().x, getBody().getPosition().y, true);
}
}
public void define(int tipo) {
BodyDef bdef = new BodyDef();
bdef.position.set((getSprite().getX() + getSprite().getWidth() / 2),
(getSprite().getY() + getSprite().getHeight() / 2));
switch (tipo) {
case 1: {
bdef.type = BodyDef.BodyType.DynamicBody;
break;
}
case 2: {
bdef.type = BodyDef.BodyType.StaticBody;
break;
}
case 3: {
bdef.type = BodyDef.BodyType.DynamicBody;
break;
}
}
body = world.createBody(bdef);
FixtureDef fdef = new FixtureDef();
fdef.density=0.001f; // (weight: range 0.01 to 1 is good)
PolygonShape shape = new PolygonShape();
shape.setAsBox(getSprite().getWidth() / 2, getSprite().getHeight() / 2);
fdef.shape = shape;
body.createFixture(fdef);
body.setUserData(this);
shape.dispose();
}
public void render(SpriteBatch batch) {
if (tipo != 2) {
float posX = getBody().getPosition().x;
float posY = getBody().getPosition().y;
getSprite().setPosition(posX - getSprite().getWidth() / 2, posY -
getSprite().getHeight() / 2);
}
getSprite().draw(batch);
}
public Sprite getSprite() {
return sprite;
}
public void setSprite(Sprite sprite) {
this.sprite = sprite;
}
public Body getBody() {
return body;
}
public void setBody(Body body) {
this.body = body;
}
And this are the in game variables
public static final int GWIDTH = 800;
public static final int GHEIGHT = (GWIDTH/16)*9;
public static final float PPM = 100f;
public static final float GRAVITY = -10f;
public static final float TILE = 64;
Could you please give me a fix?
I already tried to divide body and gamecam position still no effect
What should be the correct scale?
The right scale would be the scale in real life where 1 unit in LibGDX (Box2D) represents 1 meter in real life. I always advice people to use this scale and zoom the camera properly.
Mind though, if you are using very large objects and zoom the camera all the way back objects appear to be falling slowly. This is obviously because your camera contains a much larger space. Not only would it fall slowly but it won't interact properly with the world if the item is supposed to be smaller.
Adept the camera to your world, not your world to your camera.
More detailed answer I gave
I am using the Android Studio IDE, with the libGDX framework to develop a game. It is a clone of the Pac-Man game with a similar gameplay to flappy bird. The concept is that the pac man moves through the planks while avoiding the ghosts coming from the right direction, that are moving off straight into the left direction (not chasing the position of the player). I am not sure how I create a 'for loop' for the ghost animations, I want the ghosts to consistently reposition and re-appear from the right side after a few seconds, with the exception that they completely gone off the screen initially.
The class for one of the Ghosts.
public class Blinky {
private Vector3 position; //x y and z axis
private Rectangle bounds;
private Texture texture1;
private Animation blinkyAnimLeft;
public Blinky(int x, int y) {
position = new Vector3(x, y, 0);
Texture texture1 = new Texture("blinkyLeft.png");
blinkyAnimLeft = new Animation(new TextureRegion(texture1), 2, 0.5f);
//bounds = new Rectangle(x,y,texture1.getWidth() / 2, texture1.getHeight());
}
public void update(float dt) {
blinkyAnimLeft.update(dt);
//bounds.setPosition(position.x, position.y);
}
public Vector3 getPosition() {
return position;
}
public TextureRegion getTexture() {
return blinkyAnimLeft.getFrame();
}
//public Rectangle getBounds() {
return bounds;
}
public void dispose() {
texture1.dispose();
}
}
Ghosts and Player initialised in the GamePlayState Class
public class GamePlayState extends State {
//Variables
private float timePassed = 0;
private Texture background;
public static final int WALK = 1;
public static final double GHOST_WALK = 0.5;
private static final int PLANKS_SPACING = 125; //gap betwen the planks
private static final int PLANK_COUNT = 4;
private Array<Obstacle> planks;
private Player player;
private Blinky blinky;
private Inky inky;
private Texture missile;
public GamePlayState(GameStateManager gsm) {
super(gsm);
player = new Player(50, 100);
blinky = new Blinky(400, 220);
inky = new Inky(400, 240);
// missile = new Texture("missile.png");
background = new Texture("black.jpg");
cam.setToOrtho(false, PacMan.WIDTH/2, PacMan.HEIGHT/2);
planks = new Array<Obstacle>();
for (int i = 1; i<= PLANK_COUNT; i++) {
planks.add(new Obstacle(i * (PLANKS_SPACING + Obstacle.PLANK_WIDTH)));
}
}
#Override
public void handleInput() {
}
#Override
public void update(float dt) {
handleInput();
player.update(dt);
blinky.update(dt);
inky.update(dt);
cam.position.x = player.getPosition().x + 80; //update the position of the camera with the bird
//update when pacman cam viewport has passed plank
//make cam follow the player
for (Obstacle plank: planks) {
if (cam.position.x - (cam.viewportWidth/1) > plank.getPosTopPlank().x + plank.getTopPlank().getWidth()) {
plank.respositionPlanks(plank.getPosTopPlank().x + ((Obstacle.PLANK_WIDTH + PLANKS_SPACING * PLANK_COUNT )));
}
if (plank.collision (player.getBounds()))
gsm.set(new GamePlayState(gsm));
}
cam.update();
}
#Override
public void render(SpriteBatch sb) {
sb.setProjectionMatrix(cam.combined);
sb.begin();
sb.draw(background, cam.position.x - (cam.viewportWidth/2), 0);
timePassed += Gdx.graphics.getDeltaTime();
//Moving Inky
sb.draw(inky.getTexture(), inky.getPosition().x, inky.getPosition().y);
inky.getPosition().x -= GHOST_WALK;
//Moving Blinky
sb.draw(blinky.getTexture(), blinky.getPosition().x, blinky.getPosition().y);
blinky.getPosition().x -= GHOST_WALK;
You should get x, y( and z) position from Vector3.
Then check for borders
if (inky.getPosition().x < 0){
Thread.sleep(/*a few sdeconds*/2000);/*Android studio would ask you about wrapping it with try - catch*/
inky.setPostion( x + /*screen x size*/,
inky.getPosition().y,inky.getPosition().z)/*add setter in ghost class*/
}
In fact, this is not the best way to do it, but the simplest one
I'm making a simple Bouncing Ball game, even if i call the init() method via BouncingBall class constructor in Show() method of the BallScreen class, the ball isn't created. So to create a ball, i should call BouncingBall's init() method in Resize() method. Why? Isn't it supposed to be created in Show() method ?
Here is the code for Bouncing Ball
public class BouncingBall {
public static final float RADIUS_RATIO = 0.04f;
public static final float START_KICK = 500.0f;
private static final float KICK_INTERVAL = 3f;
private static final float DRAG = 1f;
private Vector2 position;
private Vector2 velocity;
float radius;
float lastKick;
public BouncingBall(Viewport viewport) {
init(viewport);
}
public void init(Viewport viewport) {
position = new Vector2();
position.x = viewport.getWorldWidth() / 2;
position.y = viewport.getWorldHeight() / 2;
velocity = new Vector2(0,0);
radius = RADIUS_RATIO * Math.min(viewport.getScreenWidth(), viewport.getScreenHeight());
startKick();
}
public void update(float delta, Viewport viewport) {
float elapsedSeconds = MathUtils.nanoToSec * (TimeUtils.nanoTime() - lastKick);
if (elapsedSeconds > KICK_INTERVAL) {
lastKick = TimeUtils.nanoTime();
startKick();
}
velocity.x -= delta * DRAG * velocity.x;
velocity.y -= delta * DRAG * velocity.y;
position.x += velocity.x * delta;
position.y += velocity.y * delta;
collision(radius,viewport);
}
public void collision(float radius, Viewport viewport) {
if (position.x + radius > viewport.getWorldWidth()) {
position.x = viewport.getWorldWidth() - radius;
velocity.x = -velocity.x;
}
if (position.x - radius < 0) {
position.x = radius;
velocity.x = -velocity.x;
}
if (position.y + radius > viewport.getScreenHeight()) {
position.y = viewport.getWorldHeight() - radius;
velocity.y = -velocity.y;
}
if (position.y - radius < 0) {
position.y = radius;
velocity.y = -velocity.y;
}
}
public void startKick() {
Random random = new Random();
float angle = random.nextFloat() * MathUtils.PI2;
velocity.x = START_KICK * MathUtils.cos(angle);
velocity.y = START_KICK * MathUtils.sin(angle);
}
public void render(ShapeRenderer renderer) {
renderer.setColor(Color.RED);
renderer.circle(position.x, position.y, radius);
}
}
Here is the Screen Class for Bouncing Ball
public class BallScreen extends ScreenAdapter {
private static final float WORLD_SIZE = 480f;
private static final String TAG = BallScreen.class.getSimpleName();
private BouncingBall ball;
private Viewport viewport;
private ShapeRenderer renderer;
#Override
public void show() {
Gdx.app.log(TAG, "Show");
viewport = new FitViewport(WORLD_SIZE, WORLD_SIZE);
renderer = new ShapeRenderer();
ball = new BouncingBall(viewport);
}
#Override
public void resize(int width, int height) {
Gdx.app.log(TAG, "resize" + width + " " + height);
viewport.update(width,height,true);
ball.init(viewport);
}
#Override
public void dispose() {
Gdx.app.log(TAG, "dispose");
renderer.dispose();
}
#Override
public void render(float delta) {
viewport.apply();
Gdx.gl.glClearColor(0, 0, 0, 1);
Gdx.gl20.glClear(GL20.GL_COLOR_BUFFER_BIT);
renderer.setProjectionMatrix(viewport.getCamera().combined);
renderer.begin(ShapeType.Filled);
ball.update(delta, viewport);
ball.render(renderer);
renderer.end();
}
}
Problem is in this line :
radius = RADIUS_RATIO * Math.min(viewport.getScreenWidth(), viewport.getScreenHeight());
When you create FitViewPort and pass to BouncingBall, at that time viewport having only worldWidth and worldHeight that is what you set 480. At that time screenwidth and screenheight of viewport is zero so your radius initialise with zero value.
But After update() method if I call init() method :
viewport.update(width,height,true); // this method set value to screenwidth/height
when you call update method on viewport after that screenwidth and screenheight of viewport having some value according to what type of ViewPort you're using.
After update() if I call ball.init(viewport); then radius is not zero value so that ball is visible on screen.
In short:
I create Polygon object with a help of this method:
public static float[][] getPolygonArrays(float cx, float cy, float R, int sides) {
float[] x = new float[sides];
float[] y = new float[sides];
double thetaInc = 2 * Math.PI / sides;
double theta = (sides % 2 == 0) ? thetaInc : -Math.PI / 2;
for (int j = 0; j < sides; j++) {
x[j] = (float) (cx + R * Math.cos(theta));
y[j] = (float) (cy + R * Math.sin(theta));
theta += thetaInc;
}
return new float[][]{x, y};
}
and merge it to one dimension array with:
public static float[] mergeCoordinates(float[][] vertices) throws Exception {
if (vertices.length != 2 || vertices[0].length != vertices[1].length) throw new Exception("No valid data");
ArrayList<Float> mergedArrayList = new ArrayList<Float>();
float[] mergedArray = new float[vertices[0].length * 2];
for(int i = 0; i < vertices[0].length; i++) {
mergedArrayList.add(vertices[0][i]);
mergedArrayList.add(vertices[1][i]);
}
int i = 0;
for (Float f : mergedArrayList) {
mergedArray[i++] = (f != null ? f : Float.NaN);
}
return mergedArray;
}
I use 0 as value for X and Y for all newly created Polygons (named in code as Platform). And result of method mergeCoordinates i pass to method setVertices of Polygon object.
After this step i do setPosition with x = Gdx.graphics.getWidth()/2 and y = Gdx.graphics.getHeight()/2. Polygons are positioned good, right on the game screen center.
Than i create a new one Polygon, which must use origin coordinates from first Polygon object, this new polygon i named Figure. To set origin coordinates i use method setOrigin of Polygon class, and use X and Y of Platform Polygon object.
When i run method rotate of Platform Polygon object i also rotate Figure Polygon object, and Figure must rotate around origin point, center of Platform. But it does not.
Figure do rotation around bottom right corner.
For example:
my screen size is 640 x 480. Center point will be 320 x 240. This is X and Y of Platform Polygon object, i checked it with getX and getY of Polygon. I create Figure at 0,0, do setPosition(320, 200) (this is preferred orbit distance for figure to platform). And Figure positioned also good.
I run setOrigin(320, 240) for Figure Polygon Object.
I run rotate for Figure object. And it somehow think that right bottom corner have coordinates x = 320 and y = 240 and do rotation around this point.
Any could help me to solve this problem?
More details on problem you can find below(details, images, gifs, schemes and also sources).
More detailed part starts here
i'm trying to understand how coordinate system in libgdx work, cause i have a problem with positioning objects in game world.
I created simple application, with one big Red Polygon object(Platform in code),
10 White Triangle Polygons(Sector in code) which are included into big polygon object and inherits it's behavior(such like rotate, moveTo and etc).
Than i added inside each Sector one Green Polyline(Direction in code) from first vertice of Sector Polygon to midle point of the opposite side to first point.
This is technical line and i will use it's vertices(coordinates of two points) to move small Red Polygon Object(Figure in code), from center to oposite side to center point of Sector.
When i click on Stage and click coordinates are inside Platform i rotate it to the left or to the right(depends on where click was made). On rotate all Sectors and technical lines are rotated correctly.
http://i.imgur.com/s5xaI8j.gif
(670Kb)
As you can see on gif, figures rotates around theire's center point. I found that Polygon class has method setOrigin(float x, float y) and in annotation to this method said next:
/** Sets the origin point to which all of the polygon's local vertices
are relative to. */
So i tried to use this method, set origin X of Figure as center X of Platform and origin Y as center Y of Platform, and tried to rotate Platform.
http://i.imgur.com/pXpTuQi.gif
(1.06Mb)
As you can see, Figure polygon think that his origin coordinates are at right bottom corner. And Figure do rotation around right bottom corner.
I changed origin to next values: x = 50 and y = 50, here is a result:
http://i.imgur.com/Iajb9sN.gif
(640Kb)
I cannot get why it behave like that. What should i change in my logic?
I have not much classes in my project. I removed all imports and getters/setter to reduce amount of lines.
If it is necessary i could provide entire project.
GameScreen code:
public class GameScreen extends DefaultScreen {
private final GameWorld world;
private final GameRenderer renderer;
public GameScreen() {
world = new GameWorld();
renderer = new GameRenderer(world);
}
#Override
public void render(float delta) {
world.update(delta);
renderer.render();
}
#Override
public void resize(int width, int height) {
world.resize(width, height);
}
}
GameWorld code:
public class GameWorld {
private ArrayList < Platform > platforms = new ArrayList < Platform > ();
private OrthographicCamera camera;
private Stage stage;
private Array < Figure > activeFigures;
private Pool < Figure > figuresPool;
private long lastFigureTime = TimeUtils.nanoTime();
public GameWorld() {
setCamera(new OrthographicCamera());
getCamera().setToOrtho(true, Gdx.graphics.getWidth(), Gdx.graphics.getHeight());
setStage(new Stage());
getStage().setViewport(new ScreenViewport(getCamera()));
initializePools();
createPlatforms();
getStage().addListener(new InputListener() {
public boolean touchDown(InputEvent event, float x, float y, int pointer, int button) {
float degrees = Config.PLATFORM_ROTATE_DEGREES;
if (x <= Gdx.graphics.getWidth() / 2) {
degrees *= -1;
}
int i = getPlatforms().size();
while (i-- > 0) {
Platform platform = getPlatforms().get(i);
if (!platform.isLocked() && platform.isRotatable() && platform.getShape().getPolygon().contains(x, y)) {
platform.addAction(Actions.rotateBy(degrees, 1, Interpolation.bounceOut));
break;
}
}
return true;
}
});
Gdx.input.setInputProcessor(getStage());
}
private void initializePools() {
setActiveFigures(new Array < Figure > ());
setFiguresPool(new Pool < Figure > () {#Override
protected Figure newObject() {
return new Figure();
}
});
}
private void createPlatforms() {
float max = Gdx.graphics.getHeight() / (Gdx.graphics.getWidth() / (Gdx.graphics.getWidth() / 2));
float x = Gdx.graphics.getWidth() / 2;
float y = Gdx.graphics.getHeight() / 2;
float sides = 10f;
Color color = Color.RED;
createPlatform(x, y, max * Config.THIRD_PLATFORM_RADIUS_MULTIPLIER, sides, color, true, false, false, null);
}
private Platform createPlatform(float x, float y, float radius, float sides, Color color, boolean rotatable, boolean locked, boolean isEmpty, Platform relatedTo) {
Platform platform = new Platform(0, 0, radius, sides, color, isEmpty, this);
platform.moveTo(x, y);
platform.setRotatable(rotatable);
platform.setLocked(locked);
getPlatforms().add(platform);
getStage().addActor(platform);
if (relatedTo != null) {
relatedTo.addRelation(platform);
}
return platform;
}
private Figure createFigure(float x, float y) {
Figure figure = getFiguresPool().obtain();
figure.init(this, 0, 0, 10f, 4f, Color.DARK_GRAY);
figure.moveTo(x, y);
getActiveFigures().add(figure);
getStage().addActor(figure);
return figure;
}
public void spawnFigure() {
if (getActiveFigures().size >= 10) return;
if (TimeUtils.nanoTime() - getLastFigureTime() <= 2000000000) return;
Platform platform = null;
for (Platform p: getPlatforms()) {
if (!p.isEmpty()) {
platform = p;
break;
}
}
if (platform == null) {
setLastFigureTime(TimeUtils.nanoTime());
return;
}
Sector sector = platform.getSectors().get(MathUtils.random(platform.getSectors().size() - 1));
float x = platform.getX();
float y = platform.getY();
Figure figure = createFigure(x, y);
figure.origin(x, y);
x = sector.getDirection().getTransformedVertices()[2];
y = sector.getDirection().getTransformedVertices()[3];
figure.addAction(Actions.moveTo(x, y, 1));
setLastFigureTime(TimeUtils.nanoTime());
}
public void update(float delta) {
updatePlatforms(delta);
updateFigures(delta);
spawnFigure();
}
private void updatePlatforms(float delta) {
for (Platform platform: getPlatforms()) {
platform.update(delta);
}
}
private void updateFigures(float delta) {
Figure figure;
int figures = getActiveFigures().size;
for (int i = figures; --i >= 0;) {
figure = getActiveFigures().get(i);
if (figure.isAlive() == false) {
getActiveFigures().removeIndex(i);
getFiguresPool().free(figure);
} else {
figure.update(delta);
}
}
}
public void resize(int width, int height) {
getCamera().setToOrtho(true, width, height);
getStage().getViewport().update(width, height, true);
for (Platform platform: getPlatforms()) {
platform.resize(true, width, height);
}
for (Figure figure: getActiveFigures()) {
figure.resize(true, width, height);
}
}
}
GameRenderer code:
public class GameRenderer {
private ShapeRenderer shapeRenderer;
private GameWorld world;
private SpriteBatch spriteBatch;
public GameRenderer(GameWorld world) {
setWorld(world);
setShapeRenderer(new ShapeRenderer());
getShapeRenderer().setProjectionMatrix(getWorld().getCamera().combined);
setSpriteBatch(new SpriteBatch());
getSpriteBatch().setProjectionMatrix(getWorld().getCamera().combined);
}
public void render() {
Gdx.gl.glClearColor(0f, 0.2f, 0.4f, 1f);
Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);
getWorld().getCamera().update();
getSpriteBatch().setProjectionMatrix(getWorld().getCamera().combined);
getShapeRenderer().setProjectionMatrix(getWorld().getCamera().combined);
getWorld().getStage().act(Gdx.graphics.getDeltaTime());
getWorld().getStage().draw();
renderGameObjects();
}
private void renderGameObjects() {
renderPlatforms();
renderFigures();
}
private void renderFigures() {
getShapeRenderer().begin(ShapeRenderer.ShapeType.Line);
for (Figure figure: getWorld().getActiveFigures()) {
figure.render(getSpriteBatch(), getShapeRenderer());
}
getShapeRenderer().end();
}
private void renderPlatforms() {
getShapeRenderer().begin(ShapeRenderer.ShapeType.Line);
for (Platform platform: world.getPlatforms()) {
platform.render(getSpriteBatch(), getShapeRenderer());
}
getShapeRenderer().end();
}
}
Platform code:
public class Platform extends GameObject {
private ArrayList < Sector > sectors = new ArrayList < Sector > ();
private ArrayList < Platform > relations = new ArrayList < Platform > ();
private boolean rotatable = true;
private boolean locked = false;
private void initialize(float cx, float cy, float radius, float sides, Color color) {
setPosition(cx, cy);
setRadius(radius);
setShape(ShapeType.POLYGON.getInstance(new float[] {
cx, cy, radius, sides
}, color));
}
public Platform(float cx, float cy, float radius, float sides, Color color, boolean isEmpty, GameWorld gameWorld) {
setGameWorld(gameWorld);
initialize(cx, cy, radius, sides, color);
setEmpty(isEmpty);
if (!isEmpty()) {
generateSectors();
}
}
private void generateSectors() {
float[] vertices = getShape().getVertices();
for (int i = 0; i < vertices.length; i += 2) {
try {
Color color = Color.WHITE;
if (i + 3 > vertices.length) {
getSectors().add(new Sector(new float[] {
getX(), getY(), vertices[i], vertices[i + 1], vertices[0], vertices[1]
}, color, this, i / 2));
} else {
getSectors().add(new Sector(new float[] {
getX(), getY(), vertices[i], vertices[i + 1], vertices[i + 2], vertices[i + 3]
}, color, this, i / 2));
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
public void rotateBy(float degrees) {
setRotation(degrees);
getShape().rotate(degrees);
for (Sector sector: getSectors()) {
sector.rotate(degrees);
}
for (Platform platform: getRelations()) {
platform.rotateBy(degrees);
}
for (Figure figure: getGameWorld().getActiveFigures()) {
figure.rotate(degrees);
}
}
#Override
public void moveTo(float x, float y) {
super.moveTo(x, y);
getShape().moveTo(x, y);
for (Sector sector: getSectors()) {
sector.moveTo(x, y);
}
for (Platform platform: getRelations()) {
platform.moveTo(x, y);
}
}
public void addRelation(Platform platform) {
if (platform.equals(this)) return;
getRelations().add(platform);
}
#Override
public void update(float delta) {
for (Sector sector: getSectors()) {
sector.update(delta);
}
}
#Override
public void dispose() {
for (Sector sector: getSectors()) {
sector.dispose();
}
}
#Override
public void render(SpriteBatch spriteBatch, ShapeRenderer shapeRenderer) {
render(spriteBatch);
if (Config.DEBUG_LAYOUTS) render(shapeRenderer);
for (Sector sector: getSectors()) {
sector.render(spriteBatch, shapeRenderer);
}
}
private void render(ShapeRenderer shapeRenderer) {
shapeRenderer.setColor(getShape().getColor());
shapeRenderer.polygon(getShape().getVertices());
}
public void resize(boolean reposition, int width, int height) {
if (reposition) {
moveTo(width / 2, height / 2);
}
}
}
Sector code:
public class Sector extends GameObject {
private Polyline direction;
private float[] vertices;
private Platform platform;
private int sectorId;
public Sector(float[] vertices, Color color, Platform platform, int sectorId) throws Exception {
setSectorId(sectorId);
setVertices(vertices);
initialize(vertices, color, platform);
}
private void createDirection() {
float[] vertices = getShape().getPolygon().getVertices();
float x1 = vertices[0];
float y1 = vertices[1];
float x2 = (vertices[2]+vertices[4])/2;
float y2 = (vertices[3]+vertices[5])/2;
setDirection(new Polyline(new float[]{ x1, y1, x2, y2 }));
}
public Sector(float[] vertices, Color color, boolean isEmpty) throws Exception {
initialize(vertices, color);
setEmpty(isEmpty);
}
private void initialize(float[] vertices, Color color) throws Exception {
if (vertices.length != 6) {
throw new Exception("Sector constructor expects 6 vertices");
}
setShape(ShapeType.TRIANGLE.getInstance(vertices, color));
createDirection();
}
private void initialize(float[] vertices, Color color, Platform platform) throws Exception {
if (vertices.length != 6) {
throw new IllegalArgumentException("Sector constructor expects 6 vertices");
}
setShape(ShapeType.TRIANGLE.getInstance(vertices, color));
setPlatform(platform);
createDirection();
}
public void rotate(float degrees) {
getShape().rotate(degrees);
getDirection().rotate(degrees);
}
#Override
public void moveTo(float x, float y) {
super.moveTo(x, y);
getShape().moveTo(x, y);
getDirection().setPosition(x, y);
}
#Override
public void render(SpriteBatch spriteBatch, ShapeRenderer shapeRenderer) {
render(spriteBatch);
if (Config.DEBUG_LAYOUTS) render(shapeRenderer);
}
private void render(ShapeRenderer shapeRenderer) {
shapeRenderer.setColor(getShape().getColor());
shapeRenderer.polygon(getShape().getVertices());
shapeRenderer.setColor(Color.GREEN);
shapeRenderer.line(getDirection().getTransformedVertices()[0], getDirection().getTransformedVertices()[1], getDirection().getTransformedVertices()[2], getDirection().getTransformedVertices()[3]);
}
}
Figure code:
public class Figure extends GameObject {
private GameWorld world;
public void init(GameWorld world, float cx, float cy, float radius, float sides, Color color) {
super.init();
setWorld(world);
initialize(cx, cy, radius, sides, color);
}
private void initialize(float cx, float cy, float radius, float sides, Color color) {
super.moveTo(cx, cy);
setRadius(radius);
setShape(ShapeType.POLYGON.getInstance(new float[] {
cx, cy, radius, sides
}, color));
}
#Override
public void moveTo(float x, float y) {
super.moveTo(x, y);
getShape().moveTo(x, y);
}
#Override
public void setPosition(float x, float y) {
if (!isAllowedToFlyFuther()) {
clearActions();
return;
}
moveTo(x, y);
}
private boolean isAllowedToFlyFuther() {
for (Figure figure: getWorld().getActiveFigures()) {
if (!figure.equals(this) && Intersector.overlapConvexPolygons(figure.getShape().getPolygon(), getShape().getPolygon())) {
return false;
}
}
return true;
}
#Override
public void reset() {
super.reset();
remove();
}
#Override
public void update(float delta) {}
private void render(SpriteBatch spriteBatch) {}
#Override
public void dispose() {}
#Override
public void render(SpriteBatch spriteBatch, ShapeRenderer shapeRenderer) {
render(spriteBatch);
if (Config.DEBUG_LAYOUTS) render(shapeRenderer);
}
private void render(ShapeRenderer shapeRenderer) {
shapeRenderer.setColor(getShape().getColor());
shapeRenderer.polygon(getShape().getVertices());
}
public void rotate(float degrees) {
setRotation(degrees);
getShape().rotate(degrees);
}
public void origin(float originX, float originY) {
setOrigin(originX, originY);
getShape().setOrigin(originX, originY);
}
public void resize(boolean reposition, int width, int height) {
if (reposition) {
//TODO: implement reposition for figures
}
}
}
GameObject code:
public abstract class GameObject extends Actor implements Poolable {
private int speed = 200;
private int baseSpeed = 200;
private boolean alive;
private float radius;
private GameWorld gameWorld;
private Shape shape;
private boolean empty = false;
public GameObject() {
setAlive(false);
}
public void init() {
setAlive(true);
}
public void reset() {
setAlive(false);
}
public abstract void update(float delta);
public abstract void render(SpriteBatch spriteBatch, ShapeRenderer shapeRenderer);
public abstract void dispose();
public void moveTo(float x, float y) {
super.setPosition(x, y);
}
}
Shape code:
public class Shape {
private Color color = new Color(Color.RED);
private float[] vertices;
private int sides;
private float radius;
private Polygon polygon = new Polygon();
public void rotate(float degrees) {
getPolygon().rotate(degrees);
setVertices(getPolygon().getTransformedVertices());
}
public void moveTo(float x, float y) {
getPolygon().setPosition(x, y);
setVertices(getPolygon().getTransformedVertices());
}
public void setOrigin(float originX, float originY) {
getPolygon().setOrigin(originX, originY);
setVertices(getPolygon().getTransformedVertices());
}
public void scale(float ratio) {
getPolygon().setScale(ratio, ratio);
setVertices(getPolygon().getTransformedVertices());
}
}
ShapeType code:
public enum ShapeType {
POLYGON {#Override
public Shape getInstance(float[] settings, Color color) {
try {
return new PolygonShape(settings, color);
} catch (Exception e) {
e.printStackTrace();
}
return new BaseShape();
}
},
TRIANGLE {#Override
public Shape getInstance(float[] settings, Color color) {
try {
return new TriangleShape(settings, color);
} catch (Exception e) {
e.printStackTrace();
}
return new BaseShape();
}
};
public abstract Shape getInstance(float[] settings, Color color);
}
PolygonShape code:
public class PolygonShape extends Shape {
public PolygonShape(float[] settings, Color color) throws Exception {
if (settings.length < 4) {
throw new IllegalArgumentException("Polygon shape constructor expects minimum 4 items in settings");
}
setSides((int) settings[3]);
setRadius(settings[2]);
setVertices(Utils.mergeCoordinates(Utils.getPolygonArrays(settings[0], settings[1], settings[2], (int) settings[3])));
getPolygon().setVertices(getVertices());
}
}
TriangleShape code:
public class TriangleShape extends Shape {
public TriangleShape(float[] settings, Color color) throws Exception {
if (settings.length < 6) {
throw new IllegalArgumentException("Triangle shape constructor expects minimum 6 items in settings");
}
setVertices(settings);
setColor(color);
getPolygon().setVertices(getVertices());
}
}
Utils code:
public class Utils {
public static float[] mergeCoordinates(float[][] vertices) throws Exception {
if (vertices.length != 2 || vertices[0].length != vertices[1].length) throw new Exception("No valid data");
ArrayList < Float > mergedArrayList = new ArrayList < Float > ();
float[] mergedArray = new float[vertices[0].length * 2];
for (int i = 0; i < vertices[0].length; i++) {
mergedArrayList.add(vertices[0][i]);
mergedArrayList.add(vertices[1][i]);
}
int i = 0;
for (Float f: mergedArrayList) {
mergedArray[i++] = (f != null ? f : Float.NaN);
}
return mergedArray;
}
public static float[][] getPolygonArrays(float cx, float cy, float R, int sides) {
float[] x = new float[sides];
float[] y = new float[sides];
double thetaInc = 2 * Math.PI / sides;
double theta = (sides % 2 == 0) ? thetaInc : -Math.PI / 2;
for (int j = 0; j < sides; j++) {
x[j] = (float)(cx + R * Math.cos(theta));
y[j] = (float)(cy + R * Math.sin(theta));
theta += thetaInc;
}
return new float[][] {
x, y
};
}
}
Config code:
public class Config {
public static final String LOG = TheGame.class.getSimpleName();
public static final boolean DEBUG_LAYOUTS = true;
public static final boolean SHOW_LOG = false;
public static final float PLATFORM_ROTATE_DEGREES = 36;
}
DesktopLauncher code:
public class DesktopLauncher {
public static void main(String[] arg) {
LwjglApplicationConfiguration config = new LwjglApplicationConfiguration();
config.title = "The Game!";
config.width = 1920 / 3;
config.height = 1080 / 3;
new LwjglApplication(new TheGame(), config);
}
}
Project structure:
Platform object structure and dependencies:
Render objects workflow