Over the last few months one of my good friends, Trevor Marsh, and I have been trying to develop a ridiculously intense, overly difficult, seizure inducing Contra/Ikaruga reminiscent 2D game for Android.
We have developed (still in the process of developing really) a level editor - Ubik. The goal of Ubik was to make a level editor robust enough to build a large majority of 2D games. You simply build your layers and levels and export them as JSON such that, for the most part, the entire engine can run off of the data exported in the JSON.
Currently Ubik is 2 projects - Ubik and Ubik-Shared. The whole project was designed with the intent of being able to put multiple front ends on it - currently the front end is done in Swing, but it would be really cool to have a Web version hosted on App Engine that anybody could use. The project was written using MVP with all the Models, View interfaces and Presenter logic contained in the Shared project. The Swing implementation uses simple adapter interfaces (very similar to GWT's HasText and all that) such that if we made an Ubik-Web all we should have to do is implement the Views and an export Servlet to get at least a basic version of it working.
Here are 2 simple screenshots of its current state:
We plan on open sourcing the level editor once we actually release the game and have it usable (currently we've only implemented what we need). If you are at all interested in the source code for any of this feel free to email me at sean.exposure@gmail.com for a zip distribution. (The code is fairly clean but like I said, we've only implemented exactly what we need for now).
As of right now - I'd like to say our game engine is roughly 80% done - we have no music or art so... that's kind of a buzzkill... but I'll burn that bridge when I get there. The heart of the game is nothing more than a simple state pattern which is responsible for loading up everything from JSON, caching most of it to be used in the game, and also simply going from splash screens, through menus, to the real game itself.
Here's a simple screenshot of the demo level with the sweet Mario graphics shown above being played in PlayN.
Now, I'm just going to post a lot of the source code I've used to get the game up to this point.
This demo is using Google Guice to set everything up. The game kicks off by starting a Controller and a State Manager - they are listed below.
This is the actual entry point. All this does is spin up the Controller then delegate everything to it.
package com.exposure101.playn.runrun;
import playn.core.Game;
import com.google.inject.Guice;
import com.google.inject.Injector;
public class RunRun implements Game {
private final Injector injector;
private final Controller controller;
public RunRun() {
injector = Guice.createInjector(new RunRunModule());
controller = injector.getInstance(Controller.class);
}
@Override
public void init() {
controller.initialize();
}
@Override
public void update(float delta) {
controller.update(delta);
}
@Override
public void paint(float alpha) {
controller.paint(alpha);
}
@Override
public int updateRate() {
return 25;
}
}
The controller class is given here. All this guy really does is kick off the state machine.
This Controller is dependent on a Context Object which is really nothing more than a glorified HashMap. This is basically used such that - let's say you select a Level from a specific World (think Angry Birds). If you exit back to the menu from a given World, the World will be stored in the Context such that the background of the Menu can reflect this (think Angry Birds Seasons).
package com.exposure101.playn.runrun;
import static com.exposure101.playn.runrun.context.ContextKeys.FPS;
import javax.inject.Inject;
import javax.inject.Singleton;
import playn.core.PlayN;
import com.exposure101.playn.runrun.context.Context;
import com.exposure101.playn.runrun.shared.Initializable;
import com.exposure101.playn.runrun.shared.Paintable;
import com.exposure101.playn.runrun.shared.Updatable;
import com.exposure101.playn.runrun.state.StateManager;
@Singleton
public class Controller implements Initializable, Paintable, Updatable {
private final Context context;
private final StateManager stateManager;
@Inject
public Controller(Context context, StateManager stateManager) {
this.context = context;
this.stateManager = stateManager;
}
@Override
public void initialize() {
context.set(FPS, Integer.valueOf(30));
}
@Override
public void update(float delta) {
if (stateManager.getCurrentState() == null) {
stateManager.goTo(stateManager.getExposure101SplashScreenState());
}
stateManager.getCurrentState().update(delta);
}
@Override
public void paint(float alpha) {
if (stateManager.getCurrentState() != null) {
stateManager.getCurrentState().paint(alpha);
} else {
PlayN.log().error("current state is null");
}
}
public Context getContext() {
return context;
}
}
The StateManager is dependent on the State interface, this is fairly simple.
package com.exposure101.playn.runrun.state;
import com.exposure101.playn.runrun.shared.Destroyable;
import com.exposure101.playn.runrun.shared.HandlesError;
import com.exposure101.playn.runrun.shared.HasKeyboardListener;
import com.exposure101.playn.runrun.shared.Initializable;
import com.exposure101.playn.runrun.shared.Paintable;
import com.exposure101.playn.runrun.shared.Updatable;
public interface State extends Destroyable, Updatable, Paintable, Initializable,
HasKeyboardListener, HandlesError {
}
The Abstract State is also fairly simple.
package com.exposure101.playn.runrun.state;
import playn.core.Keyboard;
import com.google.inject.Inject;
public abstract class AbstractState implements State {
protected final StateManager stateManager;
@Inject
public AbstractState(StateManager stateManager) {
this.stateManager = stateManager;
}
@Override
public Keyboard.Listener getKeyboardListener() {
return null;
}
@Override
public void reportError(Throwable error) {
stateManager.reportError(error);
}
}
The StateManager implementation is given below - this is pretty stripped down. If you notice in the goTo(State to) method that before the State is switched, the current State is destroyed. Any Group Layers or Layers added to the Root Layer should be removed - any other resources should definitely be freed up in this method.
package com.exposure101.playn.runrun.state;
import javax.inject.Inject;
import com.exposure101.playn.runrun.context.Context;
import com.exposure101.playn.runrun.state.level.LevelState;
import com.exposure101.playn.runrun.state.level.LevelStateBinding;
import com.exposure101.playn.runrun.state.loading.LevelLoadingStateBinding;
import com.exposure101.playn.runrun.state.loading.LoadingState;
import com.exposure101.playn.runrun.state.loading.LoadingStateBinding;
import com.exposure101.playn.runrun.state.menu.LevelGroupMenuStateBinding;
import com.exposure101.playn.runrun.state.menu.MainMenuStateBinding;
import com.exposure101.playn.runrun.state.menu.MenuState;
import com.exposure101.playn.runrun.state.splash.Exposure101SplashScreenStateBinding;
public class StateManagerImpl implements StateManager {
private final Context context;
private State currentState;
private final State exposure101SplashScreenState;
private final State loadingState;
private final State menuState;
private final State levelGroupState; // NEED BETTER NAME
private final State levelLoadingState;
private final State levelState;
@Inject
public StateManagerImpl(
Context context,
@Exposure101SplashScreenStateBinding State exposure101SplashScreenState,
@LoadingStateBinding LoadingState loadingState,
@MainMenuStateBinding MenuState menuState,
@LevelGroupMenuStateBinding MenuState levelGroupState,
@LevelLoadingStateBinding LoadingState levelLoadingState,
@LevelStateBinding LevelState levelState) {
this.context = context;
this.exposure101SplashScreenState = exposure101SplashScreenState;
this.loadingState = loadingState;
this.menuState = menuState;
this.levelGroupState = levelGroupState;
this.levelLoadingState = levelLoadingState;
this.levelState = levelState;
}
@Override
public void goTo(State to) {
if (currentState != null) {
currentState.destroy();
}
currentState = to;
currentState.initialize();
}
@Override
public void reportError(Throwable error) {
error.printStackTrace();
}
@Override
public State getCurrentState() {
return currentState;
}
@Override
public Context getContext() {
return context;
}
@Override
public State getExposure101SplashScreenState() {
return exposure101SplashScreenState;
}
@Override
public State getLoadingState() {
return loadingState;
}
@Override
public State getMenuState() {
return menuState;
}
@Override
public State getLevelGroupMenuState() {
return levelGroupState;
}
@Override
public State getLevelLoadingState() {
return levelLoadingState;
}
@Override
public State getLevelState() {
return levelState;
}
}
So, now that this is all given we can see where the game kicks off. The first thing we use is a Splash Screen State. Some people do loading in this state - but I definitely don't. Since many games have more than one Splash Screen - I made an Abstract Splash Screen State. We only use the Exposure101 Splash Screen, but if you wanted to add another all you would have to do is add it to the State Manager.
Here's the Abstract Splash State - it's pretty simple.
package com.exposure101.playn.runrun.state.splash;
import playn.core.Keyboard;
import com.exposure101.playn.runrun.state.AbstractState;
import com.exposure101.playn.runrun.state.StateManager;
public abstract class AbstractSplashScreenState extends AbstractState {
protected final long SPLASH_SCREEN_TIMEOUT = 2600;
public AbstractSplashScreenState(StateManager stateManager) {
super(stateManager);
}
@Override
public Keyboard.Listener getKeyboardListener() {
return null;
}
}
And here's the full blown Splash Screen State. Note that the paint() method is empty - this is because any Image Layers added to the Root Layer will be drawn by PlayN automatically (somebody feel free to correct me if this is wrong).
package com.exposure101.playn.runrun.state.splash;
import static playn.core.PlayN.assets;
import static playn.core.PlayN.graphics;
import javax.inject.Inject;
import playn.core.Image;
import playn.core.ImageLayer;
import playn.core.PlayN;
import com.exposure101.playn.runrun.shared.Command;
import com.exposure101.playn.runrun.shared.Timeout;
import com.exposure101.playn.runrun.state.StateManager;
public class Exposure101SplashScreenState extends AbstractSplashScreenState {
private Image splashScreenImage;
private ImageLayer imageLayer;
private Timeout timeout;
@Inject
public Exposure101SplashScreenState(StateManager stateManager) {
super(stateManager);
}
@Override
public void initialize() {
splashScreenImage = assets().getImage("img/exposure101_logo.png");
imageLayer = graphics().createImageLayer(splashScreenImage);
graphics().rootLayer().add(imageLayer);
final float xScale = graphics().width() / splashScreenImage.width();
final float yScale = graphics().height() / splashScreenImage.height();
PlayN.log().debug("screen width = " + graphics().width());
PlayN.log().debug("screen height = " + graphics().height());
PlayN.log().debug("splash screen width = " + splashScreenImage.width());
PlayN.log().debug("splash screen height = " + splashScreenImage.height());
imageLayer.setScale(xScale, yScale);
graphics().rootLayer().add(imageLayer);
timeout = new Timeout(SPLASH_SCREEN_TIMEOUT, new Command() {
@Override
public void execute() {
stateManager.goTo(stateManager.getLoadingState());
}
});
}
@Override
public void destroy() {
imageLayer.destroy();
}
@Override
public void update(float delta) {
if (timeout != null) {
if (timeout.isRunning() == false) {
timeout.start();
} else {
timeout.update(delta);
}
}
}
@Override
public void paint(float alpha) {
}
}
If you notice, this class used a Timeout Object - this is given below as it's worth knowing a somewhat easy way to do this in PlayN.
package com.exposure101.playn.runrun.shared;
import static playn.core.PlayN.currentTime;
public class Timeout implements Updatable {
private final float timeout;
private final Command command;
private boolean running;
private double startTime;
private double endTime;
public Timeout(float timeout, Command command) {
this.timeout = timeout;
this.command = command;
}
public void start() {
startTime = currentTime();
endTime = startTime + timeout;
running = true;
}
public boolean isRunning() {
return running;
}
@Override
public void update(float delta) {
if (running) {
if (currentTime() >= endTime) {
running = false;
command.execute();
}
}
}
}
So - once the Timeout is finished it will execute the Command to go to the Loading State. This one's a little bit more complicated.
The Entity Metadata Cache as well as the Level Group Metadata Cache are passed into this state, added to a List of Loadable (which is a simple Interface with a load() method), and then loaded in the initialize() method when the goTo(...) method is called on the State Manager.
package com.exposure101.playn.runrun.state.loading;
import static playn.core.PlayN.assets;
import static playn.core.PlayN.graphics;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import javax.inject.Inject;
import javax.inject.Singleton;
import playn.core.Image;
import playn.core.ImageLayer;
import playn.core.PlayN;
import com.exposure101.playn.runrun.state.AbstractState;
import com.exposure101.playn.runrun.state.StateManager;
@Singleton
public class LoadingStateImpl extends AbstractState implements LoadingState {
private final List<Loadable> loadables;
private final Iterator<Loadable> iterator;
private Image loadingImage;
private ImageLayer imageLayer;
@Inject
public LoadingStateImpl(
StateManager stateManager,
@EntityMetadataCacheLoaderBinding Loadable entityMetadataCacheLoader,
@LevelGroupMetadataCacheLoaderBinding Loadable levelGroupMetadataCacheLoader) {
super(stateManager);
loadables = new ArrayList<Loadable>();
loadables.add(entityMetadataCacheLoader);
loadables.add(levelGroupMetadataCacheLoader);
iterator = loadables.iterator();
}
@Override
public void loadNext() {
if (iterator.hasNext()) {
iterator.next().load();
} else {
stateManager.goTo(stateManager.getMenuState());
}
}
@Override
public void initialize() {
loadingImage = assets().getImage("img/loading.png");
imageLayer = graphics().createImageLayer(loadingImage);
graphics().rootLayer().add(imageLayer);
final float xScale = graphics().width() / loadingImage.width();
final float yScale = graphics().height() / loadingImage.height();
PlayN.log().debug("loading image width = " + loadingImage.width());
PlayN.log().debug("loading image height = " + loadingImage.height());
imageLayer.setScale(xScale, yScale);
graphics().rootLayer().add(imageLayer);
loadNext();
}
@Override
public void destroy() {
imageLayer.destroy();
}
@Override
public void update(float delta) {
}
@Override
public void paint(float alpha) {
}
}
The Metadata Caches are loaded up at start up with just the basic Metadata for all the entities. The entities are actually read from the Level JSON files and populated in the Level state which we'll see later.
Here is the Entity Metadata Cache. This is somewhat complex but basically just uses a series of callbacks to manage loading up all the entity metadata from a set of JSON files - the path to this set is passed in via Guice.
package com.exposure101.playn.runrun.state.loading;
import static com.exposure101.playn.runrun.context.ContextKeys.ENTITY_METADATA_CACHE;
import static playn.core.PlayN.assets;
import static playn.core.PlayN.json;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Provider;
import javax.inject.Singleton;
import playn.core.Json;
import playn.core.ResourceCallback;
import com.exposure101.playn.runrun.context.Context;
import com.exposure101.playn.runrun.entity.EntityMetadata;
import com.exposure101.playn.runrun.entity.EntityMetadataCache;
import com.exposure101.playn.runrun.json.JsonReader;
import com.exposure101.playn.runrun.shared.Callback;
@Singleton
public class EntityMetadataCacheLoader implements Loadable {
private final LoadingState loadingState;
private final Context context;
private final EntityMetadataCache cache;
private final Provider<JsonReader<EntityMetadata>> jsonReader;
private final String entityMetadata;
private List<String> paths;
private Iterator<String> iterator;
@Inject
public EntityMetadataCacheLoader(
@LoadingStateBinding LoadingState loadingState,
Context context,
EntityMetadataCache entityMetadataCache,
Provider<JsonReader<EntityMetadata>> jsonReader,
@Named("EntityMetadata") String entityMetadata) {
this.loadingState = loadingState;
this.context = context;
this.cache = entityMetadataCache;
this.jsonReader = jsonReader;
this.entityMetadata = entityMetadata;
}
@Override
public void load() {
assets().getText(entityMetadata, new ResourceCallback<String>() {
@Override
public void error(Throwable error) {
loadingState.reportError(error);
}
@Override
public void done(String resource) {
final Json.Object json = json().parse(resource);
final Json.Array array = json.getArray("entities");
paths = new ArrayList<String>(array.length());
for (int i = 0; i < array.length(); i++) {
paths.add(array.getObject(i).getString("path"));
}
iterator = paths.iterator();
cache();
}
});
}
private void cache() {
if (iterator.hasNext()) {
cache(iterator.next(), new Callback.Default<String>(loadingState) {
@Override
public void onSuccess(String t) {
cache();
}
});
} else {
context.set(ENTITY_METADATA_CACHE, cache);
loadingState.loadNext();
}
}
private void cache(String path, final Callback<String> callback) {
jsonReader.get().read(path, new ResourceCallback<EntityMetadata>() {
@Override
public void error(Throwable error) {
callback.onFailure(error);
}
@Override
public void done(EntityMetadata resource) {
cache.cache(resource);
callback.onSuccess(resource.getName());
}
});
}
}
The Level Group Metadata Cache Loader is basically the same thing.
package com.exposure101.playn.runrun.state.loading;
import static com.exposure101.playn.runrun.context.ContextKeys.LEVEL_GROUP_METADATA_CACHE;
import java.util.Iterator;
import java.util.List;
import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Provider;
import javax.inject.Singleton;
import playn.core.ResourceCallback;
import com.exposure101.playn.runrun.context.Context;
import com.exposure101.playn.runrun.json.JsonReader;
import com.exposure101.playn.runrun.level.LevelGroupMetadata;
import com.exposure101.playn.runrun.level.LevelGroupMetadataCache;
import com.exposure101.playn.runrun.shared.Callback;
/**
*
* @author sean christe (sean.exposure@gmail.com)
* @author trevor marsh
*
*/
@Singleton
public class LevelGroupMetadataCacheLoader implements Loadable {
private final LoadingState loadingState;
private final Context context;
private final LevelGroupMetadataCache cache;
private final Provider<JsonReader<LevelGroupMetadata>> jsonReader;
private final Iterator<String> iterator;
@Inject
public LevelGroupMetadataCacheLoader(
@LoadingStateBinding LoadingState loadingState,
Context context,
LevelGroupMetadataCache cache,
Provider<JsonReader<LevelGroupMetadata>> jsonReader,
@Named("LevelGroups") List<String> levelGroups) {
this.loadingState = loadingState;
this.cache = cache;
this.context = context;
this.jsonReader = jsonReader;
this.iterator = levelGroups.iterator();
}
@Override
public void load() {
if (iterator.hasNext()) {
load(iterator.next(), new Callback.Default<String>(loadingState) {
@Override
public void onSuccess(String t) {
load();
}
});
} else {
context.set(LEVEL_GROUP_METADATA_CACHE, cache);
loadingState.loadNext();
}
}
private void load(String levelGroup, final Callback<String> callback) {
jsonReader.get().read(levelGroup, new ResourceCallback<LevelGroupMetadata>() {
@Override
public void done(LevelGroupMetadata resource) {
cache.cache(resource);
callback.onSuccess(resource.getName());
}
@Override
public void error(Throwable error) {
callback.onFailure(error);
}
});
}
}
The JSON Readers are fairly technical and not really important right now - they simply interpret the JSON produced by Ubik and return an Entity Metadata or Level Group Metadata Object.
Once the Loading State has completed it uses the State Manager to go to the Menu State. The Menu State is currently not very pretty - I'm using the Triple Play UI to just make some buttons on the page - in the future (when I actually have graphics to work with that don't suck or aren't just ripped from another game) I'll make the Menu look better.
There are 2 Menus - the Main Menu and the Level Group Menu (I couldn't think of a better name for this - think Angry Birds how you select a World then a Level inside that world). If you wanted to add any more Menus you would just add them to the state machine and change the goTo(...) to whatever your Menu State is.
package com.exposure101.playn.runrun.state.menu;
import static com.exposure101.playn.runrun.context.ContextKeys.LEVEL_GROUP;
import static com.exposure101.playn.runrun.context.ContextKeys.LEVEL_GROUP_METADATA_CACHE;
import static playn.core.PlayN.graphics;
import javax.inject.Inject;
import playn.core.GroupLayer;
import react.UnitSlot;
import tripleplay.ui.Background;
import tripleplay.ui.Button;
import tripleplay.ui.Group;
import tripleplay.ui.Interface;
import tripleplay.ui.Label;
import tripleplay.ui.Root;
import tripleplay.ui.SimpleStyles;
import tripleplay.ui.Style;
import tripleplay.ui.layout.AxisLayout;
import com.exposure101.playn.runrun.level.LevelGroupMetadata;
import com.exposure101.playn.runrun.level.LevelGroupMetadataCache;
import com.exposure101.playn.runrun.state.AbstractState;
import com.exposure101.playn.runrun.state.StateManager;
public class MainMenuStateImpl extends AbstractState implements MenuState {
private Interface ui;
private GroupLayer groupLayer;
@Inject
public MainMenuStateImpl(StateManager stateManager) {
super(stateManager);
}
@Override
public void initialize() {
groupLayer = graphics().createGroupLayer();
graphics().rootLayer().add(groupLayer);
ui = new Interface();
final Root root = ui.createRoot(AxisLayout.vertical().gap(15), SimpleStyles.newSheet());
root.setSize(graphics().width(), graphics().height());
root.addStyles(Style.BACKGROUND.is(Background.solid(0xFF99CCFF).inset(5)));
groupLayer.add(root.layer);
final Group buttons = new Group(AxisLayout.vertical().offStretch());
root.add(new Label("RunRun:"), buttons);
final LevelGroupMetadataCache cache = (LevelGroupMetadataCache) stateManager.getContext().get(
LEVEL_GROUP_METADATA_CACHE);
for (final LevelGroupMetadata levelGroup : cache) {
final Button button = new Button(levelGroup.getName());
buttons.add(button);
button.clicked().connect(new UnitSlot() {
@Override
public void onEmit() {
stateManager.getContext().set(LEVEL_GROUP, cache.get(levelGroup.getName()));
stateManager.goTo(stateManager.getLevelGroupMenuState());
}
});
}
}
@Override
public void destroy() {
groupLayer.destroy();
}
@Override
public void update(float delta) {
}
@Override
public void paint(float alpha) {
if (ui != null) {
ui.paint(alpha);
}
}
}
The Main Menu state just directs to this.
package com.exposure101.playn.runrun.state.menu;
import static com.exposure101.playn.runrun.context.ContextKeys.LEVEL_GROUP;
import static com.exposure101.playn.runrun.context.ContextKeys.LEVEL_JSON;
import static playn.core.PlayN.graphics;
import static playn.core.PlayN.keyboard;
import javax.inject.Inject;
import playn.core.GroupLayer;
import playn.core.Key;
import playn.core.Keyboard;
import playn.core.Keyboard.Event;
import react.UnitSlot;
import tripleplay.ui.Background;
import tripleplay.ui.Button;
import tripleplay.ui.Group;
import tripleplay.ui.Interface;
import tripleplay.ui.Label;
import tripleplay.ui.Root;
import tripleplay.ui.SimpleStyles;
import tripleplay.ui.Style;
import tripleplay.ui.layout.AxisLayout;
import com.exposure101.playn.runrun.level.LevelGroupMetadata;
import com.exposure101.playn.runrun.level.LevelMetadata;
import com.exposure101.playn.runrun.state.AbstractState;
import com.exposure101.playn.runrun.state.StateManager;
/**
*
* @author sean.exposure
* @author trevor.exposure
*
*/
public class LevelGroupMenuStateImpl extends AbstractState implements MenuState {
private Interface ui;
private GroupLayer groupLayer;
@Inject
public LevelGroupMenuStateImpl(StateManager stateManager) {
super(stateManager);
}
@Override
public void initialize() {
groupLayer = graphics().createGroupLayer();
graphics().rootLayer().add(groupLayer);
ui = new Interface();
final LevelGroupMetadata levelGroup = (LevelGroupMetadata) stateManager.getContext().get(
LEVEL_GROUP);
final Root root = ui.createRoot(AxisLayout.vertical().gap(15), SimpleStyles.newSheet());
root.setSize(graphics().width(), graphics().height());
root.addStyles(Style.BACKGROUND.is(Background.solid(0xFF99CCFF).inset(5)));
groupLayer.add(root.layer);
final Group buttons = new Group(AxisLayout.vertical().offStretch());
root.add(new Label(levelGroup.getName()), buttons);
final LevelMetadata[] levels = levelGroup.getLevels();
for (final LevelMetadata level : levels) {
final Button button = new Button(level.getName());
buttons.add(button);
button.clicked().connect(new UnitSlot() {
@Override
public void onEmit() {
stateManager.getContext().set(LEVEL_JSON, level.getPath());
stateManager.goTo(stateManager.getLevelLoadingState());
}
});
}
keyboard().setListener(new Keyboard.Adapter() {
@Override
public void onKeyDown(Event event) {
if (event.key().equals(Key.ESCAPE)) {
destroy();
stateManager.goTo(stateManager.getMenuState());
}
}
});
}
@Override
public void destroy() {
groupLayer.destroy();
}
@Override
public void update(float delta) {
}
@Override
public void paint(float alpha) {
if (ui != null) {
ui.paint(alpha);
}
}
}
As you can see - when a State is selected the State Manager goes to the Level Loading State. The Level Loading State starts the Level World up (which creates actual Entities from the Entity Metadata Cache and populates them into the Level and the Physics Engine - that will not be displayed since it's kind of out of the scope of this). The Level World is given to the Context and the Level State is started.
package com.exposure101.playn.runrun.state.loading;
import static com.exposure101.playn.runrun.context.ContextKeys.LEVEL;
import static com.exposure101.playn.runrun.context.ContextKeys.LEVEL_JSON;
import static playn.core.PlayN.assets;
import static playn.core.PlayN.graphics;
import javax.inject.Inject;
import javax.inject.Provider;
import playn.core.Image;
import playn.core.ImageLayer;
import playn.core.PlayN;
import playn.core.ResourceCallback;
import com.exposure101.playn.runrun.json.JsonReader;
import com.exposure101.playn.runrun.level.Level;
import com.exposure101.playn.runrun.level.LevelWorld;
import com.exposure101.playn.runrun.state.AbstractState;
import com.exposure101.playn.runrun.state.StateManager;
/**
*
* @author sean.exposure
* @author trevor.exposure
*
*/
public class LevelLoadingStateImpl extends AbstractState implements LoadingState {
private final Provider<JsonReader<Level>> jsonReader;
private Image loadingImage;
private ImageLayer imageLayer;
@Inject
public LevelLoadingStateImpl(StateManager stateManager, Provider<JsonReader<Level>> jsonReader) {
super(stateManager);
this.jsonReader = jsonReader;
}
@Override
public void loadNext() {
stateManager.goTo(stateManager.getLevelState());
}
@Override
public void initialize() {
loadingImage = assets().getImage("img/loading.png");
imageLayer = graphics().createImageLayer(loadingImage);
graphics().rootLayer().add(imageLayer);
final float xScale = graphics().width() / loadingImage.width();
final float yScale = graphics().height() / loadingImage.height();
PlayN.log().debug("loading image width = " + loadingImage.width());
PlayN.log().debug("loading image height = " + loadingImage.height());
imageLayer.setScale(xScale, yScale);
graphics().rootLayer().add(imageLayer);
final String path = (String) stateManager.getContext().get(LEVEL_JSON);
jsonReader.get().read(path, new ResourceCallback<Level>() {
@Override
public void done(Level resource) {
stateManager.getContext().set(LEVEL, new LevelWorld(resource, stateManager.getContext()));
stateManager.goTo(stateManager.getLevelState());
}
@Override
public void error(Throwable error) {
stateManager.reportError(error);
}
});
}
@Override
public void destroy() {
imageLayer.destroy();
}
@Override
public void update(float delta) {
}
@Override
public void paint(float alpha) {
}
}
And, here's the finale - the Level State.
package com.exposure101.playn.runrun.state.level;
import static com.exposure101.playn.runrun.context.ContextKeys.LEVEL;
import static playn.core.PlayN.graphics;
import static playn.core.PlayN.keyboard;
import static playn.core.PlayN.pointer;
import javax.inject.Inject;
import playn.core.Key;
import playn.core.Keyboard;
import playn.core.Pointer;
import playn.core.Pointer.Event;
import com.exposure101.playn.runrun.level.LevelWorld;
import com.exposure101.playn.runrun.state.AbstractState;
import com.exposure101.playn.runrun.state.StateManager;
public class LevelStateImpl extends AbstractState implements LevelState {
private LevelWorld levelWorld;
@Inject
public LevelStateImpl(StateManager stateManager) {
super(stateManager);
}
@Override
public void initialize() {
levelWorld = (LevelWorld) stateManager.getContext().get(LEVEL);
pointer().setListener(new Pointer.Adapter() {
@Override
public void onPointerStart(Event event) {
// mouse stuff - don't wanna give away our idea in here just yet - sorry guys
// but basically - just get mouse x & y and delegate it to the Level World
}
});
keyboard().setListener(new Keyboard.Adapter() {
@Override
public void onKeyDown(playn.core.Keyboard.Event event) {
if (event.key().equals(Key.ESCAPE)) {
destroy();
stateManager.goTo(stateManager.getLevelGroupMenuState());
}
if (event.key().equals(Key.P)) {
levelWorld.setPaused(!levelWorld.isPaused());
}
}
});
}
@Override
public void destroy() {
levelWorld.destroy();
}
@Override
public void update(float delta) {
levelWorld.update(delta);
}
@Override
public void paint(float alpha) {
levelWorld.paint(alpha);
}
}
As you can see the Level State really doesn't do anything but delegate to the Level World.
This is the whole Skeleton of the Game. All of the actual intense game logic takes place in the Entity classes and the Level World class. Other things can also be set from here such as various Commands passed to the Level World on what to execute if your player dies or the time's up or anything along those lines.
I know this isn't too PlayN intensive - but everything we've done so far has actually just been figured out from the Peas demo that comes with it - that's a great source of knowledge for a lot of this stuff.
I may post some information about the actual Level World soon - it is definitely outside of this though.
Also - one last thing, the Context and Context Keys.
package com.exposure101.playn.runrun.context;
import com.exposure101.playn.runrun.entity.EntityMetadataCache;
import com.exposure101.playn.runrun.level.LevelGroupMetadata;
import com.exposure101.playn.runrun.level.LevelGroupMetadataCache;
import com.exposure101.playn.runrun.level.LevelWorld;
public enum ContextKeys implements Context.Key {
@Context.Key.Type(Integer.class)
FPS,
@Context.Key.Type(EntityMetadataCache.class)
ENTITY_METADATA_CACHE,
@Context.Key.Type(LevelGroupMetadataCache.class)
LEVEL_GROUP_METADATA_CACHE,
@Context.Key.Type(LevelGroupMetadata.class)
LEVEL_GROUP,
@Context.Key.Type(String.class)
LEVEL_JSON,
@Context.Key.Type(LevelWorld.class)
LEVEL
}
package com.exposure101.playn.runrun.context;
import java.util.HashMap;
import java.util.Map;
import javax.inject.Singleton;
@Singleton
public class ContextImpl implements Context {
private final Map<Context.Key, Object> context;
public ContextImpl() {
context = new HashMap<Context.Key, Object>();
}
@Override
public Object get(Context.Key key) {
return context.get(key);
}
@Override
public void set(Context.Key key, Object value) {
context.put(key, value);
}
}
package com.exposure101.playn.runrun.context;
import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.RetentionPolicy.SOURCE;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
public interface Context {
interface Key {
@Target({ FIELD })
@Retention(SOURCE)
@interface Type {
Class<?> value();
}
}
Object get(Context.Key key);
void set(Context.Key key, Object value);
}