Spawn + shoot asteroids

* Add AsteroidSpawningSystem which adds asteroids to the game at a regular
  interval up to a set limit.
* Add EventFactory for generating entity-specific Collision events
* Detect asteroids being shot with BulletAsteroidCollisionEvent
* Add DEBUG output to creating entities
* New getPointAlongPolygon utils function
This commit is contained in:
Matt Low 2020-01-27 14:31:42 +04:00
parent e6356e4210
commit 042282646b
9 changed files with 283 additions and 20 deletions

View File

@ -3,6 +3,7 @@ package com.me.asteroids;
import com.badlogic.gdx.math.Polygon; import com.badlogic.gdx.math.Polygon;
import com.badlogic.gdx.math.Vector2; import com.badlogic.gdx.math.Vector2;
import com.me.asteroids.components.AccelerationComponent; import com.me.asteroids.components.AccelerationComponent;
import com.me.asteroids.components.AsteroidComponent;
import com.me.asteroids.components.BulletComponent; import com.me.asteroids.components.BulletComponent;
import com.me.asteroids.components.ModelComponent; import com.me.asteroids.components.ModelComponent;
import com.me.asteroids.components.PlayerComponent; import com.me.asteroids.components.PlayerComponent;
@ -11,8 +12,20 @@ import com.me.asteroids.components.VelocityComponent;
import com.me.common.ecs.Engine; import com.me.common.ecs.Engine;
import com.me.common.ecs.Entity; import com.me.common.ecs.Entity;
import static com.me.asteroids.Constants.rand;
public class EntityFactory { public class EntityFactory {
private static final Vector2 tmp = new Vector2();
private static Entity createEntity(Engine engine) {
Entity entity = engine.createEntity();
if (Constants.DEBUG) {
System.out.println("Spawned entity: " + entity);
}
return entity;
}
public static Entity createPlayer(Engine engine) { public static Entity createPlayer(Engine engine) {
PositionComponent position = new PositionComponent(); PositionComponent position = new PositionComponent();
position.position = new Vector2(Constants.HALF_WIDTH, Constants.HALF_HEIGHT); position.position = new Vector2(Constants.HALF_WIDTH, Constants.HALF_HEIGHT);
@ -35,7 +48,7 @@ public class EntityFactory {
AccelerationComponent accel = new AccelerationComponent(); AccelerationComponent accel = new AccelerationComponent();
accel.acceleration = new Vector2(0,1f); accel.acceleration = new Vector2(0,1f);
Entity player = engine.createEntity(); Entity player = createEntity(engine);
player.addComponent(position); player.addComponent(position);
player.addComponent(velocity); player.addComponent(velocity);
player.addComponent(model); player.addComponent(model);
@ -45,26 +58,30 @@ public class EntityFactory {
} }
public static Entity createBullet(Engine engine, Entity player) { public static Entity createBullet(Engine engine, Entity player) {
PositionComponent position = new PositionComponent();
float[] modelVertices = player.getComponent(ModelComponent.class).model.getTransformedVertices(); float[] modelVertices = player.getComponent(ModelComponent.class).model.getTransformedVertices();
position.position = new Vector2(modelVertices[0], modelVertices[1]); float rotation = player.getComponent(PositionComponent.class).rotation;
position.rotation = player.getComponent(PositionComponent.class).rotation;
Vector2 direction = Utils.setUnitVectorAngle(tmp, rotation);
VelocityComponent velocity = new VelocityComponent(); VelocityComponent velocity = new VelocityComponent();
velocity.velocity = Utils.setUnitVectorAngle(new Vector2(), position.rotation).scl(500); velocity.velocity = new Vector2(direction).scl(500);
PositionComponent position = new PositionComponent();
position.position = new Vector2(modelVertices[0], modelVertices[1]).add(direction.scl(4));
position.rotation = rotation;
ModelComponent model = new ModelComponent(); ModelComponent model = new ModelComponent();
model.model = new Polygon(new float[] { model.model = new Polygon(new float[] {
-1f, 2f, 1f, 0f,
1f, 2f, -1f, 0f,
1f, -2f, -1f, -4f,
-1f, -2f, 1f, -4f,
}); });
model.model.setRotation(position.rotation); model.model.setRotation(position.rotation);
model.model.setPosition(position.position.x, position.position.x); model.model.setPosition(position.position.x, position.position.y);
Entity bullet = engine.createEntity(); Entity bullet = createEntity(engine);
bullet.addComponent(position); bullet.addComponent(position);
bullet.addComponent(velocity); bullet.addComponent(velocity);
bullet.addComponent(model); bullet.addComponent(model);
@ -72,4 +89,27 @@ public class EntityFactory {
return bullet; return bullet;
} }
public static Entity createAsteroid(Engine engine) {
// Creates an asteroid entity with position and velocity unset - the AsteroidSpawningSystem
// is responsible for setting its position and velocity
PositionComponent position = new PositionComponent();
VelocityComponent velocity = new VelocityComponent();
ModelComponent model = new ModelComponent();
int size = rand.nextInt(30, 60);
model.model = new AsteroidFactory()
.setVertexCount(rand.nextInt(16, 24))
.setSize(size)
.setSizeVariation(size * 0.7f)
.generate();
model.aabb = model.model.getBoundingRectangle();
Entity asteroid = createEntity(engine);
asteroid.addComponent(position);
asteroid.addComponent(velocity);
asteroid.addComponent(model);
asteroid.addComponent(new AsteroidComponent());
return asteroid;
}
} }

View File

@ -0,0 +1,36 @@
package com.me.asteroids;
import com.me.asteroids.components.AsteroidComponent;
import com.me.asteroids.components.BulletComponent;
import com.me.asteroids.components.PlayerComponent;
import com.me.asteroids.events.BulletAsteroidCollisionEvent;
import com.me.asteroids.events.CollisionEvent;
import com.me.asteroids.events.PlayerASteroidCollisionEvent;
import com.me.common.ecs.Entity;
public class EventFactory {
public static CollisionEvent getNewCollisionEvent(Entity a, Entity b) {
boolean isEntityAPlayer = a.hasComponent(PlayerComponent.class);
boolean isEntityBPlayer = b.hasComponent(PlayerComponent.class);
boolean isEntityABullet = !isEntityAPlayer && a.hasComponent(BulletComponent.class);
boolean isEntityBBullet = !isEntityBPlayer && b.hasComponent(BulletComponent.class);
boolean isEntityAAsteroid = !isEntityAPlayer && !isEntityABullet && a.hasComponent(AsteroidComponent.class);
boolean isEntityBAsteroid = !isEntityBPlayer && !isEntityBBullet && b.hasComponent(AsteroidComponent.class);
if (isEntityAAsteroid || isEntityBAsteroid) {
if (isEntityAPlayer && isEntityBAsteroid) {
return new PlayerASteroidCollisionEvent(a, b);
} else if (isEntityBPlayer && isEntityAAsteroid) {
return new PlayerASteroidCollisionEvent(b, a);
} else if (isEntityABullet && isEntityBAsteroid) {
return new BulletAsteroidCollisionEvent(a, b);
} else if (isEntityBBullet && isEntityAAsteroid) {
return new BulletAsteroidCollisionEvent(b, a);
}
}
return new CollisionEvent(a, b);
}
}

View File

@ -7,6 +7,8 @@ import java.lang.Math;
public final class Utils { public final class Utils {
private static final Vector2 tmp = new Vector2();
public static float rotate(float rotation, float degrees) { public static float rotate(float rotation, float degrees) {
rotation += degrees; rotation += degrees;
if (rotation < 0) { if (rotation < 0) {
@ -25,4 +27,21 @@ public final class Utils {
return vector.set((float) Math.cos(radians), (float) Math.sin(radians)); return vector.set((float) Math.cos(radians), (float) Math.sin(radians));
} }
public static Vector2 getPointAlongPolygon(Vector2[] vertices, float distanceAlongPerimeter) {
for (int i = 0, n = vertices.length; i < n; i++) {
Vector2 a = vertices[i];
Vector2 b = vertices[i + 1 == n ? 0 : i + 1];
tmp.set(a).sub(b);
float len = tmp.len();
if (len < distanceAlongPerimeter) {
distanceAlongPerimeter -= len;
continue;
}
return a.add(tmp.set(tmp.x / len, tmp.y / len).scl(distanceAlongPerimeter));
}
return null;
}
} }

View File

@ -0,0 +1,7 @@
package com.me.asteroids.components;
import com.me.common.ecs.Component;
public class AsteroidComponent implements Component {
// TODO: See PlayerComponent's TODO
}

View File

@ -0,0 +1,19 @@
package com.me.asteroids.events;
import com.me.common.ecs.Entity;
public class BulletAsteroidCollisionEvent extends CollisionEvent {
public BulletAsteroidCollisionEvent(Entity bullet, Entity asteroid) {
super(bullet, asteroid);
}
public Entity getBullet() {
return entityA;
}
public Entity getAsteroid() {
return entityB;
}
}

View File

@ -0,0 +1,19 @@
package com.me.asteroids.events;
import com.me.common.ecs.Entity;
public class PlayerASteroidCollisionEvent extends CollisionEvent {
public PlayerASteroidCollisionEvent(Entity player, Entity asteroid) {
super(player, asteroid);
}
public Entity getPlayer() {
return entityA;
}
public Entity getAsteroid() {
return entityB;
}
}

View File

@ -4,22 +4,28 @@ import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.graphics.Color; import com.badlogic.gdx.graphics.Color;
import com.badlogic.gdx.graphics.g2d.BitmapFont; import com.badlogic.gdx.graphics.g2d.BitmapFont;
import com.badlogic.gdx.graphics.g2d.SpriteBatch; import com.badlogic.gdx.graphics.g2d.SpriteBatch;
import com.badlogic.gdx.math.Polygon;
import com.badlogic.gdx.math.Vector2;
import com.me.asteroids.Constants; import com.me.asteroids.Constants;
import com.me.asteroids.EntityFactory; import com.me.asteroids.EntityFactory;
import com.me.asteroids.Graphics; import com.me.asteroids.Graphics;
import com.me.asteroids.components.AccelerationComponent; import com.me.asteroids.components.AccelerationComponent;
import com.me.asteroids.components.AsteroidComponent;
import com.me.asteroids.components.BulletComponent; import com.me.asteroids.components.BulletComponent;
import com.me.asteroids.components.ModelComponent; import com.me.asteroids.components.ModelComponent;
import com.me.asteroids.components.PlayerComponent; import com.me.asteroids.components.PlayerComponent;
import com.me.asteroids.components.PositionComponent; import com.me.asteroids.components.PositionComponent;
import com.me.asteroids.components.VelocityComponent; import com.me.asteroids.components.VelocityComponent;
import com.me.asteroids.events.BulletAsteroidCollisionEvent;
import com.me.asteroids.events.ScreenWrapEvent; import com.me.asteroids.events.ScreenWrapEvent;
import com.me.asteroids.systems.AsteroidSpawningSystem;
import com.me.asteroids.systems.CollisionSystem; import com.me.asteroids.systems.CollisionSystem;
import com.me.asteroids.systems.ModelRenderSystem; import com.me.asteroids.systems.ModelRenderSystem;
import com.me.asteroids.systems.MovementSystem; import com.me.asteroids.systems.MovementSystem;
import com.me.asteroids.systems.PlayerInputSystem; import com.me.asteroids.systems.PlayerInputSystem;
import com.me.asteroids.systems.ScreenWrapSystem; import com.me.asteroids.systems.ScreenWrapSystem;
import com.me.common.Screen; import com.me.common.Screen;
import com.me.common.ecs.ComponentMapper;
import com.me.common.ecs.Engine; import com.me.common.ecs.Engine;
import com.me.common.ecs.Entity; import com.me.common.ecs.Entity;
import com.me.common.ecs.event.EventHandler; import com.me.common.ecs.event.EventHandler;
@ -48,18 +54,20 @@ public class GameScreen extends Screen implements Listener {
engine.registerComponentClass(PlayerComponent.class); engine.registerComponentClass(PlayerComponent.class);
engine.registerComponentClass(BulletComponent.class); engine.registerComponentClass(BulletComponent.class);
engine.registerComponentClass(AsteroidComponent.class);
engine.registerComponentClass(PositionComponent.class); engine.registerComponentClass(PositionComponent.class);
engine.registerComponentClass(VelocityComponent.class); engine.registerComponentClass(VelocityComponent.class);
engine.registerComponentClass(AccelerationComponent.class); engine.registerComponentClass(AccelerationComponent.class);
engine.registerComponentClass(ModelComponent.class); engine.registerComponentClass(ModelComponent.class);
engine.registerSystem(new PlayerInputSystem(engine)); engine.registerSystem(new PlayerInputSystem(engine));
engine.registerSystem(new AsteroidSpawningSystem(engine));
engine.registerSystem(new MovementSystem(engine)); engine.registerSystem(new MovementSystem(engine));
engine.registerSystem(new CollisionSystem(engine)); engine.registerSystem(new CollisionSystem(engine));
engine.registerSystem(new ScreenWrapSystem(engine)); engine.registerSystem(new ScreenWrapSystem(engine));
engine.registerSystem(new ModelRenderSystem(engine, graphics)); engine.registerSystem(new ModelRenderSystem(engine, graphics));
engine.registerListener(this); engine.registerListener(this.new EventListener(engine));
engine.ready(); engine.ready();
@ -84,14 +92,41 @@ public class GameScreen extends Screen implements Listener {
} }
private class EventListener implements Listener {
ComponentMapper<PositionComponent> positionMapper;
ComponentMapper<ModelComponent> modelMapper;
ComponentMapper<BulletComponent> bulletMapper;
EventListener(Engine engine) {
this.positionMapper = engine.getComponentMapper(PositionComponent.class);
this.modelMapper = engine.getComponentMapper(ModelComponent.class);
this.bulletMapper = engine.getComponentMapper(BulletComponent.class);
}
@EventHandler @EventHandler
public void onScreenWrap(ScreenWrapEvent event) { public void onScreenWrap(ScreenWrapEvent event) {
if (event.entity.hasComponent(BulletComponent.class)) { if (bulletMapper.has(event.entity)) {
// Remove bullets when they leave the screen // Remove bullets when they leave the screen
event.setCancelled(true); //event.setCancelled(true);
event.entity.remove(); event.entity.remove();
} }
} }
@EventHandler
public void onBulletAsteroidCollision(BulletAsteroidCollisionEvent event) {
Vector2 bulletPosition = positionMapper.get(event.getBullet()).position;
Polygon asteroidModel = modelMapper.get(event.getAsteroid()).model;
if (asteroidModel.contains(bulletPosition)) {
// AABBs intersect but let's only consider it a hit if the bullet's position
// is actually inside the asteroid
event.getBullet().remove();
event.getAsteroid().remove();
}
}
}
} }

View File

@ -0,0 +1,88 @@
package com.me.asteroids.systems;
import com.badlogic.gdx.math.Vector2;
import com.badlogic.gdx.utils.Array;
import com.me.asteroids.Constants;
import com.me.asteroids.EntityFactory;
import com.me.asteroids.Utils;
import com.me.asteroids.components.AsteroidComponent;
import com.me.asteroids.components.ModelComponent;
import com.me.asteroids.components.PositionComponent;
import com.me.asteroids.components.VelocityComponent;
import com.me.common.ecs.BaseSystem;
import com.me.common.ecs.ComponentMapper;
import com.me.common.ecs.Engine;
import com.me.common.ecs.Entity;
import static com.me.asteroids.Constants.rand;
public class AsteroidSpawningSystem extends BaseSystem {
private ComponentMapper<AsteroidComponent> asteroidMapper;
private ComponentMapper<PositionComponent> positionMapper;
private ComponentMapper<VelocityComponent> velocityMapper;
private ComponentMapper<ModelComponent> modelMapper;
private float asteroidSpawnDelay = 0f;
public AsteroidSpawningSystem(Engine engine) {
super(engine);
asteroidMapper = engine.getComponentMapper(AsteroidComponent.class);
positionMapper = engine.getComponentMapper(PositionComponent.class);
velocityMapper = engine.getComponentMapper(VelocityComponent.class);
modelMapper = engine.getComponentMapper(ModelComponent.class);
}
private Vector2 getRandomSpawnLocation(float asteroidWidth, float asteroidHeight) {
float halfWidth = asteroidWidth / 2;
float halfHeight = asteroidHeight / 2;
Vector2[] spawnPerimeter = new Vector2[]{
new Vector2(-halfWidth, -halfHeight),
new Vector2(-halfWidth, Constants.HEIGHT + halfHeight),
new Vector2(Constants.WIDTH + halfWidth, Constants.HEIGHT + halfHeight),
new Vector2(Constants.WIDTH + halfWidth, -halfHeight),
};
float perimeterLength = (Constants.WIDTH + Constants.HEIGHT + asteroidWidth + asteroidHeight) * 2;
float positionAlongPerimeter = rand.nextFloat(0, perimeterLength);
return Utils.getPointAlongPolygon(spawnPerimeter, positionAlongPerimeter);
}
private void spawnAsteroid() {
Entity asteroid = EntityFactory.createAsteroid(engine);
ModelComponent model = modelMapper.get(asteroid);
Vector2 position
= positionMapper.get(asteroid).position
= getRandomSpawnLocation(model.aabb.getWidth(), model.aabb.getHeight());
model.model.setPosition(position.x, position.y);
model.aabb = model.model.getBoundingRectangle();
VelocityComponent velocityComponent = velocityMapper.get(asteroid);
velocityComponent.velocity = new Vector2().setToRandomDirection().scl(rand.nextFloat(125, 175));
asteroid.activate();
}
@Override
public void process(float dt) {
Array<Entity> entities = engine.getEntities();
int asteroidCount = 0;
for (Entity entity : entities) {
// It's rather inefficient to have to check our entire entity list every frame
// to count how many entities have a specific component. Maybe we should keep a count of
// how many entites a given component?
if (asteroidMapper.has(entity)) {
asteroidCount++;
}
}
if (asteroidCount++ < 6 && (asteroidSpawnDelay -= dt) <= 0) {
spawnAsteroid();
asteroidSpawnDelay = Constants.ASTEROID_SPAWN_DELAY;
}
}
}

View File

@ -1,6 +1,7 @@
package com.me.asteroids.systems; package com.me.asteroids.systems;
import com.badlogic.gdx.math.Rectangle; import com.badlogic.gdx.math.Rectangle;
import com.me.asteroids.EventFactory;
import com.me.asteroids.components.ModelComponent; import com.me.asteroids.components.ModelComponent;
import com.me.asteroids.events.CollisionEvent; import com.me.asteroids.events.CollisionEvent;
import com.me.common.ecs.ComponentMapper; import com.me.common.ecs.ComponentMapper;
@ -29,8 +30,7 @@ public class CollisionSystem extends EntitySystem {
Rectangle aabbB = modelMapper.get(entityB).aabb; Rectangle aabbB = modelMapper.get(entityB).aabb;
if (aabbA.overlaps(aabbB)) { if (aabbA.overlaps(aabbB)) {
CollisionEvent event = new CollisionEvent(entityA, entityB); engine.callEvent(EventFactory.getNewCollisionEvent(entityA, entityB));
engine.callEvent(event);
} }
} }
} }