From 042282646ba4a9f38961c491c67ca791baab2261 Mon Sep 17 00:00:00 2001 From: Matt Low Date: Mon, 27 Jan 2020 14:31:42 +0400 Subject: [PATCH] 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 --- core/src/com/me/asteroids/EntityFactory.java | 62 ++++++++++--- core/src/com/me/asteroids/EventFactory.java | 36 ++++++++ core/src/com/me/asteroids/Utils.java | 19 ++++ .../components/AsteroidComponent.java | 7 ++ .../events/BulletAsteroidCollisionEvent.java | 19 ++++ .../events/PlayerASteroidCollisionEvent.java | 19 ++++ .../com/me/asteroids/screens/GameScreen.java | 49 +++++++++-- .../systems/AsteroidSpawningSystem.java | 88 +++++++++++++++++++ .../me/asteroids/systems/CollisionSystem.java | 4 +- 9 files changed, 283 insertions(+), 20 deletions(-) create mode 100644 core/src/com/me/asteroids/EventFactory.java create mode 100644 core/src/com/me/asteroids/components/AsteroidComponent.java create mode 100644 core/src/com/me/asteroids/events/BulletAsteroidCollisionEvent.java create mode 100644 core/src/com/me/asteroids/events/PlayerASteroidCollisionEvent.java create mode 100644 core/src/com/me/asteroids/systems/AsteroidSpawningSystem.java diff --git a/core/src/com/me/asteroids/EntityFactory.java b/core/src/com/me/asteroids/EntityFactory.java index a11e1fe..e57f65f 100644 --- a/core/src/com/me/asteroids/EntityFactory.java +++ b/core/src/com/me/asteroids/EntityFactory.java @@ -3,6 +3,7 @@ package com.me.asteroids; import com.badlogic.gdx.math.Polygon; import com.badlogic.gdx.math.Vector2; import com.me.asteroids.components.AccelerationComponent; +import com.me.asteroids.components.AsteroidComponent; import com.me.asteroids.components.BulletComponent; import com.me.asteroids.components.ModelComponent; 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.Entity; +import static com.me.asteroids.Constants.rand; + 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) { PositionComponent position = new PositionComponent(); position.position = new Vector2(Constants.HALF_WIDTH, Constants.HALF_HEIGHT); @@ -35,7 +48,7 @@ public class EntityFactory { AccelerationComponent accel = new AccelerationComponent(); accel.acceleration = new Vector2(0,1f); - Entity player = engine.createEntity(); + Entity player = createEntity(engine); player.addComponent(position); player.addComponent(velocity); player.addComponent(model); @@ -45,26 +58,30 @@ public class EntityFactory { } public static Entity createBullet(Engine engine, Entity player) { - PositionComponent position = new PositionComponent(); float[] modelVertices = player.getComponent(ModelComponent.class).model.getTransformedVertices(); - position.position = new Vector2(modelVertices[0], modelVertices[1]); - position.rotation = player.getComponent(PositionComponent.class).rotation; + float rotation = player.getComponent(PositionComponent.class).rotation; + + Vector2 direction = Utils.setUnitVectorAngle(tmp, rotation); 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(); model.model = new Polygon(new float[] { - -1f, 2f, - 1f, 2f, - 1f, -2f, - -1f, -2f, + 1f, 0f, + -1f, 0f, + -1f, -4f, + 1f, -4f, }); 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(velocity); bullet.addComponent(model); @@ -72,4 +89,27 @@ public class EntityFactory { 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; + } + } diff --git a/core/src/com/me/asteroids/EventFactory.java b/core/src/com/me/asteroids/EventFactory.java new file mode 100644 index 0000000..3fd4f41 --- /dev/null +++ b/core/src/com/me/asteroids/EventFactory.java @@ -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); + } + +} diff --git a/core/src/com/me/asteroids/Utils.java b/core/src/com/me/asteroids/Utils.java index fc75f1a..a74f9a1 100644 --- a/core/src/com/me/asteroids/Utils.java +++ b/core/src/com/me/asteroids/Utils.java @@ -7,6 +7,8 @@ import java.lang.Math; public final class Utils { + private static final Vector2 tmp = new Vector2(); + public static float rotate(float rotation, float degrees) { rotation += degrees; if (rotation < 0) { @@ -25,4 +27,21 @@ public final class Utils { 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; + } + } diff --git a/core/src/com/me/asteroids/components/AsteroidComponent.java b/core/src/com/me/asteroids/components/AsteroidComponent.java new file mode 100644 index 0000000..68a5eb3 --- /dev/null +++ b/core/src/com/me/asteroids/components/AsteroidComponent.java @@ -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 +} diff --git a/core/src/com/me/asteroids/events/BulletAsteroidCollisionEvent.java b/core/src/com/me/asteroids/events/BulletAsteroidCollisionEvent.java new file mode 100644 index 0000000..3acbb62 --- /dev/null +++ b/core/src/com/me/asteroids/events/BulletAsteroidCollisionEvent.java @@ -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; + } + +} diff --git a/core/src/com/me/asteroids/events/PlayerASteroidCollisionEvent.java b/core/src/com/me/asteroids/events/PlayerASteroidCollisionEvent.java new file mode 100644 index 0000000..1361bbd --- /dev/null +++ b/core/src/com/me/asteroids/events/PlayerASteroidCollisionEvent.java @@ -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; + } + +} diff --git a/core/src/com/me/asteroids/screens/GameScreen.java b/core/src/com/me/asteroids/screens/GameScreen.java index 0e06fb7..260edcd 100644 --- a/core/src/com/me/asteroids/screens/GameScreen.java +++ b/core/src/com/me/asteroids/screens/GameScreen.java @@ -4,22 +4,28 @@ import com.badlogic.gdx.Gdx; import com.badlogic.gdx.graphics.Color; import com.badlogic.gdx.graphics.g2d.BitmapFont; 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.EntityFactory; import com.me.asteroids.Graphics; import com.me.asteroids.components.AccelerationComponent; +import com.me.asteroids.components.AsteroidComponent; import com.me.asteroids.components.BulletComponent; import com.me.asteroids.components.ModelComponent; import com.me.asteroids.components.PlayerComponent; import com.me.asteroids.components.PositionComponent; import com.me.asteroids.components.VelocityComponent; +import com.me.asteroids.events.BulletAsteroidCollisionEvent; import com.me.asteroids.events.ScreenWrapEvent; +import com.me.asteroids.systems.AsteroidSpawningSystem; import com.me.asteroids.systems.CollisionSystem; import com.me.asteroids.systems.ModelRenderSystem; import com.me.asteroids.systems.MovementSystem; import com.me.asteroids.systems.PlayerInputSystem; import com.me.asteroids.systems.ScreenWrapSystem; import com.me.common.Screen; +import com.me.common.ecs.ComponentMapper; import com.me.common.ecs.Engine; import com.me.common.ecs.Entity; import com.me.common.ecs.event.EventHandler; @@ -48,18 +54,20 @@ public class GameScreen extends Screen implements Listener { engine.registerComponentClass(PlayerComponent.class); engine.registerComponentClass(BulletComponent.class); + engine.registerComponentClass(AsteroidComponent.class); engine.registerComponentClass(PositionComponent.class); engine.registerComponentClass(VelocityComponent.class); engine.registerComponentClass(AccelerationComponent.class); engine.registerComponentClass(ModelComponent.class); engine.registerSystem(new PlayerInputSystem(engine)); + engine.registerSystem(new AsteroidSpawningSystem(engine)); engine.registerSystem(new MovementSystem(engine)); engine.registerSystem(new CollisionSystem(engine)); engine.registerSystem(new ScreenWrapSystem(engine)); engine.registerSystem(new ModelRenderSystem(engine, graphics)); - engine.registerListener(this); + engine.registerListener(this.new EventListener(engine)); engine.ready(); @@ -84,13 +92,40 @@ public class GameScreen extends Screen implements Listener { } - @EventHandler - public void onScreenWrap(ScreenWrapEvent event) { - if (event.entity.hasComponent(BulletComponent.class)) { - // Remove bullets when they leave the screen - event.setCancelled(true); - event.entity.remove(); + private class EventListener implements Listener { + + ComponentMapper positionMapper; + ComponentMapper modelMapper; + ComponentMapper bulletMapper; + + EventListener(Engine engine) { + this.positionMapper = engine.getComponentMapper(PositionComponent.class); + this.modelMapper = engine.getComponentMapper(ModelComponent.class); + this.bulletMapper = engine.getComponentMapper(BulletComponent.class); } + + @EventHandler + public void onScreenWrap(ScreenWrapEvent event) { + if (bulletMapper.has(event.entity)) { + // Remove bullets when they leave the screen + //event.setCancelled(true); + 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(); + } + } + } diff --git a/core/src/com/me/asteroids/systems/AsteroidSpawningSystem.java b/core/src/com/me/asteroids/systems/AsteroidSpawningSystem.java new file mode 100644 index 0000000..3be963c --- /dev/null +++ b/core/src/com/me/asteroids/systems/AsteroidSpawningSystem.java @@ -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 asteroidMapper; + private ComponentMapper positionMapper; + private ComponentMapper velocityMapper; + private ComponentMapper 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 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; + } + } + +} diff --git a/core/src/com/me/asteroids/systems/CollisionSystem.java b/core/src/com/me/asteroids/systems/CollisionSystem.java index a76c551..d181c22 100644 --- a/core/src/com/me/asteroids/systems/CollisionSystem.java +++ b/core/src/com/me/asteroids/systems/CollisionSystem.java @@ -1,6 +1,7 @@ package com.me.asteroids.systems; import com.badlogic.gdx.math.Rectangle; +import com.me.asteroids.EventFactory; import com.me.asteroids.components.ModelComponent; import com.me.asteroids.events.CollisionEvent; import com.me.common.ecs.ComponentMapper; @@ -29,8 +30,7 @@ public class CollisionSystem extends EntitySystem { Rectangle aabbB = modelMapper.get(entityB).aabb; if (aabbA.overlaps(aabbB)) { - CollisionEvent event = new CollisionEvent(entityA, entityB); - engine.callEvent(event); + engine.callEvent(EventFactory.getNewCollisionEvent(entityA, entityB)); } } }