From d052cbfa9d9487cf45bf548257e96293279310ed Mon Sep 17 00:00:00 2001 From: gered Date: Mon, 1 Jul 2013 12:10:36 -0400 Subject: [PATCH] initial commit --- .classpath | 7 + .gitignore | 9 + .project | 15 + src/com/blarg/gdx/GameApp.java | 81 +++ src/com/blarg/gdx/GdxGameAppListener.java | 67 ++ src/com/blarg/gdx/ReflectionUtils.java | 23 + src/com/blarg/gdx/Strings.java | 7 + src/com/blarg/gdx/entities/Component.java | 6 + .../blarg/gdx/entities/ComponentSystem.java | 38 ++ src/com/blarg/gdx/entities/Entity.java | 37 ++ src/com/blarg/gdx/entities/EntityManager.java | 263 ++++++++ .../systemcomponents/InactiveComponent.java | 9 + src/com/blarg/gdx/events/Event.java | 6 + src/com/blarg/gdx/events/EventHandler.java | 22 + src/com/blarg/gdx/events/EventListener.java | 5 + src/com/blarg/gdx/events/EventManager.java | 183 ++++++ .../gdx/graphics/BillboardSpriteBatch.java | 12 + .../graphics/DefaultScreenPixelScaler.java | 47 ++ .../gdx/graphics/DelayedSpriteBatch.java | 203 ++++++ .../graphics/NoScaleScreenPixelScaler.java | 25 + src/com/blarg/gdx/graphics/RenderContext.java | 107 +++ .../blarg/gdx/graphics/ScreenPixelScaler.java | 10 + .../gdx/graphics/SolidColorTextureCache.java | 57 ++ src/com/blarg/gdx/graphics/TTF.java | 16 + .../screeneffects/DimScreenEffect.java | 38 ++ .../graphics/screeneffects/EffectInfo.java | 15 + .../screeneffects/FadeScreenEffect.java | 89 +++ .../screeneffects/FlashScreenEffect.java | 63 ++ .../graphics/screeneffects/ScreenEffect.java | 37 ++ .../screeneffects/ScreenEffectManager.java | 157 +++++ src/com/blarg/gdx/processes/GameProcess.java | 76 +++ src/com/blarg/gdx/processes/ProcessInfo.java | 35 + .../blarg/gdx/processes/ProcessManager.java | 373 +++++++++++ src/com/blarg/gdx/states/GameState.java | 105 +++ src/com/blarg/gdx/states/StateInfo.java | 37 ++ src/com/blarg/gdx/states/StateManager.java | 608 ++++++++++++++++++ 36 files changed, 2888 insertions(+) create mode 100644 .classpath create mode 100644 .gitignore create mode 100644 .project create mode 100644 src/com/blarg/gdx/GameApp.java create mode 100644 src/com/blarg/gdx/GdxGameAppListener.java create mode 100644 src/com/blarg/gdx/ReflectionUtils.java create mode 100644 src/com/blarg/gdx/Strings.java create mode 100644 src/com/blarg/gdx/entities/Component.java create mode 100644 src/com/blarg/gdx/entities/ComponentSystem.java create mode 100644 src/com/blarg/gdx/entities/Entity.java create mode 100644 src/com/blarg/gdx/entities/EntityManager.java create mode 100644 src/com/blarg/gdx/entities/systemcomponents/InactiveComponent.java create mode 100644 src/com/blarg/gdx/events/Event.java create mode 100644 src/com/blarg/gdx/events/EventHandler.java create mode 100644 src/com/blarg/gdx/events/EventListener.java create mode 100644 src/com/blarg/gdx/events/EventManager.java create mode 100644 src/com/blarg/gdx/graphics/BillboardSpriteBatch.java create mode 100644 src/com/blarg/gdx/graphics/DefaultScreenPixelScaler.java create mode 100644 src/com/blarg/gdx/graphics/DelayedSpriteBatch.java create mode 100644 src/com/blarg/gdx/graphics/NoScaleScreenPixelScaler.java create mode 100644 src/com/blarg/gdx/graphics/RenderContext.java create mode 100644 src/com/blarg/gdx/graphics/ScreenPixelScaler.java create mode 100644 src/com/blarg/gdx/graphics/SolidColorTextureCache.java create mode 100644 src/com/blarg/gdx/graphics/TTF.java create mode 100644 src/com/blarg/gdx/graphics/screeneffects/DimScreenEffect.java create mode 100644 src/com/blarg/gdx/graphics/screeneffects/EffectInfo.java create mode 100644 src/com/blarg/gdx/graphics/screeneffects/FadeScreenEffect.java create mode 100644 src/com/blarg/gdx/graphics/screeneffects/FlashScreenEffect.java create mode 100644 src/com/blarg/gdx/graphics/screeneffects/ScreenEffect.java create mode 100644 src/com/blarg/gdx/graphics/screeneffects/ScreenEffectManager.java create mode 100644 src/com/blarg/gdx/processes/GameProcess.java create mode 100644 src/com/blarg/gdx/processes/ProcessInfo.java create mode 100644 src/com/blarg/gdx/processes/ProcessManager.java create mode 100644 src/com/blarg/gdx/states/GameState.java create mode 100644 src/com/blarg/gdx/states/StateInfo.java create mode 100644 src/com/blarg/gdx/states/StateManager.java diff --git a/.classpath b/.classpath new file mode 100644 index 0000000..c13c043 --- /dev/null +++ b/.classpath @@ -0,0 +1,7 @@ + + + + + + + diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..40c86ba --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +.DS_Store +out/ +log/ +target/ +.settings/ +*.iml +*.eml +*.class +*.jar diff --git a/.project b/.project new file mode 100644 index 0000000..8bfddfd --- /dev/null +++ b/.project @@ -0,0 +1,15 @@ + + + gdx-toolbox + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.jdt.core.javanature + + diff --git a/src/com/blarg/gdx/GameApp.java b/src/com/blarg/gdx/GameApp.java new file mode 100644 index 0000000..b4cbc5c --- /dev/null +++ b/src/com/blarg/gdx/GameApp.java @@ -0,0 +1,81 @@ +package com.blarg.gdx; + +import com.badlogic.gdx.Gdx; +import com.badlogic.gdx.utils.Disposable; +import com.badlogic.gdx.utils.TimeUtils; +import com.blarg.gdx.events.EventManager; +import com.blarg.gdx.graphics.RenderContext; +import com.blarg.gdx.states.StateManager; + +public abstract class GameApp implements Disposable { + public final EventManager eventManager; + public final StateManager stateManager; + public final RenderContext renderContext; + + boolean logHeapMemUsage = false; + long lastHeapMemLogTime = 0; + + public GameApp() { + Gdx.app.debug("GameApp", "ctor"); + + eventManager = new EventManager(); + stateManager = new StateManager(this, eventManager); + renderContext = new RenderContext(true); + } + + protected void toggleHeapMemUsageLogging(boolean enable) { + logHeapMemUsage = enable; + if (enable) + lastHeapMemLogTime = TimeUtils.millis(); + } + + public abstract void onCreate(); + + public void onResize(int width, int height) { + Gdx.app.debug("GameApp", String.format("onResize(%d, %d)", width, height)); + renderContext.onResize(width, height); + } + + public void onRender(float delta) { + renderContext.onPreRender(); + stateManager.onRender(delta, renderContext); + renderContext.onPostRender(); + } + + public void onUpdate(float delta) { + renderContext.onUpdate(delta); + stateManager.onUpdate(delta); + if (stateManager.isEmpty()) { + Gdx.app.debug("GameApp", "No states running. Quitting."); + Gdx.app.exit(); + return; + } + + if (logHeapMemUsage) { + long currentTime = TimeUtils.millis(); + if (currentTime - lastHeapMemLogTime > 1000) { + lastHeapMemLogTime = currentTime; + Gdx.app.debug("GameApp", String.format("Heap memory usage: %d", Gdx.app.getJavaHeap())); + } + } + } + + public void onPause() { + Gdx.app.debug("GameApp", "onPause"); + stateManager.onAppPause(); + renderContext.onPause(); + } + + public void onResume() { + Gdx.app.debug("GameApp", "onResume"); + renderContext.onResume(); + stateManager.onAppResume(); + } + + @Override + public void dispose() { + Gdx.app.debug("GameApp", "dispose"); + stateManager.dispose(); + renderContext.dispose(); + } +} diff --git a/src/com/blarg/gdx/GdxGameAppListener.java b/src/com/blarg/gdx/GdxGameAppListener.java new file mode 100644 index 0000000..0b2a857 --- /dev/null +++ b/src/com/blarg/gdx/GdxGameAppListener.java @@ -0,0 +1,67 @@ +package com.blarg.gdx; + +import com.badlogic.gdx.ApplicationListener; +import com.badlogic.gdx.Gdx; + +public class GdxGameAppListener implements ApplicationListener { + Class gameAppType; + GameApp gameApp; + + public GdxGameAppListener(Class gameAppType) { + this.gameAppType = gameAppType; + } + + @Override + public void create() { + Gdx.app.debug("GdxGameAppListener", "create"); + Gdx.app.debug("GdxGameAppListener", String.format("Application type: %s", Gdx.app.getType())); + + try { + gameApp = gameAppType.newInstance(); + } catch (Exception e) { + Gdx.app.log("GdxGameAppListener", String.format("Instantiation of GameApp object failed: %s", e)); + gameApp = null; + } + + if (gameApp == null) { + Gdx.app.log("GdxGameAppListener", "Failed to create a GameApp. Aborting."); + Gdx.app.exit(); + return; + } + + gameApp.onCreate(); + } + + @Override + public void resize(int width, int height) { + Gdx.app.debug("GdxGameAppListener", String.format("resize(%d, %d)", width, height)); + gameApp.onResize(width, height); + } + + @Override + public void render() { + // TODO: probably not the best idea to share the same delta with both renders and updates... + float delta = Gdx.graphics.getDeltaTime(); + gameApp.onUpdate(delta); + gameApp.onRender(delta); + } + + @Override + public void pause() { + Gdx.app.debug("GdxGameAppListener", "pause"); + gameApp.onPause(); + } + + @Override + public void resume() { + Gdx.app.debug("GdxGameAppListener", "resume"); + gameApp.onResume(); + } + + @Override + public void dispose() { + Gdx.app.debug("GdxGameAppListener", "dispose"); + if (gameApp != null) + gameApp.dispose(); + } +} diff --git a/src/com/blarg/gdx/ReflectionUtils.java b/src/com/blarg/gdx/ReflectionUtils.java new file mode 100644 index 0000000..e6b0e72 --- /dev/null +++ b/src/com/blarg/gdx/ReflectionUtils.java @@ -0,0 +1,23 @@ +package com.blarg.gdx; + +import java.lang.reflect.Constructor; + +public class ReflectionUtils { + public static T instantiateObject(Class type, Class[] constructorArgTypes, Object[] constructorArgValues) throws Exception { + Constructor constructor; + try { + constructor = type.getConstructor(constructorArgTypes); + } catch (NoSuchMethodException e) { + throw new Exception("No constructor found with these argument types."); + } + + T instance; + try { + instance = constructor.newInstance(constructorArgValues); + } catch (Exception e) { + throw new Exception("Could not create new instance of this class.", e); + } + + return instance; + } +} diff --git a/src/com/blarg/gdx/Strings.java b/src/com/blarg/gdx/Strings.java new file mode 100644 index 0000000..c699fce --- /dev/null +++ b/src/com/blarg/gdx/Strings.java @@ -0,0 +1,7 @@ +package com.blarg.gdx; + +public final class Strings { + public static boolean isNullOrEmpty(String s) { + return (s == null || s.isEmpty()); + } +} diff --git a/src/com/blarg/gdx/entities/Component.java b/src/com/blarg/gdx/entities/Component.java new file mode 100644 index 0000000..42ef95a --- /dev/null +++ b/src/com/blarg/gdx/entities/Component.java @@ -0,0 +1,6 @@ +package com.blarg.gdx.entities; + +import com.badlogic.gdx.utils.Pool; + +public abstract class Component implements Pool.Poolable { +} diff --git a/src/com/blarg/gdx/entities/ComponentSystem.java b/src/com/blarg/gdx/entities/ComponentSystem.java new file mode 100644 index 0000000..8e2dc50 --- /dev/null +++ b/src/com/blarg/gdx/entities/ComponentSystem.java @@ -0,0 +1,38 @@ +package com.blarg.gdx.entities; + +import com.blarg.gdx.events.Event; +import com.blarg.gdx.events.EventHandler; +import com.blarg.gdx.events.EventManager; +import com.blarg.gdx.graphics.RenderContext; + +public abstract class ComponentSystem extends EventHandler { + public final EntityManager entityManager; + + public ComponentSystem(EntityManager entityManager, EventManager eventManager) { + super(eventManager); + if (entityManager == null) + throw new IllegalArgumentException("entityManager can not be null."); + + this.entityManager = entityManager; + } + + public void onAppPause() { + } + + public void onAppResume() { + } + + public void onResize() { + } + + public void onRender(float delta, RenderContext renderContext) { + } + + public void onUpdate(float delta) { + } + + @Override + public boolean handle(Event e) { + return false; + } +} diff --git a/src/com/blarg/gdx/entities/Entity.java b/src/com/blarg/gdx/entities/Entity.java new file mode 100644 index 0000000..3f57b5f --- /dev/null +++ b/src/com/blarg/gdx/entities/Entity.java @@ -0,0 +1,37 @@ +package com.blarg.gdx.entities; + +// Yes, this class SHOULD be marked final. No, you ARE wrong for wanting to subclass this. +// There IS a better way to do what you were thinking of doing that DOESN'T involve +// subclassing Entity! +// Still don't agree with me? Read this (or really, any other article on entity systems in games): +// http://t-machine.org/index.php/2010/05/09/entity-system-1-javaandroid/ + +public final class Entity { + EntityManager entityManager; + + /** + * Do not instantiate Entity's directly. Use EntityManager.add(). + */ + public Entity(EntityManager entityManager) { + if (entityManager == null) + throw new IllegalArgumentException("entityManager can not be null."); + + this.entityManager = entityManager; + } + + public T get(Class componentType) { + return entityManager.getComponent(componentType, this); + } + + public T add(Class componentType) { + return entityManager.addComponent(componentType, this); + } + + public void remove(Class componentType) { + entityManager.removeComponent(componentType, this); + } + + public boolean has(Class componentType) { + return entityManager.hasComponent(componentType, this); + } +} diff --git a/src/com/blarg/gdx/entities/EntityManager.java b/src/com/blarg/gdx/entities/EntityManager.java new file mode 100644 index 0000000..ac8ff7b --- /dev/null +++ b/src/com/blarg/gdx/entities/EntityManager.java @@ -0,0 +1,263 @@ +package com.blarg.gdx.entities; + +import com.badlogic.gdx.utils.*; +import com.blarg.gdx.entities.systemcomponents.InactiveComponent; +import com.blarg.gdx.events.EventManager; +import com.blarg.gdx.ReflectionUtils; +import com.blarg.gdx.graphics.RenderContext; + +public class EntityManager { + public final EventManager eventManager; + + ObjectSet entities; + ObjectMap, ObjectMap> componentStore; + ObjectMap, Component> globalComponents; + Array componentSystems; + + Pool entityPool = new Pool() { + @Override + protected Entity newObject() { + return new Entity(EntityManager.this); + } + }; + + ObjectMap empty; + + public EntityManager(EventManager eventManager) { + if (eventManager == null) + throw new IllegalArgumentException("eventManager can not be null."); + + this.eventManager = eventManager; + entities = new ObjectSet(); + componentStore = new ObjectMap, ObjectMap>(); + globalComponents = new ObjectMap, Component>(); + componentSystems = new Array(); + + // possibly ugliness, but this allows us to return empty.keys() in getAllWith() + // when there are no entities with a given component, preventing the calling code + // from needing to check for null before a for-loop + empty = new ObjectMap(); + } + + /*** public ComponentSystem management */ + + public T addSubsystem(Class componentSystemType) { + if (getSubsystem(componentSystemType) != null) + throw new UnsupportedOperationException("ComponentSystem of that type is already registered."); + + T subsystem; + try { + subsystem = ReflectionUtils.instantiateObject(componentSystemType, + new Class[] { EntityManager.class, EventManager.class }, + new Object[] { this, eventManager }); + } catch (Exception e) { + throw new IllegalArgumentException("Could not instantiate this type of ComponentSystem.", e); + } + + componentSystems.add(subsystem); + return subsystem; + } + + public T getSubsystem(Class componentSystemType) { + int i = getSubsystemIndex(componentSystemType); + if (i == -1) + return null; + else + return componentSystemType.cast(componentSystems.get(i)); + } + + public void removeSubsystem(Class componentSystemType) { + int i = getSubsystemIndex(componentSystemType); + if (i == -1) + return; + + componentSystems.removeIndex(i); + } + + public void removeAllSubsystems() { + componentSystems.clear(); + } + + /*** public Entity management ***/ + + public Entity add() { + Entity entity = entityPool.obtain(); + entities.add(entity); + return entity; + } + + public Entity getFirstWith(Class componentType) { + ObjectMap componentEntities = componentStore.get(componentType); + if (componentEntities == null) + return null; + + if (componentEntities.size > 0) + return componentEntities.keys().next(); + else + return null; + } + + public ObjectMap.Keys getAllWith(Class componentType) { + ObjectMap componentEntities = componentStore.get(componentType); + if (componentEntities == null) + return empty.keys(); // calling code won't need to check for null + else + return componentEntities.keys(); + } + + public void remove(Entity entity) { + if (entity == null) + throw new IllegalArgumentException("entity can not be null."); + + removeAllComponentsFrom(entity); + entities.remove(entity); + + entityPool.free(entity); + } + + public void removeAll() { + for (Entity i : entities) + removeAllComponentsFrom(i); + + entities.clear(); + } + + /*** public Entity Component management ***/ + + public T addComponent(Class componentType, Entity entity) { + if (getComponent(componentType, entity) != null) + throw new UnsupportedOperationException("Component of that type has been added to this entity already."); + + // find the component-to-entity list for this component type, or create it if it doesn't exist yet + ObjectMap componentEntities = componentStore.get(componentType); + if (componentEntities == null) { + componentEntities = new ObjectMap(); + componentStore.put(componentType, componentEntities); + } + + T component = Pools.obtain(componentType); + + componentEntities.put(entity, component); + return componentType.cast(component); + } + + public T getComponent(Class componentType, Entity entity) { + ObjectMap componentEntities = componentStore.get(componentType); + if (componentEntities == null) + return null; + + Component existing = componentEntities.get(entity); + if (existing == null) + return null; + else + return componentType.cast(existing); + } + + public void removeComponent(Class componentType, Entity entity) { + ObjectMap componentEntities = componentStore.get(componentType); + if (componentEntities == null) + return; + + Component component = componentEntities.remove(entity); + Pools.free(component); + } + + public boolean hasComponent(Class componentType, Entity entity) { + ObjectMap componentEntities = componentStore.get(componentType); + if (componentEntities == null) + return false; + + return componentEntities.containsKey(entity); + } + + public void getAllComponentsFor(Entity entity, Array list) { + if (list == null) + throw new IllegalArgumentException("list can not be null."); + + for (ObjectMap.Entry, ObjectMap> i : componentStore.entries()) { + ObjectMap entitiesWithComponent = i.value; + Component component = entitiesWithComponent.get(entity); + if (component != null) + list.add(component); + } + } + + /*** global component management ***/ + + public T addGlobal(Class componentType) { + if (getGlobal(componentType) != null) + throw new UnsupportedOperationException("Global component of that type has been added already."); + + T component = Pools.obtain(componentType); + + globalComponents.put(componentType, component); + return componentType.cast(component); + } + + public T getGlobal(Class componentType) { + Component existing = globalComponents.get(componentType); + if (existing == null) + return null; + else + return componentType.cast(existing); + } + + public void removeGlobal(Class componentType) { + Component component = globalComponents.remove(componentType); + Pools.free(component); + } + + public boolean hasGlobal(Class componentType) { + return globalComponents.containsKey(componentType); + } + + /*** events ***/ + + public void onAppResume() { + for (int i = 0; i < componentSystems.size; ++i) + componentSystems.get(i).onAppResume(); + } + + public void onAppPause() { + for (int i = 0; i < componentSystems.size; ++i) + componentSystems.get(i).onAppPause(); + } + + public void onResize() { + for (int i = 0; i < componentSystems.size; ++i) + componentSystems.get(i).onResize(); + } + + public void onRender(float delta, RenderContext renderContext) { + for (int i = 0; i < componentSystems.size; ++i) + componentSystems.get(i).onRender(delta, renderContext); + } + + public void onUpdate(float delta) { + for (Entity i : getAllWith(InactiveComponent.class)) + remove(i); + + for (int i = 0; i < componentSystems.size; ++i) + componentSystems.get(i).onUpdate(delta); + } + + /*** private Entity/Component management ***/ + + private void removeAllComponentsFrom(Entity entity) { + if (entity == null) + throw new IllegalArgumentException("entity can not be null."); + + for (ObjectMap.Entry, ObjectMap> i : componentStore.entries()) { + ObjectMap entitiesWithComponent = i.value; + entitiesWithComponent.remove(entity); + } + } + + private int getSubsystemIndex(Class componentSystemType) { + for (int i = 0; i < componentSystems.size; ++i) { + if (componentSystems.get(i).getClass() == componentSystemType) + return i; + } + return -1; + } +} diff --git a/src/com/blarg/gdx/entities/systemcomponents/InactiveComponent.java b/src/com/blarg/gdx/entities/systemcomponents/InactiveComponent.java new file mode 100644 index 0000000..ab11098 --- /dev/null +++ b/src/com/blarg/gdx/entities/systemcomponents/InactiveComponent.java @@ -0,0 +1,9 @@ +package com.blarg.gdx.entities.systemcomponents; + +import com.blarg.gdx.entities.Component; + +public class InactiveComponent extends Component { + @Override + public void reset() { + } +} diff --git a/src/com/blarg/gdx/events/Event.java b/src/com/blarg/gdx/events/Event.java new file mode 100644 index 0000000..806e4ed --- /dev/null +++ b/src/com/blarg/gdx/events/Event.java @@ -0,0 +1,6 @@ +package com.blarg.gdx.events; + +import com.badlogic.gdx.utils.Pool; + +public abstract class Event implements Pool.Poolable { +} diff --git a/src/com/blarg/gdx/events/EventHandler.java b/src/com/blarg/gdx/events/EventHandler.java new file mode 100644 index 0000000..1cd7db4 --- /dev/null +++ b/src/com/blarg/gdx/events/EventHandler.java @@ -0,0 +1,22 @@ +package com.blarg.gdx.events; + +// "EventHandler" is a poor name, but better then what I used to call it: "EventListenerEx" + +public abstract class EventHandler implements EventListener { + public final EventManager eventManager; + + public EventHandler(EventManager eventManager) { + if (eventManager == null) + throw new IllegalArgumentException("eventManager can not be null."); + + this.eventManager = eventManager; + } + + public boolean listenFor(Class eventType) { + return eventManager.addListener(eventType, this); + } + + public boolean stopListeningFor(Class eventType) { + return eventManager.removeListener(eventType, this); + } +} diff --git a/src/com/blarg/gdx/events/EventListener.java b/src/com/blarg/gdx/events/EventListener.java new file mode 100644 index 0000000..abac36a --- /dev/null +++ b/src/com/blarg/gdx/events/EventListener.java @@ -0,0 +1,5 @@ +package com.blarg.gdx.events; + +public interface EventListener { + boolean handle(Event e); +} diff --git a/src/com/blarg/gdx/events/EventManager.java b/src/com/blarg/gdx/events/EventManager.java new file mode 100644 index 0000000..8bca7f7 --- /dev/null +++ b/src/com/blarg/gdx/events/EventManager.java @@ -0,0 +1,183 @@ +package com.blarg.gdx.events; + +import com.badlogic.gdx.Gdx; +import com.badlogic.gdx.utils.Array; +import com.badlogic.gdx.utils.ObjectMap; +import com.badlogic.gdx.utils.ObjectSet; +import com.badlogic.gdx.utils.Pools; + +import java.util.LinkedList; + +@SuppressWarnings("unchecked") +public class EventManager { + static final int NUM_EVENT_QUEUES = 2; + + ObjectSet> typeList; + ObjectMap, Array> registry; + LinkedList[] queues; + int activeQueue; + + public EventManager() { + Gdx.app.debug("EventManager", "ctor"); + typeList = new ObjectSet>(); + registry = new ObjectMap, Array>(); + queues = new LinkedList[NUM_EVENT_QUEUES]; + for (int i = 0; i < queues.length; ++i) + queues[i] = new LinkedList(); + + activeQueue = 0; + } + + public boolean addListener(Class eventType, EventListener listener) { + if (listener == null) + throw new IllegalArgumentException("listener can not be null."); + + Array listeners = registry.get(eventType); + if (listeners == null) { + // need to register this listener for the given type + listeners = new Array(); + registry.put(eventType, listeners); + } + + if (listeners.contains(listener, true)) + throw new IllegalArgumentException("Duplicate event listener registration."); + + listeners.add(listener); + Gdx.app.debug("EventManager", String.format("Added listener for event type: %s", eventType.getSimpleName())); + + // also update the list of currently registered event types + typeList.add(eventType); + + return true; + } + + public boolean removeListener(Class eventType, EventListener listener) { + if (listener == null) + throw new IllegalArgumentException("listener can not be null."); + + // get the listeners for this event type + Array listeners = registry.get(eventType); + if (listeners == null || !listeners.contains(listener, true)) + return false; // either no listeners for this type, or the listener wasn't registered with us + + listeners.removeValue(listener, true); + Gdx.app.debug("EventManager", String.format("Removed listener for event type: %s", eventType.getSimpleName())); + + // if there are no more listeners for this type, remove the event type + // from the list of registered event types + if (listeners.size == 0) + typeList.remove(eventType); + + return true; + } + + public boolean trigger(Event e) { + if (e == null) + throw new IllegalArgumentException("event can not be null."); + + Class type = e.getClass().asSubclass(Event.class); + + // find the listeners for this event type + Array listeners = registry.get(type); + if (listeners == null) + return false; // no listeners for this event type have been registered -- we can't handle the event + + // trigger event in each listener + boolean result = false; + for (EventListener listener : listeners) { + if (listener.handle(e)) { + result = true; + break; // don't let other listeners handle the event if this one signals it handled it + } + } + + // TODO: maybe, for trigger() only, it's better to force the calling code + // to "putback" the event object being triggered? since we handle the + // event immediately, unlike with queue() where it makes a lot more + // sense for us to place it back in the pool ourselves ... + free(e); + + // a result of "false" merely indicates that no listener indicates that it "handled" the event + return result; + } + + public boolean queue(Event e) { + if (e == null) + throw new IllegalArgumentException("event can not be null."); + + // validate that there is infact a listener for this event type + // (otherwise, we don't queue this event) + Class type = e.getClass().asSubclass(Event.class); + if (!typeList.contains(type)) + return false; + + queues[activeQueue].add(e); + + return true; + } + + public boolean abort(Class eventType) { + return abort(eventType, true); + } + + public boolean abort(Class eventType, boolean stopAfterFirstRemoval) { + // validate that there is infact a listener for this event type + // (otherwise, we don't queue this event) + if (!typeList.contains(eventType)) + return false; + + boolean result = false; + + // walk through the queue and remove matching events + LinkedList queue = queues[activeQueue]; + int i = 0; + while (i < queue.size()) { + if (queue.get(i).getClass() == eventType) { + Event e = queue.remove(i); + free(e); + result = true; + if (stopAfterFirstRemoval) + break; + } else + i++; + } + + return result; + } + + public boolean onUpdate(float delta) { + // swap active queues and empty the new queue + int queueToProcess = activeQueue; + activeQueue = (activeQueue + 1) % NUM_EVENT_QUEUES; + queues[activeQueue].clear(); + + // process the "old" queue + LinkedList queue = queues[queueToProcess]; + while (queue.size() > 0) { + Event e = queue.pop(); + + Class type = e.getClass().asSubclass(Event.class); + + // find the listeners for this event type + Array listeners = registry.get(type); + if (listeners != null) { + for (EventListener listener : listeners) { + if (listener.handle(e)) + break; // don't let other listeners handle the event if this one signals it handled it + } + } + + free(e); + } + + return true; + } + + public T create(Class eventType) { + return Pools.obtain(eventType); + } + + public void free(T event) { + Pools.free(event); + } +} diff --git a/src/com/blarg/gdx/graphics/BillboardSpriteBatch.java b/src/com/blarg/gdx/graphics/BillboardSpriteBatch.java new file mode 100644 index 0000000..bb92861 --- /dev/null +++ b/src/com/blarg/gdx/graphics/BillboardSpriteBatch.java @@ -0,0 +1,12 @@ +package com.blarg.gdx.graphics; + +/*** + *

+ * Wrapper over libgdx's included {@link DecalBatch} with automatic easy management of + * {@link Decal} objects. This is intended to make "on the fly" rendering of decals/billboards + * as easy as rendering 2D sprites is with SpriteBatch / DelayedSpriteBatch. + *

+ */ +public class BillboardSpriteBatch { + +} diff --git a/src/com/blarg/gdx/graphics/DefaultScreenPixelScaler.java b/src/com/blarg/gdx/graphics/DefaultScreenPixelScaler.java new file mode 100644 index 0000000..cf146b1 --- /dev/null +++ b/src/com/blarg/gdx/graphics/DefaultScreenPixelScaler.java @@ -0,0 +1,47 @@ +package com.blarg.gdx.graphics; + +public class DefaultScreenPixelScaler implements ScreenPixelScaler { + int scale = 0; + int viewportWidth = 0; + int viewportHeight = 0; + int scaledViewportWidth = 0; + int scaledViewportHeight = 0; + + @Override + public int getScale() { + return scale; + } + + @Override + public int getScaledWidth() { + return scaledViewportWidth; + } + + @Override + public int getScaledHeight() { + return scaledViewportHeight; + } + + @Override + public void calculateScale(int screenWidth, int screenHeight) { + viewportWidth = screenWidth; + viewportHeight = screenHeight; + + // TODO: these might need tweaking, this is fairly arbitrary + if (viewportWidth < 640 || viewportHeight < 480) + scale = 1; + else if (viewportWidth < 960 || viewportHeight < 720) + scale = 2; + else if (viewportWidth < 1280 || viewportHeight < 960) + scale = 3; + else if (viewportWidth < 1920 || viewportHeight < 1080) + scale = 4; + else + scale = 5; + + // TODO: desktop "retina" / 4K display sizes? 1440p? + + scaledViewportWidth = viewportWidth / scale; + scaledViewportHeight = viewportHeight / scale; + } +} diff --git a/src/com/blarg/gdx/graphics/DelayedSpriteBatch.java b/src/com/blarg/gdx/graphics/DelayedSpriteBatch.java new file mode 100644 index 0000000..a979af2 --- /dev/null +++ b/src/com/blarg/gdx/graphics/DelayedSpriteBatch.java @@ -0,0 +1,203 @@ +package com.blarg.gdx.graphics; + +import com.badlogic.gdx.graphics.Color; +import com.badlogic.gdx.graphics.Texture; +import com.badlogic.gdx.graphics.g2d.BitmapFont; +import com.badlogic.gdx.graphics.g2d.Sprite; +import com.badlogic.gdx.graphics.g2d.SpriteBatch; +import com.badlogic.gdx.graphics.g2d.TextureRegion; +import com.badlogic.gdx.utils.Array; + +/*** + *

+ * Wrapper over libgdx's included {@link SpriteBatch} which doesn't manipulate _any_ OpenGL + * state until the call to {@link SpriteBatch#end()}. This allows a begin/end block to + * wrap over a large amount of code that might manipulate OpenGL state one or more times. + * Sprites rendered with this are simply queued up until the end() call and rendered in that + * same order with {@link SpriteBatch}. + *

+ * + *

+ * This "delayed" queueing behaviour will not necessarily be desirable behaviour-wise / + * performance-wise in all situations. This class introduces a small bit of performance overhead + * compared to using {@link SpriteBatch} directly. + *

+ */ +public class DelayedSpriteBatch { + static final int CAPACITY_INCREMEMT = 128; + + Array sprites; + int pointer; + boolean hasBegun; + SpriteBatch spriteBatch; + + public DelayedSpriteBatch() { + sprites = new Array(true, CAPACITY_INCREMEMT, Sprite.class); + pointer = 0; + increaseCapacity(); + + hasBegun = false; + spriteBatch = null; + } + + public void begin(SpriteBatch spriteBatch) { + if (hasBegun) + throw new IllegalStateException("Cannot be called within an existing begin/end block."); + if (spriteBatch == null) + throw new IllegalArgumentException(); + + this.spriteBatch = spriteBatch; + hasBegun = true; + pointer = 0; + } + + public void draw(Texture texture, float x, float y) { + draw(texture, x, y, texture.getWidth(), texture.getHeight(), Color.WHITE); + } + + public void draw(Texture texture, float x, float y, Color tint) { + draw(texture, x, y, texture.getWidth(), texture.getHeight(), tint); + } + + public void draw(Texture texture, float x, float y, float width, float height) { + draw(texture, x, y, width, height, Color.WHITE); + } + + public void draw(Texture texture, float x, float y, float width, float height, Color tint) { + Sprite sprite = nextUsable(); + sprite.setTexture(texture); + sprite.setRegion(0, 0, texture.getWidth(), texture.getHeight()); + sprite.setColor(tint); + sprite.setSize(width, height); + sprite.setPosition(x, y); + } + + public void draw(Texture texture, float x, float y, float width, float height, float u, float v, float u2, float v2) { + draw(texture, x, y, width, height, u, v, u2, v2, Color.WHITE); + } + + public void draw(Texture texture, float x, float y, float width, float height, float u, float v, float u2, float v2, Color tint) { + Sprite sprite = nextUsable(); + sprite.setTexture(texture); + sprite.setRegion(u, v, u2, v2); + sprite.setColor(tint); + sprite.setSize(width, height); + sprite.setPosition(x, y); + } + + public void draw(Texture texture, float x, float y, int srcX, int srcY, int srcWidth, int srcHeight) { + draw(texture, x, y, srcX, srcY, srcWidth, srcHeight, Color.WHITE); + } + + public void draw(Texture texture, float x, float y, int srcX, int srcY, int srcWidth, int srcHeight, Color tint) { + Sprite sprite = nextUsable(); + sprite.setTexture(texture); + sprite.setRegion(srcX, srcY, srcWidth, srcHeight); + sprite.setColor(tint); + sprite.setSize(Math.abs(srcWidth), Math.abs(srcHeight)); + sprite.setPosition(x, y); + } + + public void draw(TextureRegion region, float x, float y) { + draw(region, x, y, region.getRegionWidth(), region.getRegionWidth(), Color.WHITE); + } + + public void draw(TextureRegion region, float x, float y, Color tint) { + draw(region, x, y, region.getRegionWidth(), region.getRegionWidth(), tint); + } + + public void draw(TextureRegion region, float x, float y, float width, float height) { + draw(region, x, y, width, height, Color.WHITE); + } + + public void draw(TextureRegion region, float x, float y, float width, float height, Color tint) { + Sprite sprite = nextUsable(); + sprite.setRegion(region); + sprite.setColor(tint); + sprite.setSize(width, height); + sprite.setPosition(x, y); + } + + public void draw(BitmapFont font, float x, float y, CharSequence str) { + draw(font, x, y, str, Color.WHITE); + } + + public void draw(BitmapFont font, float x, float y, CharSequence str, Color tint) { + BitmapFont.BitmapFontData fontData = font.getData(); + Texture fontTexture = font.getRegion().getTexture(); + + float currentX = x; + float currentY = y; + + for (int i = 0; i < str.length(); ++i) { + char c = str.charAt(i); + + // multiline support + if (c == '\r') + continue; // can't render this anyway, and likely a '\n' is right behind ... + if (c == '\n') { + currentY -= fontData.lineHeight; + currentX = x; + continue; + } + + BitmapFont.Glyph glyph = fontData.getGlyph(c); + if (glyph == null) { + // TODO: maybe rendering some special char here instead would be better? + currentX += fontData.spaceWidth; + continue; + } + + draw(fontTexture, currentX + glyph.xoffset, currentY + glyph.yoffset, glyph.srcX, glyph.srcY, glyph.width, glyph.height, tint); + + currentX += glyph.xadvance; + } + } + + public void flush() { + if (!hasBegun) + throw new IllegalStateException("Cannot call outside of a begin/end block."); + + spriteBatch.setColor(Color.WHITE); + spriteBatch.begin(); + for (int i = 0; i < pointer; ++i) { + Sprite sprite = sprites.items[i]; + sprite.draw(spriteBatch); + sprite.setTexture(null); // don't keep references! + } + spriteBatch.end(); + + pointer = 0; + } + + public void end() { + if (!hasBegun) + throw new IllegalStateException("Must call begin() first."); + + flush(); + + hasBegun = false; + spriteBatch = null; // don't need to hold on to this particular reference anymore + } + + private void increaseCapacity() { + int newCapacity = sprites.items.length + CAPACITY_INCREMEMT; + sprites.ensureCapacity(newCapacity); + + for (int i = 0; i < CAPACITY_INCREMEMT; ++i) + sprites.add(new Sprite()); + } + + private int getRemainingSpace() { + return sprites.size - pointer; + } + + private Sprite nextUsable() { + if (getRemainingSpace() == 0) + increaseCapacity(); + + Sprite usable = sprites.items[pointer]; + pointer++; + return usable; + } +} diff --git a/src/com/blarg/gdx/graphics/NoScaleScreenPixelScaler.java b/src/com/blarg/gdx/graphics/NoScaleScreenPixelScaler.java new file mode 100644 index 0000000..35d6bf0 --- /dev/null +++ b/src/com/blarg/gdx/graphics/NoScaleScreenPixelScaler.java @@ -0,0 +1,25 @@ +package com.blarg.gdx.graphics; + +import com.badlogic.gdx.Gdx; + +public class NoScaleScreenPixelScaler implements ScreenPixelScaler { + @Override + public int getScale() { + return 1; + } + + @Override + public int getScaledWidth() { + return Gdx.graphics.getWidth(); + } + + @Override + public int getScaledHeight() { + return Gdx.graphics.getHeight(); + } + + @Override + public void calculateScale(int screenWidth, int screenHeight) { + // nothing! + } +} diff --git a/src/com/blarg/gdx/graphics/RenderContext.java b/src/com/blarg/gdx/graphics/RenderContext.java new file mode 100644 index 0000000..01704db --- /dev/null +++ b/src/com/blarg/gdx/graphics/RenderContext.java @@ -0,0 +1,107 @@ +package com.blarg.gdx.graphics; + +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.PerspectiveCamera; +import com.badlogic.gdx.graphics.g2d.SpriteBatch; +import com.badlogic.gdx.graphics.g3d.ModelBatch; +import com.badlogic.gdx.graphics.glutils.ShapeRenderer; +import com.badlogic.gdx.utils.Disposable; + +public class RenderContext implements Disposable { + public final SpriteBatch spriteBatch; + public final DelayedSpriteBatch delayedSpriteBatch; + public final ShapeRenderer debugGeometryRenderer; + public final ModelBatch modelBatch; + public final ScreenPixelScaler pixelScaler; + public final SolidColorTextureCache solidColorTextures; + + Camera perspectiveCamera; + OrthographicCamera orthographicCamera; + + public RenderContext(boolean use2dPixelScaling) { + Gdx.app.debug("RenderContext", "ctor"); + spriteBatch = new SpriteBatch(); + delayedSpriteBatch = new DelayedSpriteBatch(); + debugGeometryRenderer = new ShapeRenderer(); + modelBatch = new ModelBatch(); + solidColorTextures = new SolidColorTextureCache(); + + if (use2dPixelScaling) + pixelScaler = new DefaultScreenPixelScaler(); + else + pixelScaler = new NoScaleScreenPixelScaler(); + pixelScaler.calculateScale(Gdx.graphics.getWidth(), Gdx.graphics.getHeight()); + orthographicCamera = new OrthographicCamera(pixelScaler.getScaledWidth(), pixelScaler.getScaledHeight()); + + setDefaultPerspectiveCamera(); + } + + public Camera getPerspectiveCamera() { + return perspectiveCamera; + } + + public OrthographicCamera getOrthographicCamera() { + return orthographicCamera; + } + + public void setPerspectiveCamera(Camera camera) { + perspectiveCamera = camera; + } + + public void setDefaultPerspectiveCamera() { + perspectiveCamera = new PerspectiveCamera(60.0f, Gdx.graphics.getWidth(), Gdx.graphics.getHeight()); + } + + public void clear() { + clear(0.0f, 0.0f, 0.0f, 1.0f); + } + + public void clear(float red, float green, float blue, float alpha) { + Gdx.graphics.getGL20().glClearColor(red, green, blue, alpha); + Gdx.graphics.getGL20().glClear(GL20.GL_COLOR_BUFFER_BIT | GL20.GL_DEPTH_BUFFER_BIT); + } + + public void onPreRender() { + spriteBatch.setProjectionMatrix(orthographicCamera.combined); + debugGeometryRenderer.begin(ShapeRenderer.ShapeType.Line); + delayedSpriteBatch.begin(spriteBatch); + modelBatch.begin(perspectiveCamera); + } + + public void onPostRender() { + modelBatch.end(); + delayedSpriteBatch.end(); + debugGeometryRenderer.end(); + } + + public void onUpdate(float delta) { + perspectiveCamera.update(); + orthographicCamera.update(); + } + + public void onResize(int width, int height) { + Gdx.app.debug("RenderContext", String.format("onResize(%d, %d)", width, height)); + pixelScaler.calculateScale(width, height); + orthographicCamera.setToOrtho(false, pixelScaler.getScaledWidth(), pixelScaler.getScaledHeight()); + } + + public void onPause() { + Gdx.app.debug("RenderContext", String.format("onPause")); + solidColorTextures.onPause(); + } + + public void onResume() { + Gdx.app.debug("RenderContext", String.format("onResume")); + solidColorTextures.onResume(); + } + + @Override + public void dispose() { + Gdx.app.debug("RenderContext", String.format("dispose")); + solidColorTextures.dispose(); + spriteBatch.dispose(); + } +} diff --git a/src/com/blarg/gdx/graphics/ScreenPixelScaler.java b/src/com/blarg/gdx/graphics/ScreenPixelScaler.java new file mode 100644 index 0000000..6a52a44 --- /dev/null +++ b/src/com/blarg/gdx/graphics/ScreenPixelScaler.java @@ -0,0 +1,10 @@ +package com.blarg.gdx.graphics; + +public interface ScreenPixelScaler { + int getScale(); + + int getScaledWidth(); + int getScaledHeight(); + + void calculateScale(int screenWidth, int screenHeight); +} diff --git a/src/com/blarg/gdx/graphics/SolidColorTextureCache.java b/src/com/blarg/gdx/graphics/SolidColorTextureCache.java new file mode 100644 index 0000000..217aad2 --- /dev/null +++ b/src/com/blarg/gdx/graphics/SolidColorTextureCache.java @@ -0,0 +1,57 @@ +package com.blarg.gdx.graphics; + +import com.badlogic.gdx.graphics.Color; +import com.badlogic.gdx.graphics.Pixmap; +import com.badlogic.gdx.graphics.Texture; +import com.badlogic.gdx.utils.Disposable; +import com.badlogic.gdx.utils.ObjectMap; + +public class SolidColorTextureCache implements Disposable +{ + ObjectMap cache; + + public SolidColorTextureCache() { + cache = new ObjectMap(); + } + + public void onResume() { + } + + public void onPause() { + } + + public Texture get(Color color) { + return get(Color.rgba8888(color)); + } + + public Texture get(float r, float g, float b, float a) { + return get(Color.rgba8888(r, g, b, a)); + } + + public Texture get(int color) { + Texture tex = cache.get(color); + if (tex == null) { + tex = create(color); + cache.put(color, tex); + } + + return tex; + } + + private Texture create(int color) { + Pixmap pixmap = new Pixmap(8, 8, Pixmap.Format.RGBA8888); + pixmap.setColor(color); + pixmap.fill(); + + Texture result = new Texture(pixmap); + return result; + } + + public void dispose() { + for (ObjectMap.Entries i = cache.entries(); i.hasNext(); ) { + ObjectMap.Entry entry = i.next(); + entry.value.dispose(); + } + cache.clear(); + } +} diff --git a/src/com/blarg/gdx/graphics/TTF.java b/src/com/blarg/gdx/graphics/TTF.java new file mode 100644 index 0000000..b8f9ba8 --- /dev/null +++ b/src/com/blarg/gdx/graphics/TTF.java @@ -0,0 +1,16 @@ +package com.blarg.gdx.graphics; + +import com.badlogic.gdx.Gdx; +import com.badlogic.gdx.graphics.g2d.BitmapFont; +import com.badlogic.gdx.graphics.g2d.freetype.FreeTypeFontGenerator; + +public final class TTF +{ + public static BitmapFont make(String file, int size) { + FreeTypeFontGenerator generator = new FreeTypeFontGenerator(Gdx.files.internal(file)); + BitmapFont result = generator.generateFont(size); + generator.dispose(); + + return result; + } +} diff --git a/src/com/blarg/gdx/graphics/screeneffects/DimScreenEffect.java b/src/com/blarg/gdx/graphics/screeneffects/DimScreenEffect.java new file mode 100644 index 0000000..739a421 --- /dev/null +++ b/src/com/blarg/gdx/graphics/screeneffects/DimScreenEffect.java @@ -0,0 +1,38 @@ +package com.blarg.gdx.graphics.screeneffects; + +import com.badlogic.gdx.graphics.Color; +import com.badlogic.gdx.graphics.Texture; +import com.blarg.gdx.graphics.RenderContext; + +public class DimScreenEffect extends ScreenEffect +{ + public static final Color DEFAULT_DIM_COLOR = Color.BLACK; + public static final float DEFAULT_DIM_ALPHA = 0.5f; + + public final Color color; + public float alpha; + + Color renderColor; + + public DimScreenEffect() + { + color = new Color(DEFAULT_DIM_COLOR); + alpha = DEFAULT_DIM_ALPHA; + + renderColor = new Color(color); + } + + @Override + public void onRender(float delta, RenderContext renderContext) + { + renderColor.set(color); + renderColor.a = alpha; + Texture texture = renderContext.solidColorTextures.get(color); + + renderContext.delayedSpriteBatch.draw( + texture, + 0, 0, + renderContext.pixelScaler.getScaledWidth(), renderContext.pixelScaler.getScaledHeight(), + renderColor); + } +} diff --git a/src/com/blarg/gdx/graphics/screeneffects/EffectInfo.java b/src/com/blarg/gdx/graphics/screeneffects/EffectInfo.java new file mode 100644 index 0000000..716fdfe --- /dev/null +++ b/src/com/blarg/gdx/graphics/screeneffects/EffectInfo.java @@ -0,0 +1,15 @@ +package com.blarg.gdx.graphics.screeneffects; + +class EffectInfo +{ + public final ScreenEffect effect; + public boolean isLocal; + + public EffectInfo(ScreenEffect effect, boolean isLocal) { + if (effect == null) + throw new IllegalArgumentException("effect can not be null."); + + this.effect = effect; + this.isLocal = isLocal; + } +} diff --git a/src/com/blarg/gdx/graphics/screeneffects/FadeScreenEffect.java b/src/com/blarg/gdx/graphics/screeneffects/FadeScreenEffect.java new file mode 100644 index 0000000..69b7cd6 --- /dev/null +++ b/src/com/blarg/gdx/graphics/screeneffects/FadeScreenEffect.java @@ -0,0 +1,89 @@ +package com.blarg.gdx.graphics.screeneffects; + +import com.badlogic.gdx.graphics.Color; +import com.badlogic.gdx.graphics.Texture; +import com.blarg.gdx.graphics.RenderContext; + +public class FadeScreenEffect extends ScreenEffect +{ + public static final float DEFAULT_FADE_SPEED = 3.0f; + + float fadeSpeed; + boolean isFadingOut; + float alpha; + Color color; + float fadeToAlpha; + boolean isDoneFading; + + public FadeScreenEffect() { + color = new Color(); + } + + public boolean isDoneFading() { + return isDoneFading; + } + + public void fadeOut(float toAlpha, Color color) { + fadeOut(alpha, color, DEFAULT_FADE_SPEED); + } + + public void fadeOut(float toAlpha, Color color, float speed) { + if (toAlpha < 0.0f || toAlpha > 1.0f) + throw new IllegalArgumentException("toAlpha needs to be between 0.0 and 1.0"); + + color.set(color); + fadeSpeed = speed; + isFadingOut = true; + alpha = 0.0f; + fadeToAlpha = toAlpha; + } + + public void fadeIn(float toAlpha, Color color) { + fadeIn(alpha, color, DEFAULT_FADE_SPEED); + } + + public void fadeIn(float toAlpha, Color color, float speed) { + if (toAlpha < 0.0f || toAlpha > 1.0f) + throw new IllegalArgumentException("toAlpha needs to be between 0.0 and 1.0"); + + color.set(color); + fadeSpeed = speed; + isFadingOut = false; + alpha = 1.0f; + fadeToAlpha = toAlpha; + } + + @Override + public void onRender(float delta, RenderContext renderContext) + { + Texture texture = renderContext.solidColorTextures.get(Color.WHITE); + color.a = alpha; + + renderContext.delayedSpriteBatch.draw( + texture, + 0, 0, + renderContext.pixelScaler.getScaledWidth(), renderContext.pixelScaler.getScaledHeight(), + color); + } + + @Override + public void onUpdate(float delta) + { + if (isDoneFading) + return; + + if (isFadingOut) { + alpha += (delta + fadeSpeed); + if (alpha >= fadeToAlpha) { + alpha = fadeToAlpha; + isDoneFading = true; + } + } else { + alpha -= (delta + fadeSpeed); + if (alpha < fadeToAlpha) { + alpha = fadeToAlpha; + isDoneFading = true; + } + } + } +} diff --git a/src/com/blarg/gdx/graphics/screeneffects/FlashScreenEffect.java b/src/com/blarg/gdx/graphics/screeneffects/FlashScreenEffect.java new file mode 100644 index 0000000..53f00c7 --- /dev/null +++ b/src/com/blarg/gdx/graphics/screeneffects/FlashScreenEffect.java @@ -0,0 +1,63 @@ +package com.blarg.gdx.graphics.screeneffects; + +import com.badlogic.gdx.graphics.Color; +import com.badlogic.gdx.graphics.Texture; +import com.blarg.gdx.graphics.RenderContext; + +public class FlashScreenEffect extends ScreenEffect +{ + public static final float DEFAULT_FLASH_SPEED = 16.0f; + public static final float DEFAULT_MAX_INTENSITY = 1.0f; + + public float flashInSpeed; + public float flashOutSpeed; + public float maximumIntensity; + public final Color color; + + boolean isFlashingIn; + float alpha; + + public float getAlpha() { + return alpha; + } + + public FlashScreenEffect() { + isFlashingIn = true; + flashInSpeed = DEFAULT_FLASH_SPEED; + flashOutSpeed = DEFAULT_FLASH_SPEED; + maximumIntensity = DEFAULT_MAX_INTENSITY; + color = new Color(Color.WHITE); + } + + @Override + public void onRender(float delta, RenderContext renderContext) + { + Texture texture = renderContext.solidColorTextures.get(Color.WHITE); + color.a = alpha; + + renderContext.delayedSpriteBatch.draw( + texture, + 0, 0, + renderContext.pixelScaler.getScaledWidth(), renderContext.pixelScaler.getScaledHeight(), + color); + } + + @Override + public void onUpdate(float delta) + { + if (isFlashingIn) { + alpha += (delta * flashInSpeed); + if (alpha >= maximumIntensity) { + alpha = maximumIntensity; + isFlashingIn = false; + } + } else { + alpha -= (delta * flashOutSpeed); + if (alpha < 0.0f) + alpha = 0.0f; + } + + if (alpha == 0.0f && isFlashingIn == false) + isActive = false; + } +} diff --git a/src/com/blarg/gdx/graphics/screeneffects/ScreenEffect.java b/src/com/blarg/gdx/graphics/screeneffects/ScreenEffect.java new file mode 100644 index 0000000..81503da --- /dev/null +++ b/src/com/blarg/gdx/graphics/screeneffects/ScreenEffect.java @@ -0,0 +1,37 @@ +package com.blarg.gdx.graphics.screeneffects; + +import com.badlogic.gdx.utils.Disposable; +import com.blarg.gdx.graphics.RenderContext; + +public abstract class ScreenEffect implements Disposable +{ + public boolean isActive; + + public ScreenEffect() { + isActive = true; + } + + public void onAdd() { + } + + public void onRemove() { + } + + public void onAppPause() { + } + + public void onAppResume() { + } + + public void onResize() { + } + + public void onRender(float delta, RenderContext renderContext) { + } + + public void onUpdate(float delta) { + } + + public void dispose() { + } +} diff --git a/src/com/blarg/gdx/graphics/screeneffects/ScreenEffectManager.java b/src/com/blarg/gdx/graphics/screeneffects/ScreenEffectManager.java new file mode 100644 index 0000000..4347841 --- /dev/null +++ b/src/com/blarg/gdx/graphics/screeneffects/ScreenEffectManager.java @@ -0,0 +1,157 @@ +package com.blarg.gdx.graphics.screeneffects; + +import com.badlogic.gdx.utils.Disposable; +import com.blarg.gdx.graphics.RenderContext; + +import java.util.LinkedList; + +public class ScreenEffectManager implements Disposable +{ + LinkedList effects; + int numLocalEffects; + int numGlobalEffects; + + public ScreenEffectManager() { + effects = new LinkedList(); + numLocalEffects = 0; + numGlobalEffects = 0; + } + + /*** Get / Add / Remove ***/ + + public T add(Class effectType) { + return add(effectType, true); + } + + public T add(Class effectType, boolean isLocalEffect) { + int existingIndex = getIndexFor(effectType); + if (existingIndex != -1) + throw new UnsupportedOperationException("Cannot add an effect of the same type as an existing effect already being managed."); + + T newEffect; + try { + newEffect = effectType.newInstance(); + } catch (Exception e) { + return null; + } + + EffectInfo effectInfo = new EffectInfo(newEffect, isLocalEffect); + add(effectInfo); + return newEffect; + } + + public T get(Class effectType) { + int index = getIndexFor(effectType); + if (index == -1) + return null; + else + return effectType.cast(effects.get(index).effect); + } + + public void remove(Class effectType) { + int existingIndex = getIndexFor(effectType); + if (existingIndex != -1) + remove(existingIndex); + } + + public void removeAll() { + while (effects.size() > 0) + remove(0); + } + + private void add(EffectInfo newEffectInfo) { + if (newEffectInfo == null) + throw new IllegalArgumentException("newEffectInfo cannot be null."); + + effects.add(newEffectInfo); + newEffectInfo.effect.onAdd(); + + if (newEffectInfo.isLocal) + ++numLocalEffects; + else + ++numGlobalEffects; + } + + private void remove(int index) { + if (index < 0 || index >= effects.size()) + throw new IllegalArgumentException("Invalid effect index."); + + EffectInfo effectInfo = effects.get(index); + if (effectInfo.isLocal) + --numLocalEffects; + else + --numGlobalEffects; + + effectInfo.effect.onRemove(); + effectInfo.effect.dispose(); + effects.remove(index); + } + + /*** events ***/ + + public void onAppPause() { + for (int i = 0; i < effects.size(); ++i) + effects.get(i).effect.onAppPause(); + } + + public void onAppResume() { + for (int i = 0; i < effects.size(); ++i) + effects.get(i).effect.onAppResume(); + } + + public void onResize() { + for (int i = 0; i < effects.size(); ++i) + effects.get(i).effect.onResize(); + } + + public void onRenderLocal(float delta, RenderContext renderContext) { + if (numLocalEffects == 0) + return; + + for (int i = 0; i < effects.size(); ++i) { + EffectInfo effectInfo = effects.get(i); + if (effectInfo.isLocal) + effectInfo.effect.onRender(delta, renderContext); + } + } + + public void onRenderGlobal(float delta, RenderContext renderContext) { + if (numGlobalEffects == 0) + return; + + for (int i = 0; i < effects.size(); ++i) { + EffectInfo effectInfo = effects.get(i); + if (!effectInfo.isLocal) + effectInfo.effect.onRender(delta, renderContext); + } + } + + public void onUpdate(float delta) { + int i = 0; + while (i < effects.size()) { + EffectInfo effectInfo = effects.get(i); + if (!effectInfo.effect.isActive) { + // index doesn't change, we're removing one, so next index now equals this index + remove(i); + } else { + effectInfo.effect.onUpdate(delta); + ++i; + } + } + } + + /*** misc ***/ + + private int getIndexFor(Class effectType) { + for (int i = 0; i < effects.size(); ++i) { + if (effects.get(i).effect.getClass() == effectType) + return i; + } + return -1; + } + + /*** cleanup ***/ + + public void dispose() { + } +} diff --git a/src/com/blarg/gdx/processes/GameProcess.java b/src/com/blarg/gdx/processes/GameProcess.java new file mode 100644 index 0000000..313b940 --- /dev/null +++ b/src/com/blarg/gdx/processes/GameProcess.java @@ -0,0 +1,76 @@ +package com.blarg.gdx.processes; + +import com.badlogic.gdx.utils.Disposable; +import com.blarg.gdx.GameApp; +import com.blarg.gdx.events.Event; +import com.blarg.gdx.events.EventHandler; +import com.blarg.gdx.events.EventManager; +import com.blarg.gdx.graphics.RenderContext; +import com.blarg.gdx.states.GameState; + +public abstract class GameProcess extends EventHandler implements Disposable { + public final ProcessManager processManager; + public final GameApp gameApp; + public final GameState gameState; + + boolean isFinished; + + public GameProcess(ProcessManager processManager, EventManager eventManager) { + super(eventManager); + + if (processManager == null) + throw new IllegalArgumentException("processManager cannot be null."); + + this.processManager = processManager; + gameState = this.processManager.gameState; + gameApp = gameState.gameApp; + } + + public boolean isFinished() { + return isFinished; + } + + public void setFinished() { + isFinished = true; + } + + public void onAdd() { + } + + public void onRemove() { + } + + public void onPause(boolean dueToOverlay) { + } + + public void onResume(boolean fromOverlay) { + } + + public void onAppPause() { + } + + public void onAppResume() { + } + + public void onResize() { + } + + public void onRender(float delta, RenderContext renderContext) { + } + + public void onUpdate(float delta) { + } + + public boolean onTransition(float delta, boolean isTransitioningOut, boolean started) { + return true; + } + + @Override + public boolean handle(Event e) + { + return false; + } + + public void dispose() { + } +} diff --git a/src/com/blarg/gdx/processes/ProcessInfo.java b/src/com/blarg/gdx/processes/ProcessInfo.java new file mode 100644 index 0000000..200e67f --- /dev/null +++ b/src/com/blarg/gdx/processes/ProcessInfo.java @@ -0,0 +1,35 @@ +package com.blarg.gdx.processes; + +import com.blarg.gdx.Strings; + +class ProcessInfo { + public final GameProcess process; + public final String name; + + public boolean isTransitioning; + public boolean isTransitioningOut; + public boolean isTransitionStarting; + public boolean isInactive; + public boolean isBeingRemoved; + + String descriptor; + + public ProcessInfo(GameProcess process, String name) { + if (process == null) + throw new IllegalArgumentException("process cannot be null."); + + this.process = process; + this.name = name; + isInactive = true; + + if (Strings.isNullOrEmpty(this.name)) + descriptor = this.process.getClass().getSimpleName(); + else + descriptor = String.format("%s[%s]", this.process.getClass().getSimpleName(), this.name); + } + + @Override + public String toString() { + return descriptor; + } +} diff --git a/src/com/blarg/gdx/processes/ProcessManager.java b/src/com/blarg/gdx/processes/ProcessManager.java new file mode 100644 index 0000000..4cebbae --- /dev/null +++ b/src/com/blarg/gdx/processes/ProcessManager.java @@ -0,0 +1,373 @@ +package com.blarg.gdx.processes; + +import com.badlogic.gdx.Gdx; +import com.badlogic.gdx.utils.Disposable; +import com.blarg.gdx.Strings; +import com.blarg.gdx.events.EventManager; +import com.blarg.gdx.graphics.RenderContext; +import com.blarg.gdx.states.GameState; +import com.blarg.gdx.ReflectionUtils; + +import java.util.LinkedList; + +public class ProcessManager implements Disposable { + public final GameState gameState; + + LinkedList processes; + LinkedList queue; + + String descriptor; + + public ProcessManager(GameState gameState) { + if (gameState == null) + throw new IllegalArgumentException("gameState cannot be null."); + + this.gameState = gameState; + this.descriptor = String.format("[%s]", this.gameState.getClass().getSimpleName()); + + Gdx.app.debug("ProcessManager", String.format("%s ctor", descriptor)); + + processes = new LinkedList(); + queue = new LinkedList(); + } + + /*** public process getters ***/ + + public boolean isTransitioning() { + for (int i = 0; i < processes.size(); ++i) { + if (processes.get(i).isTransitioning) + return true; + } + return false; + } + + public boolean isEmpty() { + return (processes.size() == 0 && queue.size() == 0); + } + + public boolean isProcessTransitioning(GameProcess process) { + ProcessInfo processInfo = getProcessInfoFor(process); + return (processInfo == null ? false : processInfo.isTransitioning); + } + + public boolean hasProcess(String name) { + for (int i = 0; i < processes.size(); ++i) { + ProcessInfo info = processes.get(i); + if (!Strings.isNullOrEmpty(info.name) && info.name.equals(name)) + return true; + } + return false; + } + + /** Add / Remove ***/ + + public T add(Class processType) { + return add(processType, null); + } + + public T add(Class processType, String name) { + T newProcess; + try { + newProcess = ReflectionUtils.instantiateObject(processType, + new Class[] { ProcessManager.class, EventManager.class }, + new Object[] { this, gameState.eventManager }); + } catch (Exception e) { + throw new IllegalArgumentException("Could not instantiate a GameProcess instance of that type."); + } + + ProcessInfo processInfo = new ProcessInfo(newProcess, name); + queue(processInfo); + return newProcess; + } + + public void remove(String name) { + int i = getIndexOf(name); + if (i == -1) + throw new IllegalArgumentException("No process with that name."); + startTransitionOut(processes.get(i), true); + } + + public void removeFirstOf(Class processType) { + int i = getIndexForFirstOfType(processType); + if (i == -1) + throw new IllegalArgumentException("No processes of that type."); + startTransitionOut(processes.get(i), true); + } + + public void removeAll() { + Gdx.app.debug("ProcessManager", String.format("%s Transitioning out all processes pending removal.", descriptor)); + for (int i = 0; i < processes.size(); ++i) { + ProcessInfo processInfo = processes.get(i); + if (!processInfo.isTransitioning && !processInfo.isInactive) + startTransitionOut(processInfo, true); + } + } + + private void queue(ProcessInfo newProcessInfo) { + if (newProcessInfo == null) + throw new IllegalArgumentException("newProcessInfo cannot be null."); + if (newProcessInfo.process == null) + throw new IllegalArgumentException("New ProcessInfo object has null GameProcess."); + + Gdx.app.debug("ProcessManager", String.format("%s Queueing process %s.", descriptor, newProcessInfo)); + queue.add(newProcessInfo); + } + + /*** events ***/ + + public void onPause(boolean dueToOverlay) { + if (processes.size() == 0) + return; + + if (dueToOverlay) { + Gdx.app.debug("ProcessManager", String.format("%s Pausing all active processes due to state being overlayed on to the parent state.", descriptor)); + for (int i = 0; i < processes.size(); ++i) { + ProcessInfo processInfo = processes.get(i); + if (!processInfo.isInactive) { + Gdx.app.debug("ProcessManager", String.format("%s Pausing process %s due to parent state overlay.", descriptor, processInfo)); + processInfo.process.onPause(true); + } + } + } else { + Gdx.app.debug("ProcessManager", String.format("%s Transitioning out all active processes pending pause.", descriptor)); + for (int i = 0; i < processes.size(); ++i) { + ProcessInfo processInfo = processes.get(i); + if (!processInfo.isInactive) + startTransitionOut(processInfo, false); + } + } + } + + public void onResume(boolean fromOverlay) { + if (processes.size() == 0) + return; + + if (fromOverlay) { + Gdx.app.debug("ProcessManager", String.format("%s Resuming all active processes due to overlay state being removed from overtop of parent state.", descriptor)); + for (int i = 0; i < processes.size(); ++i) { + ProcessInfo processInfo = processes.get(i); + if (!processInfo.isInactive) { + Gdx.app.debug("ProcessManager", String.format("%s Resuming process %s due to overlay state removal.", descriptor, processInfo)); + processInfo.process.onResume(true); + } + } + } else { + Gdx.app.debug("ProcessManager", String.format("%s Resuming processes.", descriptor)); + for (int i = 0; i < processes.size(); ++i) { + ProcessInfo processInfo = processes.get(i); + if (processInfo.isInactive && !processInfo.isBeingRemoved) { + Gdx.app.debug("ProcessManager", String.format("%s Resuming process %s.", descriptor, processInfo)); + processInfo.process.onResume(true); + + startTransitionIn(processInfo); + } + } + } + } + + public void onAppPause() { + for (int i = 0; i < processes.size(); ++i) { + ProcessInfo processInfo = processes.get(i); + if (!processInfo.isInactive) + processInfo.process.onAppPause(); + } + } + + public void onAppResume() { + for (int i = 0; i < processes.size(); ++i) { + ProcessInfo processInfo = processes.get(i); + if (!processInfo.isInactive) + processInfo.process.onAppResume(); + } + } + + public void onResize() { + for (int i = 0; i < processes.size(); ++i) { + ProcessInfo processInfo = processes.get(i); + if (!processInfo.isInactive) + processInfo.process.onResize(); + } + } + + public void onRender(float delta, RenderContext renderContext) { + for (int i = 0; i < processes.size(); ++i) { + ProcessInfo processInfo = processes.get(i); + if (!processInfo.isInactive) + processInfo.process.onRender(delta, renderContext); + } + } + + public void onUpdate(float delta) { + cleanupInactiveProcesses(); + checkForFinishedProcesses(); + processQueue(); + updateTransitions(delta); + + for (int i = 0; i < processes.size(); ++i) { + ProcessInfo processInfo = processes.get(i); + if (!processInfo.isInactive) + processInfo.process.onUpdate(delta); + } + } + + /*** internal process management functions ***/ + + private void startTransitionIn(ProcessInfo processInfo) { + if (processInfo == null) + throw new IllegalArgumentException("processInfo cannot be null."); + if (!processInfo.isInactive || processInfo.isTransitioning) + throw new UnsupportedOperationException(); + + processInfo.isInactive = false; + processInfo.isTransitioning = true; + processInfo.isTransitioningOut = false; + processInfo.isTransitionStarting = true; + Gdx.app.debug("ProcessManager", String.format("%s Transition into process %s started.", descriptor, processInfo)); + } + + private void startTransitionOut(ProcessInfo processInfo, boolean forRemoval) { + if (processInfo == null) + throw new IllegalArgumentException("processInfo cannot be null."); + if (!processInfo.isInactive || processInfo.isTransitioning) + throw new UnsupportedOperationException(); + + processInfo.isTransitioning = true; + processInfo.isTransitioningOut = true; + processInfo.isTransitionStarting = true; + processInfo.isBeingRemoved = forRemoval; + Gdx.app.debug("ProcessManager", String.format("%s Transition out of process %s started pending %s.", descriptor, processInfo, (forRemoval ? "removal" : "pause"))); + } + + private void cleanupInactiveProcesses() { + int i = 0; + while (i < processes.size()) { + ProcessInfo processInfo = processes.get(i); + if (processInfo.isInactive && processInfo.isBeingRemoved) { + // remove this process and move to the next node + // (index doesn't change, we're removing one, so next index now equals this index) + processes.remove(i); + + Gdx.app.debug("ProcessManager", String.format("%s Deleting inactive process %s.", descriptor, processInfo)); + processInfo.process.dispose(); + + } else { + i++; + } + } + } + + private void checkForFinishedProcesses() { + for (int i = 0; i < processes.size(); ++i) { + ProcessInfo processInfo = processes.get(i); + if (!processInfo.isInactive && processInfo.process.isFinished() && !processInfo.isTransitioning) { + Gdx.app.debug("ProcessManager", String.format("%s Process %s marked as finished.", descriptor, processInfo)); + startTransitionOut(processInfo, true); + } + } + } + + private void processQueue() { + while (queue.size() > 0) { + ProcessInfo processInfo = queue.removeFirst(); + + Gdx.app.debug("ProcessManager", String.format("%s Adding process %s from queue.", descriptor, processInfo)); + processes.add(processInfo); + processInfo.process.onAdd(); + + startTransitionIn(processInfo); + } + } + + private void updateTransitions(float delta) { + for (int i = 0; i < processes.size(); ++i) { + ProcessInfo processInfo = processes.get(i); + if (processInfo.isTransitioning) { + boolean isDone = processInfo.process.onTransition(delta, processInfo.isTransitioningOut, processInfo.isTransitionStarting); + if (isDone) { + Gdx.app.debug("ProcessManager", String.format("%s Transition %s into process %s finished.", + descriptor, + (processInfo.isTransitioningOut ? "out of" : "into"), + processInfo)); + + // if the process was being transitioned out, then we should mark it as + // inactive, and trigger it's onRemove() event now + if (processInfo.isTransitioningOut) { + if (processInfo.isBeingRemoved) { + Gdx.app.debug("ProcessManager", String.format("%s Removing process %s.", descriptor, processInfo)); + processInfo.process.onRemove(); + } else { + Gdx.app.debug("ProcessManager", String.format("%s Pausing process %s.", descriptor, processInfo)); + processInfo.process.onPause(false); + } + processInfo.isInactive = true; + } + + // done transitioning + processInfo.isTransitioning = false; + processInfo.isTransitioningOut = false; + } + processInfo.isTransitionStarting = false; + } + } + } + + /*** private process getters ***/ + + private int getIndexOf(String processName) { + if (Strings.isNullOrEmpty(processName)) + throw new IllegalArgumentException("processName should be specified."); + + for (int i = 0; i < processes.size(); ++i) { + ProcessInfo processInfo = processes.get(i); + if (!Strings.isNullOrEmpty(processInfo.name) && processInfo.name.equals(processName)) + return i; + } + return -1; + } + + private int getIndexForFirstOfType(Class processType) { + for (int i = 0; i < processes.size(); ++i) { + if (processes.get(i).process.getClass() == processType) + return i; + } + return -1; + } + + private ProcessInfo getProcessInfoFor(GameProcess process) { + if (process == null) + throw new IllegalArgumentException("process cannot be null."); + + for (int i = 0; i < processes.size(); ++i) { + if (processes.get(i).process == process) + return processes.get(i); + } + return null; + } + + /*** cleanup ***/ + + public void dispose() { + if (processes == null) + return; + + Gdx.app.debug("ProcessManager", String.format("%s dispose", descriptor)); + + while (processes.size() > 0) { + ProcessInfo processInfo = processes.getLast(); + Gdx.app.debug("ProcessManager", String.format("%s Removing process %s as part of ProcessManager shutdown.", descriptor, processInfo)); + processInfo.process.onRemove(); + processInfo.process.dispose(); + processes.removeLast(); + } + + // the queue will likely not have anything in it, but just in case ... + while (queue.size() > 0) { + ProcessInfo processInfo = processes.removeFirst(); + Gdx.app.debug("ProcessManager", String.format("%s Removing queued process %s as part of ProcessManager shutdown.", descriptor, processInfo)); + processInfo.process.dispose(); + } + + processes = null; + queue = null; + } +} diff --git a/src/com/blarg/gdx/states/GameState.java b/src/com/blarg/gdx/states/GameState.java new file mode 100644 index 0000000..ff7d18c --- /dev/null +++ b/src/com/blarg/gdx/states/GameState.java @@ -0,0 +1,105 @@ +package com.blarg.gdx.states; + +import com.badlogic.gdx.utils.Disposable; +import com.blarg.gdx.GameApp; +import com.blarg.gdx.events.Event; +import com.blarg.gdx.events.EventHandler; +import com.blarg.gdx.events.EventManager; +import com.blarg.gdx.graphics.RenderContext; +import com.blarg.gdx.graphics.screeneffects.ScreenEffectManager; +import com.blarg.gdx.processes.ProcessManager; + +public abstract class GameState extends EventHandler implements Disposable { + public final StateManager stateManager; + public final ScreenEffectManager effectManager; + public final ProcessManager processManager; + public final GameApp gameApp; + + boolean isFinished; + + public GameState(StateManager stateManager, EventManager eventManager) { + super(eventManager); + + if (stateManager == null) + throw new IllegalArgumentException("stateManager cannot be null."); + + this.stateManager = stateManager; + gameApp = stateManager.gameApp; + + effectManager = new ScreenEffectManager(); + processManager = new ProcessManager(this); + } + + public boolean isTransitioning() { + return stateManager.isStateTransitioning(this); + } + + public boolean isTopState() { + return stateManager.isTopState(this); + } + + public boolean isFinished() { + return isFinished; + } + + public void setFinished() { + isFinished = true; + } + + public void dispose() { + effectManager.dispose(); + processManager.dispose(); + } + + public void onPush() { + } + + public void onPop() { + } + + public void onPause(boolean dueToOverlay) { + processManager.onPause(dueToOverlay); + } + + public void onResume(boolean fromOverlay) { + processManager.onResume(fromOverlay); + } + + public void onAppPause() { + processManager.onAppPause(); + effectManager.onAppPause(); + } + + public void onAppResume() { + processManager.onAppResume(); + effectManager.onAppResume(); + } + + public void onResize() { + processManager.onResize(); + effectManager.onResize(); + } + + public void onRender(float delta, RenderContext renderContext) { + // switch it up and do effects before processes here so that processes + // (which would commonly be used for UI overlay elements) don't get + // overwritten by local effects (e.g. flashes, etc.) + effectManager.onRenderLocal(delta, renderContext); + processManager.onRender(delta, renderContext); + } + + public void onUpdate(float delta) { + effectManager.onUpdate(delta); + processManager.onUpdate(delta); + } + + public boolean onTransition(float delta, boolean isTransitioningOut, boolean started) { + return true; + } + + @Override + public boolean handle(Event e) + { + return false; + } +} diff --git a/src/com/blarg/gdx/states/StateInfo.java b/src/com/blarg/gdx/states/StateInfo.java new file mode 100644 index 0000000..46c43ad --- /dev/null +++ b/src/com/blarg/gdx/states/StateInfo.java @@ -0,0 +1,37 @@ +package com.blarg.gdx.states; + +import com.blarg.gdx.Strings; + +class StateInfo { + public final GameState state; + public final String name; + + public boolean isOverlay; + public boolean isOverlayed; + public boolean isTransitioning; + public boolean isTransitioningOut; + public boolean isTransitionStarting; + public boolean isInactive; + public boolean isBeingPopped; + + String descriptor; + + public StateInfo(GameState state, String name) { + if (state == null) + throw new IllegalArgumentException("state cannot be null."); + + this.state = state; + this.name = name; + isInactive = true; + + if (Strings.isNullOrEmpty(this.name)) + descriptor = this.state.getClass().getSimpleName(); + else + descriptor = String.format("%s[%s]", this.state.getClass().getSimpleName(), this.name); + } + + @Override + public String toString() { + return descriptor; + } +} diff --git a/src/com/blarg/gdx/states/StateManager.java b/src/com/blarg/gdx/states/StateManager.java new file mode 100644 index 0000000..dc1caf4 --- /dev/null +++ b/src/com/blarg/gdx/states/StateManager.java @@ -0,0 +1,608 @@ +package com.blarg.gdx.states; + +import com.badlogic.gdx.Gdx; +import com.badlogic.gdx.utils.Disposable; +import com.blarg.gdx.GameApp; +import com.blarg.gdx.Strings; +import com.blarg.gdx.events.EventManager; +import com.blarg.gdx.ReflectionUtils; +import com.blarg.gdx.graphics.RenderContext; + +import java.util.LinkedList; + +public class StateManager implements Disposable { + public final GameApp gameApp; + public final EventManager eventManager; + + LinkedList states; + LinkedList pushQueue; + LinkedList swapQueue; + + boolean pushQueueHasOverlay; + boolean swapQueueHasOverlay; + boolean lastCleanedStatesWereAllOverlays; + + public StateManager(GameApp gameApp, EventManager eventManager) { + if (gameApp == null) + throw new IllegalArgumentException("gameApp cannot be null."); + + Gdx.app.debug("StateManager", "ctor"); + + states = new LinkedList(); + pushQueue = new LinkedList(); + swapQueue = new LinkedList(); + + this.gameApp = gameApp; + this.eventManager = eventManager; + } + + /*** public state getters ***/ + + public GameState getTopState() { + StateInfo top = getTop(); + return (top == null ? null : top.state); + } + + public GameState getTopNonOverlayState() { + StateInfo top = getTopNonOverlay(); + return (top == null ? null : top.state); + } + + public boolean isTransitioning() { + for (int i = 0; i < states.size(); ++i) { + if (states.get(i).isTransitioning) + return true; + } + return false; + } + + public boolean isEmpty() { + return (states.size() == 0 && pushQueue.size() == 0 && swapQueue.size() == 0); + } + + public boolean isStateTransitioning(GameState state) { + if (state == null) + throw new IllegalArgumentException("state cannot be null."); + + StateInfo info = getStateInfoFor(state); + return (info == null ? false : info.isTransitioning); + } + + public boolean isTopState(GameState state) { + if (state == null) + throw new IllegalArgumentException("state cannot be null."); + + StateInfo info = getTop(); + return (info == null ? false : (info.state == state)); + } + + public boolean hasState(String name) { + for (int i = 0; i < states.size(); ++i) { + StateInfo info = states.get(i); + if (!Strings.isNullOrEmpty(info.name) && info.name.equals(name)) + return true; + } + return false; + } + + /** Push / Pop / Overlay / Swap / Queue ***/ + + public T push(Class stateType) { + return push(stateType, null); + } + + public T push(Class stateType, String name) { + T newState; + try { + newState = ReflectionUtils.instantiateObject(stateType, + new Class[] { StateManager.class, EventManager.class }, + new Object[] { this, eventManager }); + } catch (Exception e) { + throw new IllegalArgumentException("Could not instantiate a GameState instance of that type."); + } + + StateInfo stateInfo = new StateInfo(newState, name); + queueForPush(stateInfo); + return newState; + } + + public T overlay(Class stateType) { + return overlay(stateType); + } + + public T overlay(Class stateType, String name) { + T newState; + try { + newState = ReflectionUtils.instantiateObject(stateType, + new Class[] { StateManager.class, EventManager.class }, + new Object[] { this, eventManager }); + } catch (Exception e) { + throw new IllegalArgumentException("Could not instantiate a GameState instance of that type."); + } + + StateInfo stateInfo = new StateInfo(newState, name); + stateInfo.isOverlay = true; + queueForPush(stateInfo); + return newState; + } + + public T swapTopWith(Class stateType) { + return swapTopWith(stateType); + } + + public T swapTopWith(Class stateType, String name) { + // figure out if the current top state is an overlay or not. use that + // same setting for the new state that is being swapped in + StateInfo currentTopStateInfo = getTop(); + if (currentTopStateInfo == null) + throw new UnsupportedOperationException("Cannot swap, no existing states."); + boolean isOverlay = currentTopStateInfo.isOverlay; + + T newState; + try { + newState = ReflectionUtils.instantiateObject(stateType, + new Class[] { StateManager.class, EventManager.class }, + new Object[] { this, eventManager }); + } catch (Exception e) { + throw new IllegalArgumentException("Could not instantiate a GameState instance of that type."); + } + + StateInfo stateInfo = new StateInfo(newState, name); + stateInfo.isOverlay = isOverlay; + queueForSwap(stateInfo, false); + return newState; + } + + public T swapTopNonOverlayWith(Class stateType) { + return swapTopWith(stateType); + } + + public T swapTopNonOverlayWith(Class stateType, String name) { + T newState; + try { + newState = ReflectionUtils.instantiateObject(stateType, + new Class[] { StateManager.class, EventManager.class }, + new Object[] { this, eventManager }); + } catch (Exception e) { + throw new IllegalArgumentException("Could not instantiate a GameState instance of that type."); + } + + StateInfo stateInfo = new StateInfo(newState, name); + queueForSwap(stateInfo, true); + return newState; + } + + public void pop() { + if (isTransitioning()) + throw new UnsupportedOperationException(); + + Gdx.app.debug("StateManager", "Pop initiated for top-most state only."); + startOnlyTopStateTransitioningOut(false); + } + + public void popTopNonOverlay() { + if (isTransitioning()) + throw new UnsupportedOperationException(); + + Gdx.app.debug("StateManager", "Pop initiated for all top active states."); + startTopStatesTransitioningOut(false); + } + + private void queueForPush(StateInfo newStateInfo) { + if (newStateInfo == null) + throw new IllegalArgumentException("newStateInfo cannot be null."); + if (newStateInfo.state == null) + throw new IllegalArgumentException("New StateInfo object has null GameState."); + if (pushQueueHasOverlay && !newStateInfo.isOverlay) + throw new UnsupportedOperationException("Cannot queue new non-overlay state while queue is active with overlay states."); + + Gdx.app.debug("StateManager", String.format("Queueing state %s for pushing.", newStateInfo)); + + if (!newStateInfo.isOverlay) + startTopStatesTransitioningOut(true); + + pushQueue.add(newStateInfo); + + if (newStateInfo.isOverlay) + pushQueueHasOverlay = true; + } + + private void queueForSwap(StateInfo newStateInfo, boolean swapTopNonOverlay) { + if (newStateInfo == null) + throw new IllegalArgumentException("newStateInfo cannot be null."); + if (newStateInfo.state == null) + throw new IllegalArgumentException("New StateInfo object has null GameState."); + if (swapQueueHasOverlay && !newStateInfo.isOverlay) + throw new UnsupportedOperationException("Cannot queue new non-overlay state while queue is active with overlay states."); + + Gdx.app.debug("StateManager", String.format("Queueing state %s for swapping with %s.", newStateInfo, (swapTopNonOverlay ? "all top active states" : "only top-most active state."))); + + if (swapTopNonOverlay) + startTopStatesTransitioningOut(false); + else + startOnlyTopStateTransitioningOut(false); + + swapQueue.add(newStateInfo); + + if (newStateInfo.isOverlay) + swapQueueHasOverlay = true; + } + + /*** events ***/ + + public void onAppPause() { + for (int i = getTopNonOverlayIndex(); i != -1 && i < states.size(); ++i) { + StateInfo stateInfo = states.get(i); + if (!stateInfo.isInactive) + stateInfo.state.onAppPause(); + } + } + + public void onAppResume() { + for (int i = getTopNonOverlayIndex(); i != -1 && i < states.size(); ++i) { + StateInfo stateInfo = states.get(i); + if (!stateInfo.isInactive) + stateInfo.state.onAppResume(); + } + } + + public void onResize() { + for (int i = getTopNonOverlayIndex(); i != -1 && i < states.size(); ++i) { + StateInfo stateInfo = states.get(i); + if (!stateInfo.isInactive) + stateInfo.state.onResize(); + } + } + + public void onRender(float delta, RenderContext renderContext) { + for (int i = getTopNonOverlayIndex(); i != -1 && i < states.size(); ++i) { + StateInfo stateInfo = states.get(i); + if (!stateInfo.isInactive) { + stateInfo.state.onRender(delta, renderContext); + stateInfo.state.effectManager.onRenderGlobal(delta, renderContext); + } + } + } + + public void onUpdate(float delta) { + lastCleanedStatesWereAllOverlays = false; + + cleanupInactiveStates(); + checkForFinishedStates(); + processQueues(); + resumeStatesIfNeeded(); + updateTransitions(delta); + + for (int i = getTopNonOverlayIndex(); i != -1 && i < states.size(); ++i) { + StateInfo stateInfo = states.get(i); + if (!stateInfo.isInactive) + stateInfo.state.onUpdate(delta); + } + } + + /*** internal state management functions ***/ + + private void startTopStatesTransitioningOut(boolean pausing) { + int i = getTopNonOverlayIndex(); + if (i == -1) + return; + + for (; i < states.size(); ++i) { + // only look at active states, since inactive ones have already been + // transitioned out and will be removed on the next onUpdate() + if (!states.get(i).isInactive) + transitionOut(states.get(i), !pausing); + } + } + + private void startOnlyTopStateTransitioningOut(boolean pausing) { + StateInfo info = getTop(); + + // if it's not active, then it's just been transitioned out and will be + // removed on the next onUpdate() + if (!info.isInactive) + transitionOut(info, !pausing); + } + + private void cleanupInactiveStates() { + // we don't want to remove any states until everything is done transitioning. + // this is to avoid the scenario where the top non-overlay state finishes + // transitioning before one of the overlays. if we removed it, the overlays + // would then be overlayed over an inactive non-overlay (which wouldn't get + // resumed until the current active overlays were done being transitioned) + if (isTransitioning()) + return; + + boolean cleanedUpSomething = false; + boolean cleanedUpNonOverlay = false; + + int i = 0; + while (i < states.size()) { + StateInfo stateInfo = states.get(i); + if (stateInfo.isInactive && stateInfo.isBeingPopped) { + cleanedUpSomething = true; + if (!stateInfo.isOverlay) + cleanedUpNonOverlay = true; + + // remove this state and move to the next node + // (index doesn't change, we're removing one, so next index now equals this index) + states.remove(i); + + Gdx.app.debug("StateManager", String.format("Deleting inactive popped state %s.", stateInfo)); + stateInfo.state.dispose(); + + } else { + i++; + } + } + + if (cleanedUpSomething && !cleanedUpNonOverlay) + lastCleanedStatesWereAllOverlays = true; + } + + private void checkForFinishedStates() { + if (states.size() == 0) + return; + + // don't do anything if something is currently transitioning + if (isTransitioning()) + return; + + boolean needToAlsoTransitionOutOverlays = false; + + // check the top non-overlay state first to see if it's finished + // and should be transitioned out + StateInfo topNonOverlayStateInfo = getTopNonOverlay(); + if (!topNonOverlayStateInfo.isInactive && topNonOverlayStateInfo.state.isFinished()) { + Gdx.app.debug("StateManager", String.format("State %s marked as finished.", topNonOverlayStateInfo)); + transitionOut(topNonOverlayStateInfo, true); + + needToAlsoTransitionOutOverlays = true; + } + + // now also check the overlay states (if there were any). we force them to + // transition out if the non-overlay state started to transition out so that + // we don't end up with overlay states without a parent non-overlay state + + // start the loop off 1 beyond the top non-overlay (which is where the + // overlays are, if any) + int i = getTopNonOverlayIndex(); + if (i != -1) { + for (++i; i < states.size(); ++i) { + StateInfo stateInfo = states.get(i); + if (!stateInfo.isInactive && (stateInfo.state.isFinished() || needToAlsoTransitionOutOverlays)) { + Gdx.app.debug("StateManager", String.format("State %s marked as finished.", stateInfo)); + transitionOut(stateInfo, true); + } + } + } + } + + private void processQueues() { + // don't do anything if stuff is currently transitioning + if (isTransitioning()) + return; + + if (pushQueue.size() > 0 && swapQueue.size() > 0) + throw new UnsupportedOperationException("Cannot process queues when both the swap and push queues have items currently in them."); + + // for each state in the queu, add it to the main list and start transitioning it in + // (note, only one of these queues will be processed each tick due to the above check!) + + while (pushQueue.size() > 0) { + StateInfo stateInfo = pushQueue.removeFirst(); + + if (states.size() > 0) { + // if this new state is an overlay, and the current top state is both + // currently active and is not currently marked as being overlay-ed + // then we should pause it due to overlay + StateInfo currentTopStateInfo = getTop(); + if (stateInfo.isOverlay && !currentTopStateInfo.isInactive && !currentTopStateInfo.isOverlayed) { + Gdx.app.debug("StateManager", String.format("Pausing %sstate %s due to overlay.", (currentTopStateInfo.isOverlay ? "overlay " : ""), currentTopStateInfo)); + currentTopStateInfo.state.onPause(true); + + // also mark the current top state as being overlay-ed + currentTopStateInfo.isOverlayed = true; + } + } + + Gdx.app.debug("StateManager", String.format("Pushing %sstate %s from push-queue.", (stateInfo.isOverlay ? "overlay " : ""), stateInfo)); + stateInfo.state.onPush(); + + transitionIn(stateInfo, false); + + states.addLast(stateInfo); + } + + while (swapQueue.size() > 0) { + StateInfo stateInfo = swapQueue.removeFirst(); + + // if this new state is an overlay, and the current top state is both + // currently active and is not currently marked as being overlay-ed + // then we should pause it due to overlay + StateInfo currentTopStateInfo = getTop(); + if (stateInfo.isOverlay && !currentTopStateInfo.isInactive && !currentTopStateInfo.isOverlayed) { + Gdx.app.debug("StateManager", String.format("Pausing %sstate %s due to overlay.", (currentTopStateInfo.isOverlay ? "overlay " : ""), currentTopStateInfo)); + currentTopStateInfo.state.onPause(true); + + // also mark the current top state as being overlay-ed + currentTopStateInfo.isOverlayed = true; + } + + Gdx.app.debug("StateManager", String.format("Pushing %sstate %s from swap-queue.", (stateInfo.isOverlay ? "overlay " : ""), stateInfo)); + stateInfo.state.onPush(); + + transitionIn(stateInfo, false); + + states.addLast(stateInfo); + } + + pushQueueHasOverlay = false; + swapQueueHasOverlay = false; + } + + private void resumeStatesIfNeeded() { + if (states.size() == 0) + return; + + // don't do anything if stuff is currently transitioning + if (isTransitioning()) + return; + + // did we just clean up one or more overlay states? + if (lastCleanedStatesWereAllOverlays) { + // then we need to resume the current top state + // (those paused with the flag "from an overlay") + StateInfo stateInfo = getTop(); + if (stateInfo.isInactive || !stateInfo.isOverlayed) + throw new UnsupportedOperationException(); + + Gdx.app.debug("StateManager", String.format("Resuming %sstate %s due to overlay removal.", (stateInfo.isOverlay ? "overlay " : ""), stateInfo)); + stateInfo.state.onResume(true); + + stateInfo.isOverlayed = false; + + return; + } + + // if the top state is no inactive, then we don't need to resume anything + if (!getTop().isInactive) + return; + + Gdx.app.debug("StateManager", "Top-most state is inactive. Resuming all top states up to and including the next non-overlay."); + + // top state is inactive. time to reusme one or more states... + // find the topmost non-overlay state and take it and all overlay states that + // are above it, and transition them in + for (int i = getTopNonOverlayIndex(); i != -1 && i < states.size(); ++i) { + StateInfo stateInfo = states.get(i); + Gdx.app.debug("StateManager", String.format("Resuming %sstate %s.", (stateInfo.isOverlay ? "overlay " : ""), stateInfo)); + stateInfo.state.onResume(false); + + transitionIn(stateInfo, true); + } + } + + private void updateTransitions(float delta) { + for (int i = getTopNonOverlayIndex(); i != -1 && i < states.size(); ++i) { + StateInfo stateInfo = states.get(i); + if (stateInfo.isTransitioning) { + boolean isDone = stateInfo.state.onTransition(delta, stateInfo.isTransitioningOut, stateInfo.isTransitionStarting); + if (isDone) { + Gdx.app.debug("StateManager", String.format("Transition %s %sstate %s finished.", + (stateInfo.isTransitioningOut ? "out of" : "into"), + (stateInfo.isOverlay ? "overlay " : ""), + stateInfo)); + + // if the state was being transitioned out, then we should mark + // it as inactive, and trigger it's onPop() or onPause() event now + if (stateInfo.isTransitioningOut) { + if (stateInfo.isBeingPopped) { + Gdx.app.debug("StateManager", String.format("Popping %sstate %s", (stateInfo.isOverlay ? "overlay " : ""), stateInfo)); + stateInfo.state.onPop(); + + // TODO: do I care enough to port the return value stuff which goes here? + } else { + Gdx.app.debug("StateManager", String.format("Pausing %sstate %s.", (stateInfo.isOverlay ? "overlay " : ""), stateInfo)); + stateInfo.state.onPause(false); + } + stateInfo.isInactive = true; + } + + // done transitioning + stateInfo.isTransitioning = false; + stateInfo.isTransitioningOut = false; + } + + stateInfo.isTransitionStarting = false; + } + } + } + + private void transitionIn(StateInfo stateInfo, boolean forResuming) { + stateInfo.isInactive = false; + stateInfo.isTransitioning = true; + stateInfo.isTransitioningOut = false; + stateInfo.isTransitionStarting = true; + Gdx.app.debug("StateManager", String.format("Transition into %sstate %s started.", (stateInfo.isOverlay ? "overlay " : ""), stateInfo)); + + //if (forResuming) + // stateInfo.getState().getProcessManager().onResume(false); + } + + private void transitionOut(StateInfo stateInfo, boolean forPopping) { + stateInfo.isTransitioning = true; + stateInfo.isTransitioningOut = true; + stateInfo.isTransitionStarting = true; + stateInfo.isBeingPopped = forPopping; + Gdx.app.debug("StateManager", String.format("Transition out of %sstate %s started.", (stateInfo.isOverlay ? "overlay " : ""), stateInfo)); + + //if (forPopping) + // stateInfo.getState().getProcessManager().removeAll(); + //else + // stateInfo.getState().getProcessManager().onPause(false); + } + + /*** private state getters ***/ + + private StateInfo getStateInfoFor(GameState state) { + if (state == null) + throw new IllegalArgumentException("state cannot be null."); + + for (int i = 0; i < states.size(); ++i) { + if (states.get(i).state == state) + return states.get(i); + } + return null; + } + + private StateInfo getTop() { + return (states.isEmpty() ? null : states.getLast()); + } + + private StateInfo getTopNonOverlay() { + int index = getTopNonOverlayIndex(); + return (index == -1 ? null : states.get(index)); + } + + private int getTopNonOverlayIndex() { + for (int i = states.size() - 1; i >= 0; i--) { + if (!states.get(i).isOverlay) + return i; + } + return (states.size() > 0 ? 0 : -1); + } + + /*** cleanup ***/ + + public void dispose() { + if (states == null) + return; + + Gdx.app.debug("StateManager", "dispose"); + + while (states.size() > 0) { + StateInfo stateInfo = states.getLast(); + Gdx.app.debug("StateManager", String.format("Popping state %s as part of StateManager shutdown.", stateInfo)); + stateInfo.state.onPop(); + stateInfo.state.dispose(); + states.removeLast(); + } + + // these queues will likely not have anything in them, but just in case ... + while (pushQueue.size() > 0) { + StateInfo stateInfo = pushQueue.removeFirst(); + Gdx.app.debug("StateManager", String.format("Deleting push-queued state %s as part of StateManager shutdown.", stateInfo)); + stateInfo.state.dispose(); + } + while (swapQueue.size() > 0) { + StateInfo stateInfo = swapQueue.removeFirst(); + Gdx.app.debug("StateManager", String.format("Deleting swap-queued state %s as part of StateManager shutdown.", stateInfo)); + stateInfo.state.dispose(); + } + + states = null; + pushQueue = null; + swapQueue = null; + } +}