diff --git a/core/src/com/me/asteroids/AsteroidFactory.java b/core/src/com/me/asteroids/AsteroidFactory.java new file mode 100644 index 0000000..e8ec6b9 --- /dev/null +++ b/core/src/com/me/asteroids/AsteroidFactory.java @@ -0,0 +1,121 @@ +package com.me.asteroids; + +import com.badlogic.gdx.math.MathUtils; +import com.badlogic.gdx.math.Polygon; +import com.badlogic.gdx.math.Vector2; +import com.me.common.Random; + +public final class AsteroidFactory { + + public static final Random rand = new Random(); + + int vertexCount; + float size; + + float sizeVariation; + float angleVariation; + + boolean sizeRelativeToLast; + + public AsteroidFactory setVertexCount(int vertexCount) { + this.vertexCount = vertexCount; + return this; + } + + public AsteroidFactory setSize(float size) { + this.size = size; + return this; + } + + public AsteroidFactory setSizeVariation(float sizeVariation) { + this.sizeVariation = sizeVariation; + return this; + } + + public AsteroidFactory sizeRelativeToLast() { + this.sizeRelativeToLast = true; + return this; + } + + public AsteroidFactory sizeRelativeToInitial() { + this.sizeRelativeToLast = false; + return this; + } + + public AsteroidFactory setAngleVariation(float angleVariation) { + this.angleVariation = angleVariation; + return this; + } + + private void validate() { + if (vertexCount <= 2) { + throw new IllegalStateException(String.format("Illegal vertexCount: %d. Must be >= 3.", vertexCount)); + } + if (size <= 0) { + throw new IllegalStateException(String.format("Illegal vertexCount: %f. Must be > 0.", size)); + } + if (sizeVariation < 0) { + throw new IllegalStateException(String.format("Illegal sizeVariation: %f. Must be >= 0.", sizeVariation)); + } + if (sizeVariation > size) { + throw new IllegalStateException(String.format("Illegal sizeVariation: %f. Must be <= size.", sizeVariation)); + } + if (angleVariation < 0) { + throw new IllegalStateException(String.format("Illegal angleVariation: %f. Must be >= 0.", angleVariation)); + } + if (angleVariation > MathUtils.PI2 / vertexCount / 2) { + throw new IllegalStateException(String.format("Illegal angleVariation: %f. May cause vertexes positions to swap.", angleVariation)); + } + } + + private Vector2 applyAngleVariation(Vector2 vertex) { + if (angleVariation > 0) { + float half = angleVariation * 0.5f; + vertex.rotateRad(rand.nextFloat(-half, half)); + } + return vertex; + } + + private float applySizeVariation(float size) { + if (sizeVariation > 0) { + float half = sizeVariation * 0.5f; + float variation = rand.nextFloat(-half, half); + if (sizeRelativeToLast) { + size += variation; + size = MathUtils.clamp(size, this.size - half, this.size + half); + } else { + size = this.size + variation; + } + } + return size; + } + + public Polygon generate() { + validate(); + + float angleStep = MathUtils.PI2 / vertexCount; + + // Pick a random starting angle + float startAngle = rand.nextFloat() * MathUtils.PI2; + Vector2 dir = new Vector2(MathUtils.cos(startAngle), MathUtils.sin(startAngle)); + + float lastSize = size; + float[] vertices = new float[vertexCount * 2]; + + for (int i = 0; i < vertexCount; i++) { + Vector2 vertex = dir.cpy(); + + vertex = applyAngleVariation(vertex); + lastSize = applySizeVariation(lastSize); + + vertex.scl(lastSize); + vertices[i * 2] = vertex.x; + vertices[(i * 2) + 1] = vertex.y; + + dir.rotateRad(angleStep); + } + + return new Polygon(vertices); + } + +} diff --git a/core/src/com/me/asteroids/Asteroids.java b/core/src/com/me/asteroids/Asteroids.java new file mode 100644 index 0000000..32942f5 --- /dev/null +++ b/core/src/com/me/asteroids/Asteroids.java @@ -0,0 +1,39 @@ +package com.me.asteroids; + +import com.badlogic.gdx.ApplicationAdapter; +import com.badlogic.gdx.Gdx; +import com.me.asteroids.screens.GameScreen; +import com.me.common.Game; + +public class Asteroids extends ApplicationAdapter { + + public Graphics graphics; + private Game game; + + @Override + public void create () { + graphics = new Graphics(Constants.WIDTH, Constants.HEIGHT); + graphics.initialize(); + + game = new Game(); + game.setNextScreen(new GameScreen(graphics)); + } + + @Override + public void render () { + game.update(Gdx.graphics.getDeltaTime()); + game.render(); + } + + @Override + public void dispose () { + graphics.dispose(); + game.dispose(); + } + + @Override + public void resize(int width, int height) { + graphics.setScreenSize(width, height); + } + +} diff --git a/core/src/com/me/asteroids/Constants.java b/core/src/com/me/asteroids/Constants.java new file mode 100644 index 0000000..06c99e3 --- /dev/null +++ b/core/src/com/me/asteroids/Constants.java @@ -0,0 +1,11 @@ +package com.me.asteroids; + +public class Constants { + + public static final int WIDTH = 800; + public static final int HEIGHT = 600; + + public static final int HALF_WIDTH = WIDTH / 2; + public static final int HALF_HEIGHT = HEIGHT / 2; + +} diff --git a/core/src/com/me/asteroids/Graphics.java b/core/src/com/me/asteroids/Graphics.java new file mode 100644 index 0000000..8a870f3 --- /dev/null +++ b/core/src/com/me/asteroids/Graphics.java @@ -0,0 +1,63 @@ +package com.me.asteroids; + +import com.badlogic.gdx.Gdx; +import com.badlogic.gdx.graphics.Camera; +import com.badlogic.gdx.graphics.GL20; +import com.badlogic.gdx.graphics.OrthographicCamera; +import com.badlogic.gdx.graphics.glutils.ShapeRenderer; +import com.badlogic.gdx.utils.viewport.FitViewport; +import com.badlogic.gdx.utils.viewport.Viewport; + +public class Graphics { + + private int worldWidth, worldHeight; + private int screenWidth, screenHeight; + + private Camera camera; + private Viewport viewport; + + private ShapeRenderer shapeRenderer; + + public Graphics(int worldWidth, int worldHeight) { + this.worldWidth = worldWidth; + this.worldHeight = worldHeight; + this.screenWidth = Gdx.graphics.getWidth(); + this.screenHeight = Gdx.graphics.getHeight(); + + this.camera = new OrthographicCamera(); + this.viewport = new FitViewport(worldHeight, worldWidth, camera); + + this.shapeRenderer = new ShapeRenderer(); + } + + public void initialize() { + Gdx.gl.glClearColor(0, 0, 0, 1); + updateDimensions(); + } + + public void reset() { + Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT); + } + + public void setScreenSize(int width, int height) { + screenWidth = width; + screenHeight = height; + updateDimensions(); + } + + public void dispose() { + shapeRenderer.dispose(); + } + + public ShapeRenderer getShapeRenderer() { + return shapeRenderer; + } + + private void updateDimensions() { + viewport.setWorldSize(worldWidth, worldHeight); + viewport.update(screenWidth, screenHeight, true); + + shapeRenderer.setProjectionMatrix(camera.combined); + } + +} diff --git a/core/src/com/me/asteroids/Utils.java b/core/src/com/me/asteroids/Utils.java new file mode 100644 index 0000000..fc75f1a --- /dev/null +++ b/core/src/com/me/asteroids/Utils.java @@ -0,0 +1,28 @@ +package com.me.asteroids; + +import com.badlogic.gdx.math.MathUtils; +import com.badlogic.gdx.math.Vector2; + +import java.lang.Math; + +public final class Utils { + + public static float rotate(float rotation, float degrees) { + rotation += degrees; + if (rotation < 0) { + rotation = 360 - rotation; + } else if (rotation > 360) { + rotation -= 360; + } + return rotation; + } + + public static Vector2 setUnitVectorAngle(Vector2 vector, float degrees) { + return vector.set((float) Math.cos(degrees * MathUtils.degreesToRadians), (float) Math.sin(degrees * MathUtils.degreesToRadians)); + } + + public static Vector2 setUnitVectorAngleRad(Vector2 vector, float radians) { + return vector.set((float) Math.cos(radians), (float) Math.sin(radians)); + } + +} diff --git a/core/src/com/me/asteroids/screens/GameScreen.java b/core/src/com/me/asteroids/screens/GameScreen.java new file mode 100644 index 0000000..59f040c --- /dev/null +++ b/core/src/com/me/asteroids/screens/GameScreen.java @@ -0,0 +1,29 @@ +package com.me.asteroids.screens; + +import com.me.asteroids.Graphics; +import com.me.common.Screen; + +public class GameScreen extends Screen { + + Graphics graphics; + + public GameScreen(Graphics graphics) { + this.graphics = graphics; + } + + @Override + public void setup() { + + } + + @Override + public void update(float dt) { + graphics.reset(); + } + + @Override + public void dispose() { + + } + +} diff --git a/core/src/com/me/common/Game.java b/core/src/com/me/common/Game.java new file mode 100644 index 0000000..138f452 --- /dev/null +++ b/core/src/com/me/common/Game.java @@ -0,0 +1,40 @@ +package com.me.common; + +public class Game { + + private Screen screen; + private Screen nextScreen; + + public void update(float dt) { + if (nextScreen != null) { + handleScreeUpdate(); + } + + if (screen != null) { + screen.update(dt); + } + } + + public void render() { + if (screen != null) { + screen.render(); + } + } + + private void handleScreeUpdate() { + if (screen != null) screen.dispose(); + + nextScreen.setup(); + screen = nextScreen; + nextScreen = null; + } + + public void setNextScreen(Screen screen) { + nextScreen = screen; + } + + public void dispose() { + screen.dispose(); + } + +} diff --git a/core/src/com/me/common/MockRenderer.java b/core/src/com/me/common/MockRenderer.java new file mode 100644 index 0000000..ca09976 --- /dev/null +++ b/core/src/com/me/common/MockRenderer.java @@ -0,0 +1,8 @@ +package com.me.common; + +public class MockRenderer implements Renderer { + + @Override + public void render() {} + +} diff --git a/core/src/com/me/common/MockScreen.java b/core/src/com/me/common/MockScreen.java new file mode 100644 index 0000000..3734ea2 --- /dev/null +++ b/core/src/com/me/common/MockScreen.java @@ -0,0 +1,13 @@ +package com.me.common; + +public class MockScreen extends Screen { + + public void setup() { + setRenderer(new MockRenderer()); + } + + public void update(float dt) {} + + public void dispose() {} + +} diff --git a/core/src/com/me/common/Random.java b/core/src/com/me/common/Random.java new file mode 100644 index 0000000..462f505 --- /dev/null +++ b/core/src/com/me/common/Random.java @@ -0,0 +1,21 @@ +package com.me.common; + +public class Random extends java.util.Random { + + public Random() { + super(); + } + + public Random(long seed) { + super(seed); + } + + public float nextFloat(float min, float max) { + return min + (nextFloat() * Math.abs(min - max)); + } + + public int nextInt(int min, int max) { + return min + nextInt(Math.abs(min - max)); + } + +} diff --git a/core/src/com/me/common/Renderer.java b/core/src/com/me/common/Renderer.java new file mode 100644 index 0000000..b25e5d8 --- /dev/null +++ b/core/src/com/me/common/Renderer.java @@ -0,0 +1,7 @@ +package com.me.common; + +public interface Renderer { + + void render(); + +} diff --git a/core/src/com/me/common/Screen.java b/core/src/com/me/common/Screen.java new file mode 100644 index 0000000..26f4391 --- /dev/null +++ b/core/src/com/me/common/Screen.java @@ -0,0 +1,23 @@ +package com.me.common; + +public abstract class Screen { + + private Renderer renderer; + + public abstract void setup(); + + public abstract void update(float dt); + + public abstract void dispose(); + + public void setRenderer(Renderer renderer) { + this.renderer = renderer; + } + + public void render() { + if (renderer != null) { + renderer.render(); + } + } + +} diff --git a/core/src/com/me/common/ecs/Component.java b/core/src/com/me/common/ecs/Component.java new file mode 100644 index 0000000..ce66296 --- /dev/null +++ b/core/src/com/me/common/ecs/Component.java @@ -0,0 +1,5 @@ +package com.me.common.ecs; + +public abstract class Component { + +} diff --git a/core/src/com/me/common/ecs/ComponentBag.java b/core/src/com/me/common/ecs/ComponentBag.java new file mode 100644 index 0000000..88407e4 --- /dev/null +++ b/core/src/com/me/common/ecs/ComponentBag.java @@ -0,0 +1,41 @@ +package com.me.common.ecs; + +public class ComponentBag { + + private Component[] items; + private int size; + + public ComponentBag() { + this.items = new Component[16]; + this.size = items.length; + } + + public Component get(int index) { + if (index > size) { + throw new IndexOutOfBoundsException("index > size"); + } + return items[index]; + } + + public void insert(int index, Component item) { + if (index >= size) { + grow((int) (index * 1.5f)); + } + items[index] = item; + } + + public void remove(int index) { + if (index >= size) { + throw new IndexOutOfBoundsException("index must be < size"); + } + items[index] = null; + } + + private void grow(int newSize) { + Component[] newItems = new Component[newSize]; + System.arraycopy(this.items, 0, newItems, 0, size); + size = newSize; + this.items = newItems; + } + +} diff --git a/core/src/com/me/common/ecs/ComponentType.java b/core/src/com/me/common/ecs/ComponentType.java new file mode 100644 index 0000000..6436fba --- /dev/null +++ b/core/src/com/me/common/ecs/ComponentType.java @@ -0,0 +1,78 @@ +package com.me.common.ecs; + +import java.util.HashMap; +import java.util.Map; + +public class ComponentType { + + private static final Map, ComponentType> types = new HashMap<>(); + private static final ComponentType[] typeById = new ComponentType[Long.SIZE]; + + private static long nextBit = 1l; + private static int nextId = 0; + + private long bits; + private int id; + + private ComponentType() { + this.bits = nextBit; + this.id = nextId++; + + this.nextBit <<= 1; + } + + protected long getBits() { + return bits; + } + + protected int getId() { + return this.id; + } + + protected static long getMaskBits(Class... components) { + long mask = 0l; + for (Class clazz : components) { + mask |= getTypeBits(clazz); + } + return mask; + } + + protected static long getTypeBits(Class component) { + return getComponentType(component).getBits(); + } + + protected static int getTypeId(Class component) { + return getComponentType(component).getId(); + } + + protected static ComponentType getById(int id) { + return typeById[id]; + } + + protected boolean isTypeInMask(long mask) { + return (bits & mask) == mask; + } + + protected static void registerComponentType(Class component) { + ComponentType type = types.get(component); + if (type != null) { + throw new IllegalArgumentException(component.getName() + " has already been registered."); + } + type = new ComponentType(); + types.put(component, type); + typeById[type.getId()] = type; + } + + protected static ComponentType getComponentType(Class component) { + ComponentType type = types.get(component); + if (type == null) { + throw new IllegalArgumentException(component.getName() + " has not been registered."); + } + return type; + } + + protected static int getRegisteredComponentTypeCount() { + return types.size(); + } + +} diff --git a/core/src/com/me/common/ecs/Engine.java b/core/src/com/me/common/ecs/Engine.java new file mode 100644 index 0000000..b62976c --- /dev/null +++ b/core/src/com/me/common/ecs/Engine.java @@ -0,0 +1,90 @@ +package com.me.common.ecs; + +import com.badlogic.gdx.utils.Array; + +public class Engine { + + private Array entities; + private ComponentBag[] components; + private Array systems; + + public Engine() { + this.entities = new Array<>(); + this.systems = new Array<>(); + } + + public void registerComponentClass(Class clazz) { + ComponentType.registerComponentType(clazz); + } + + public void registerSystem(EntitySystem system) { + this.systems.add(system); + } + + public void ready() { + this.components = new ComponentBag[ComponentType.getRegisteredComponentTypeCount()]; + for (int i = 0; i < components.length; i++) { + components[i] = new ComponentBag(); + } + } + + public void update(float dt) { + for (EntitySystem system : systems) { + system.preProcessing(); + updateSystem(system, dt); + system.postProcessing(); + } + } + + public Entity createEntity() { + Entity entity = new Entity(this); + entities.add(entity); + return entity; + } + + public void removeEntity(Entity entity) { + removeAllEntityComponents(entity.getId()); + entities.removeValue(entity, true); + } + + private void removeAllEntityComponents(int entityId) { + for (int i = 0; i < components.length; i++) { + components[i].insert(entityId, null); + } + } + + protected void addEntityComponent(Entity entity, Component component) { + ComponentType type = ComponentType.getComponentType(component.getClass()); + components[type.getId()].insert(entity.getId(), component); + entity.addComponentType(type); + } + + protected void removeEntityComponent(Entity entity, Component component) { + ComponentType type = ComponentType.getComponentType(component.getClass()); + components[type.getId()].remove(entity.getId()); + entity.removeComponentType(type); + } + + protected T getEntityComponent(Entity entity, Class clazz) { + ComponentType type = ComponentType.getComponentType(clazz); + return clazz.cast(components[type.getId()].get(entity.getId())); + } + + protected void updateSystem(EntitySystem system, float dt) { + for (Entity entity : entities) { + if (!entity.isActive()) { + continue; + } + + // Check if this system is interested in this entity + if ((entity.getComponentBits() & system.getTypeMask()) != system.getTypeMask()) { + continue; + } + + // If so, process the entity + system.processEntity(entity, dt); + } + } + + +} diff --git a/core/src/com/me/common/ecs/Entity.java b/core/src/com/me/common/ecs/Entity.java new file mode 100644 index 0000000..f820c24 --- /dev/null +++ b/core/src/com/me/common/ecs/Entity.java @@ -0,0 +1,60 @@ +package com.me.common.ecs; + +public final class Entity { + + private static int nextId = 0; + + private Engine engine; + + private int id; + private boolean active; + + private long componentBits; + + protected Entity(Engine engine) { + this.engine = engine; + this.active = false; + this.id = nextId++; + } + + public int getId() { + return id; + } + + public boolean isActive() { + return active; + } + + public void activate() { + this.active = true; + } + + public void deactivate() { + this.active = false; + } + + public T getComponent(Class clazz) { + return engine.getEntityComponent(this, clazz); + } + + public void addComponent(Component component) { + engine.addEntityComponent(this, component); + } + + public void removeComponent(Component component) { + engine.removeEntityComponent(this, component); + } + + protected void addComponentType(ComponentType type) { + componentBits |= type.getBits(); + } + + protected void removeComponentType(ComponentType type) { + componentBits &= ~type.getBits(); + } + + protected long getComponentBits() { + return componentBits; + } + +} diff --git a/core/src/com/me/common/ecs/EntitySystem.java b/core/src/com/me/common/ecs/EntitySystem.java new file mode 100644 index 0000000..8a81d08 --- /dev/null +++ b/core/src/com/me/common/ecs/EntitySystem.java @@ -0,0 +1,25 @@ +package com.me.common.ecs; + +public abstract class EntitySystem { + + private long typeBits; + + public EntitySystem(Class... components) { + typeBits = ComponentType.getMaskBits(components); + } + + /** + * @return the type mask for this system. Only entities containing all component types specified + * by the mask will be processed by it. + */ + public long getTypeMask() { + return typeBits; + } + + public void preProcessing() {} + + public abstract void processEntity(Entity entity, float dt); + + public void postProcessing() {} + +}