Friday, July 5, 2013

Initializing An AngularJS App with Oauth2 and Google Endpoints (Hackish)

So - I originally wrote this back around July 4th but wasn't really happy with it (I ended up writing it down in Florida, away from my actual work computer).  Recently, our last Android app (CoinPrice) got shut down over a cease and desist (which I was stressing about and took some time to figure that one out) and Exposure101 as a whole kind of fell apart.  That being said, I still love technology and plan to update this blog occasionally whenever I get to play with something new.

So - back when I wrote this I had to do something I considered to be kind of kludge to get an AngularJS app initialized with your Google account.  The snag I hit in doing this is as follows: Google provides a demo of bootstrap your app using the endpoints and oauth2 JavaScript libraries - but it doesn't show how to integrate your login status into the AngularJS framework (e.g. putting it in a variable on your controller's scope such that you can hide or display certain things on your page).  Now, I'm a server side Java person, I've been really digging being able to write client stuff recently with AngularJS & Bootstrap, but I am by no means a JavaScript expert.  I started reading Effective JavaScript and have been brushing up on it for work, but there may be better ways out there of doing this.

So - here's what I ended up doing:

Firstly, my goal here was to have your data show up immediately if you were already logged into your account - otherwise your data should show up after you login.

In order to bootstrap Google's Oauth library you have to have the following line in your index.html - when the script has finished loading it will run the init function you give it in the onload parameter.

<script src="https://apis.google.com/js/client.js?onload=init"></script>

Now you actually need to implement the init method somewhere in your JavaScript.  I put mine in a script tag in my index.html as shown below:

<!-- google user authentication -->
<script>
var init = function() {
  var $injector = angular.injector(['ng', 'exposure101.services', 'exposure101.lifelogger']);
  var InitializationService = $injector.get('InitializationService');
  var AuthenticationService = $injector.get('AuthenticationService');
  var EventService = $injector.get('EventService');
  InitializationService.initialize().then(function() {
    AuthenticationService.login(true).then(function(authenticationModel) {
      var $scope = angular.element('body').scope();
      $scope.$apply(function() {
        $scope.authenticationModel = authenticationModel;
      });
      EventService.loadEvents().then(function(events) {
        $scope.$apply(function() {
          $scope.events = events;
        });
      });
    });
  });
};
</script>
<script src="https://apis.google.com/js/client.js?onload=init"></script>

Now there's some stuff here you need to understand.

  • Apparently on every DOM element in Angular there exists an ng-scope that you have access to - you can get access to this scope by using Angular's element selector and calling scope() on it (When you would do this outside of this example, I'm not really sure, but you do have the option available to you).  
  • Services/Factories are normally singleton instances - you can persist state across Controllers by keeping the state in the Service/Factory objects and injecting them into your Controllers.   Normally, using Angular's Dependency Injection system, you only have to worry about single instances of your Services/Factories.  Now that this is drilled in, when you use angular.injector ($injector.get(...) in our case) it will create a new instance of the Service/Factory, this instance is not the one that will be injected into your controller (by default) through Angular's Dependency Injection system.  So - be careful with these Services/Factories and don't persist anything in them you plan to use in your controller (think of them almost like Stateless Session beans)
Now - all that being said, here's the rest of the code starting with the Authentication Model:

angular.module('exposure101.models')
.factory('AuthenticationModel', function() {
  'use strict';

  var authenticationModel = {
    isLoggedIn: false,
    authenticationToken: {}
  };

  return authenticationModel;
});


Here's The Authentication Service:

angular.module('exposure101.services')
.service('AuthenticationService', function($q, $rootScope, AuthenticationModel) {
  'use strict';

  var authenticationToken = {};
  var deferred = {}; // hackity hack hack

  this.login = function(initialLogin) {
    deferred = $q.defer();
    doLogin(initialLogin);
    return deferred.promise;
  };

  var doLogin = function(mode) {
    var opts = {
      // localhost client id (make sure this is set to port 9000 in google api console)
      client_id: 'your_client_id.apps.googleusercontent.com',
      scope: 'https://www.googleapis.com/auth/userinfo.email',
      immediate: mode,
      response_type: 'token id_token'
    };
    gapi.auth.authorize(opts, handleLogin);
  };

  var handleLogin = function() {
    gapi.client.oauth2.userinfo.get().execute(function(response) {
      if (!response.code) {
        authenticationToken = gapi.auth.getToken();
        authenticationToken.access_token = authenticationToken.id_token;
        AuthenticationModel.isLoggedIn = true;
        AuthenticationModel.authenticationToken = authenticationToken;
        $rootScope.$apply(function() {
          deferred.resolve(AuthenticationModel);
        });
      }
    });
  };
});


Here's The Initialization Service:

angular.module('exposure101.services')
.service('InitializationService', function($q, $rootScope) {
  'use strict';

  this.initialize = function() {
    var deferred = $q.defer();
    var apisToLoad = 2;

    var loginCallback = function() {
      if (--apisToLoad === 0) {
        $rootScope.$apply(function() {
          // console.log('finished loading up client libraries - should be resolving');
          deferred.resolve();
        });
      }
    };

    gapi.client.load('events', 'v1', loginCallback, 'http://localhost:8888/_ah/api');
    gapi.client.load('oauth2', 'v2', loginCallback);

    return deferred.promise;
  };
});

And lastly here's the controller:

angular.module('exposure101.lifelogger')
.controller('LifeLoggerController', function($scope, AuthenticationModel, AuthenticationService, EventService) {
  'use strict';

  $scope.authenticationModel = AuthenticationModel;

  $scope.categories = [];
  $scope.events = [];
  $scope.labels = [];

  ...

  $scope.login = function() {
    AuthenticationService.login(false).then(function(authenticationModel) {
      $scope.authenticationModel = authenticationModel;
      EventService.loadEvents().then(function(events) {
        $scope.events = events;
      });
    });
  };
});


Now - recently I got a comment asking how this was different than the directive on this site: https://github.com/sirkitree/angular-directive.g-signin/blob/master/google-plus-signin.js.  This directive is actually a solid implementation of what I was trying to do - instead of going through all the setup described in this blog this actually works very well (and with a nicer interface including a more standardized Google login button).  I'm going to modify this directive a bit to make it more to my liking and I'll post the code in a new article on this blog.

Monday, April 22, 2013

An example AngularJS app with Google App Engine RESTful Web Services

So I'm fortunate enough to work with people that are constantly pushing for us to learn new technologies.  I released a pretty niche Android app maybe 2 weeks ago for Exposure101 - that aside I've had to start learning AngularJS at my real job over the last week.  Over the last year or two I've been working pretty heavily with GWT - and in all honesty had never really done any type of JavaScript - just doing Angular for these last few days has completely sold me on it.

I put together a pretty simple App to show the basics of Angular and how to setup a simple Web Service environment.  I'm a huge fan of Google App Engine - it's really simple to use and you can stand up an entire backend in 20 minutes once you've done it before.  I'm gonna start with the server first and move on to Angular after.

Before we start you'll need the following libraries (versions are just the ones I used) - Make a new App Engine project (for this example my project name is "angular-demo" with the package "com.exposure101.angular.demo") and copy them into your war/WEB-INF/lib directory.


I try to avoid XML configurations and Guice has a really nice way of helping you to setup your server.  The first thing to do is open up your web.xml, clear everything between the web-app tags and setup the Guice Filter and Servlet Context Listener as follows:

 <!-- Servlets -->
  <listener>
    <listener-class>com.exposure101.angular.demo.server.AngularDemoServletContextListener</listener-class>
  </listener>

  <filter>
    <filter-name>guiceFilter</filter-name>
    <filter-class>com.google.inject.servlet.GuiceFilter</filter-class>
  </filter>

  <filter-mapping>
    <filter-name>guiceFilter</filter-name>
    <url-pattern>/*</url-pattern>
  </filter-mapping>

  <!-- Default page to serve -->
  <welcome-file-list>
    <welcome-file>Angular_demo.html</welcome-file>
  </welcome-file-list>

  <!-- generated by app engine - if you delete it it will come back -->
  <servlet>
    <servlet-name>SystemServiceServlet</servlet-name>
    <servlet-class>com.google.api.server.spi.SystemServiceServlet</servlet-class>
    <init-param>
      <param-name>services</param-name>
      <param-value />
    </init-param>
  </servlet>
  <servlet-mapping>
    <servlet-name>SystemServiceServlet</servlet-name>
    <url-pattern>/_ah/spi/*</url-pattern>
  </servlet-mapping>

The web.xml and the next 3 classes are basically boilerplate server side Guice. If you're not familiar this I would highly advise you go check it out - it's pretty awesome.  At a really high level here's what each one does:
  • ServletContextListener - provides all the modules you plan to inject (in our case it will only be 2)
  • ServletModule - this is now your web.xml - you define your servlets and filters in here
  • AbstractModule - this binds the classes that will be injected through your app.

Here's the ServletContextListener:

package com.exposure101.angular.demo.server;

import com.google.inject.Guice;
import com.google.inject.Injector;
import com.google.inject.servlet.GuiceServletContextListener;

public class AngularDemoServletContextListener extends GuiceServletContextListener {

  @Override
  protected Injector getInjector() {
    return Guice.createInjector(new AngularDemoModule(), new AngularDemoServletModule());
  }
}


The ServletContextListener's pretty simple - Here's the ServletModule:

package com.exposure101.angular.demo.server;

import java.util.HashMap;
import java.util.Map;

import com.google.inject.servlet.ServletModule;
import com.sun.jersey.guice.spi.container.servlet.GuiceContainer;

public class AngularDemoServletModule extends ServletModule {

  @Override
  protected void configureServlets() {
    final Map<String, String> params = new HashMap<String, String>();
    params.put("javax.ws.rs.Application",
        "com.exposure101.angular.demo.server.AngularDemoRestApplication");
    serve("/rest/*").with(GuiceContainer.class, params);
  }
}


The ServletModule does a little bit more.  REST Web Service classes (I've been calling them Providers for forever - I've seen people call them Providers and Services - I'm not sure if there's a standard naming convention - if I broke it I'm sorry) are kicked off as Singletons during deployment via an Application class.  Whenever a call is made to our server's rest path it will be served with the Application class passed in above.  Since we're serving it up through the GuiceContainer we can inject any dependencies into it through our implemented AbstractModule:

package com.exposure101.angular.demo.server;

import com.exposure101.angular.demo.server.dao.Dao;
import com.exposure101.angular.demo.server.dao.PersonDao;
import com.exposure101.angular.demo.server.model.Person;
import com.exposure101.angular.demo.server.services.AbstractProvider;
import com.exposure101.angular.demo.server.services.PersonProvider;
import com.google.inject.AbstractModule;
import com.google.inject.TypeLiteral;

public class AngularDemoModule extends AbstractModule {

  @Override
  protected void configure() {
    bind(new TypeLiteral<Dao<Person>>() {}).to(PersonDao.class);
    bind(new TypeLiteral<AbstractProvider<Person>>() {}).to(PersonProvider.class);
  }
}

These classes haven't been implemented yet - but basically any time an @Inject is used for a Dao<Person> or an AbstractProvider<Person>, the bound PersonDao or PersonProvider will be injected for you.  For this project the Guice is definitely overkill - but I've used it in every project I've worked on in the last year and I'm really trying to push it on you.

The last important class here is the actual Application class:

package com.exposure101.angular.demo.server;

import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;

import javax.inject.Inject;
import javax.ws.rs.Path;
import javax.ws.rs.core.Application;

import com.exposure101.angular.demo.server.model.Person;
import com.exposure101.angular.demo.server.services.AbstractProvider;

@Path("/rest")
public class AngularDemoRestApplication extends Application {

  @Inject
  AbstractProvider<Person> personService;
  
  @Override
  public Set<Object> getSingletons() {
    return new HashSet<Object>(Arrays.asList(new Object[] { personService }));
  }
}


Since there's only 1 provider this class seems pretty trivial, if there were more you would bind them in your Module and inject them in the same way.  Now that the boilerplate code is up you're just going to build out the rest of the backend.  All you have to do is create the Person model, a DAO to interface with the datastore (Objectify is hands down The best framework for App Engine's datastore) and the actual Web Service Provider for your Angular app to query against.  I'm just going to lay these out in bulk here.

Here's the actual Person Model:

package com.exposure101.angular.demo.server.model;

import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.PrePersist;

import com.googlecode.objectify.annotation.Indexed;
import com.googlecode.objectify.annotation.Unindexed;

@Entity
public class Person {

  @Id
  private Long id;
  
  @Unindexed
  private Integer version;
  
  @Indexed
  private String firstName;
  
  @Indexed
  private String lastName;
  
  @Indexed
  private String ssn;
  
  @Indexed
  private String gender;
  
  @Indexed
  private String dateOfBirth;
  
  public Person() {
  }

  public Long getId() {
    return id;
  }

  public Integer getVersion() {
    return version;
  }

  public String getFirstName() {
    return firstName;
  }

  public String getLastName() {
    return lastName;
  }

  public String getSsn() {
    return ssn;
  }

  public String getGender() {
    return gender;
  }

  public String getDateOfBirth() {
    return dateOfBirth;
  }

  public void setId(Long id) {
    this.id = id;
  }

  public void setVersion(Integer version) {
    this.version = version;
  }

  public void setFirstName(String firstName) {
    this.firstName = firstName;
  }

  public void setLastName(String lastName) {
    this.lastName = lastName;
  }

  public void setSsn(String ssn) {
    this.ssn = ssn;
  }

  public void setGender(String gender) {
    this.gender = gender;
  }

  public void setDateOfBirth(String dateOfBirth) {
    this.dateOfBirth = dateOfBirth;
  }
  
  @PrePersist
  public void updateVersion() {
    if (version == null) {
      version = Integer.valueOf(0);
    }
    version++;
  }
}



Here's The Objectify classes:

The DAO interface:

package com.exposure101.angular.demo.server.dao;

import java.util.List;

import com.googlecode.objectify.Key;

public interface Dao<T> {

  T find(Long id);

  T find(String id);

  T find(Key<T> key);

  Key<T> key(T t);

  List<T> findAll();

  List<T> findAll(List<Key<T>> keys);

  List<Key<T>> key(List<T> list);

  T persist(T t);

  void delete(T t);
}


The Abstract DAO:

package com.exposure101.angular.demo.server.dao;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;

import com.exposure101.angular.demo.server.model.Person;
import com.googlecode.objectify.Key;
import com.googlecode.objectify.ObjectifyService;
import com.googlecode.objectify.util.DAOBase;

public abstract class AbstractDao<T> extends DAOBase implements Dao<T> {

  static {
    ObjectifyService.register(Person.class);
  }
  
  private final Class<T> clazz;
  
  public AbstractDao(Class<T> clazz) {
    this.clazz = clazz;
  }
  
  @Override
  public T find(Long id) {
    return ObjectifyService.begin().find(clazz, id);
  }
  
  @Override
  public T find(String id) {
    return ObjectifyService.begin().find(clazz, id);
  }
  
  @Override
  public T find(Key<T> key) {
    return ObjectifyService.begin().find(key);
  }
  
  @Override
  public Key<T> key(T t) {
    return ObjectifyService.factory().getKey(t);
  }
  
  @Override
  public List<T> findAll() {
    return ObjectifyService.begin().query(clazz).list();
  }
  
  @Override
  public List<T> findAll(List<Key<T>> keys) {
    if (keys == null) {
      return null;
    }
    final Map<Key<T>, T> map = ObjectifyService.begin().get(keys);
    final List<T> list = new ArrayList<T>();
    for (final T t : map.values()) {
      list.add(t);
    }
    return list;
  }
  
  @Override
  public List<Key<T>> key(List<T> list) {
    if (list == null) {
      return null;
    }
    final List<Key<T>> keys = new ArrayList<Key<T>>(list.size());
    for (final T t : list) {
      final Key<T> key = ObjectifyService.factory().getKey(t);
      keys.add(key);
    }
    return keys;
  }
  
  @Override
  public T persist(T t) {
    ObjectifyService.begin().put(t);
    return t;
  }
  
  @Override
  public void delete(T t) {
    ObjectifyService.begin().delete(t);
  }
}

And the actual Person DAO implementation:

package com.exposure101.angular.demo.server.dao;

import com.exposure101.angular.demo.server.model.Person;

public class PersonDao extends AbstractDao<Person> {

  public PersonDao() {
    super(Person.class);
  }
}

And here's the Provider classes:
The Abstract Provider
package com.exposure101.angular.demo.server.services;

import javax.inject.Inject;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;

import com.exposure101.angular.demo.server.dao.Dao;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;


public abstract class AbstractProvider<T> {

  @Inject
  private Dao<T> dao;
  
  private final Class<T> clazz;
  
  public AbstractProvider(Class<T> clazz) {
    this.clazz = clazz;
  }
  
  @GET
  @Path("/get/{id}")
  public String get(@PathParam("id") String id) {
    final Gson gson = new GsonBuilder().setPrettyPrinting().create();
    if ("all".equals(id)) {
      return gson.toJson(dao.findAll().toArray());
    } else {
      return gson.toJson(dao.find(Long.valueOf(id)));
    }
  }
  
  @POST
  @Path("/persist")
  public void persist(String body) {
    final Gson gson = new GsonBuilder().setPrettyPrinting().create();
    final T t = gson.fromJson(body, clazz);
    dao.persist(t);
  }
}


And the Person Provider Implementation

package com.exposure101.angular.demo.server.services;

import javax.ws.rs.Path;

import com.exposure101.angular.demo.server.model.Person;

@Path("/person")
public class PersonProvider extends AbstractProvider<Person> {

  public PersonProvider() {
    super(Person.class);
  }
}

Now that this is all up we can test the Web Services.  You should be able to start your server in debug mode and put a breakpoint on the first line in the GET method of your AbstractProvider class.  Once the server comes up try hitting the following link (assuming you didn't change the default port of 8888): http://127.0.0.1:8888/rest/person/get/all.

Time to start the Angular side.


Create a new Angular project using the following commands:

npm install generator-angular generator-karma
yo angular
bower install angular-ui
npm install grunt-proxy

The first thing you'll need to do once you'e finished creating the project is to setup a proxy in your Gruntfile.  By default your Angular Server will run on port 9000 - if you try to access an App Engine Web Service on port 8888 you will get a cross domain error.  In the root of your Angular App open up the Gruntfile.js and edit the following:

  • Before the grunt.initConfig call load the grunt proxy server
  • Create the proxy on an arbitrary port (not 8888 or 9000) and setup the routes as shown below
  • Edit the open task to load your Angular app on the correct port


  grunt.loadNpmTasks('grunt-proxy');
  grunt.initConfig({
    yeoman: yeomanConfig,
    watch: {
      ...
    },
    proxy: {
      proxy1: {
        options: {
          port : 8050,
          host : 'localhost',
          router : {
            'localhost/rest/*' : 'localhost:8888',
            'localhost' : 'localhost:9000'
          }
        }
      }
    },
    connect: {
       ...
    },
    open: {
      server: {
        url: 'http://localhost:<%= proxy.proxy1.options.port %>'
      }
    },
    ...
  });    


From now on, when you start your Grunt server, instead of typing in "grunt server" you will need to call "grunt proxy server".

The first thing I did was build up the views since that's an easy place to start. Under your app's views directory create a file called "CreatePerson.html".  Once you copy in the HTML below it will look as follows:



<div class="hero-unit">
  <div class="row">
    <div class="span2">First Name:</div>
    <div class="span3">
      <input type="text" ng-model="person.firstName"></input>
    </div>
  </div>
  <div class="row">
    <div class="span2">Last Name:</div>
    <div class="span3">
      <input type="text" ng-model="person.lastName"></input>
    </div>
  </div>
  <div class="row">
    <div class="span2">SSN:</div>
    <div class="span3">
      <input type="text" ng-model="person.ssn"></input>
    </div>
  </div>
  <div class="row">
    <div class="span2">Gender:</div>
    <div class="span3">
      <input type="text" ng-model="person.gender"></input>
    </div>
  </div>
  <div class="row">
    <div class="span2">Date of Birth:</div>
    <div class="span3">
      <input type="text" ng-model="person.dateOfBirth"></input>
    </div>
  </div>
  <div class="row">
    <div class="span2">
      <button class="btn" ng-click="doSubmit()">Submit</button>
    </div>
  </div>
</div>


There's some important things to cover before continuing.  Currently there is no Controller for this View, that doesn't matter right now but it will.  In Angular, each View is mapped to a Controller and the two communicate through a Scope object.  There's some nice visualizations for this on Angular's site but I found it made a lot more sense to me after just implementing it.  It's important to note (and I'll cover it in more detail soon) that everything in Angular is injected - you don't have to worry about instantiating these Controllers or Scope objects.

You'll notice in the above HTML there's a bunch of attributes that start with "ng".  These are Angular's built in directives.  You can think of directives as HTML tasks - they encapsulate both HTML and JavaScript in order to do a certain task.  Let's say in your HTML code you want to turn an input into a lookup widget such that when you begin typing into it, it queries a server and creates a typeahead to display the results.  You can write all the necessary JavaScript and HTML in a directive called "lookupWidget" and simply create your input in HTML along the lines of
<input type="text" data-lookup-widget="lookupResults"/>.

In this file particularly you'll notice the "ng-model" directives.  There is no Controller currently but this directive is actually binding the fields of the "person" object in the Scope to the text value of the inputs.  If that doesn't quite click yet I'm sure it will a few paragraphs down.  It is really important to note that you do not have to explicitly define any of your objects in your Controller, any object used/defined in the View will be present in the Scope.

Now I'm going to setup the actual Controller for this View.

In your app/script/controllers directory, create a file called CreatePersonController.js.  (Whenever you create a new JavaScript file remember to import it in your index.html file)  All this class is going to be responsible for is taking the person object from the view and persisting it to the server.  The code to do this is shown below:

angular.module('angular.demoApp')
  .controller('CreatePersonController', function ($scope, $routeParams, PersonService) {
    'use strict';

    $scope.doSubmit = function() {
        PersonService.persist($scope.person);
    };
});


The syntax on this is a little funky at first but I'll try and break it down.  The first 2 lines are basically "constructing" (not really, more like getting as far as I understand) the Controller and injecting the $scope and $routeParams objects directly from Angular.  As far as I understand, anything injected into the Controller's constructor function starting with a '$' is injected by Angular.  You're also injecting a PersonService object which is not prefaced with a '$' which means it was created by the developer. (This doesn't exist yet but you're going to create it here in a second).  Since everything is injected, any time you find yourself needing an Angular resource you can simply pass it in the constructor function and the Controller will have access to it magically.

You can see some binding from the View to the Controller here as well.  The "$scope.doSubmit()" function will be called on click from the View's submit button, and the person that we built up in the View is accessed in the doSubmit function as $scope.person.  The Controller and View share data through the Scope object, the default scope for the Controller is passed in as $scope.  I'm sure I'll find a better way to word this tomorrow and change up the article.

The last thing to do here is to create the actual PersonService object.  This will be responsible for actually making the Web Service call.  In your app/scripts/services directory create a file called PersonService.js.

angular.module('PersonServices', ['ngResource']).
  factory('PersonService', function($resource) {
    'use strict';

    /* the actual person json object */
    var person = {};

    /* the actual service to hit the server */
    var personService = {};

    var getResource = $resource('/rest/person/get/:id', {id:'@id'}, {
      get: {method:'GET'},
      query: {method:'GET', isArray:true}
    });

    var postResource = $resource('/rest/person/persist', {}, {
      persist: {method:'POST'}
    });

    /* retrieve the medical details from the server for the medical details htid */
    personService.getPerson = function(id) {
      person = getResource.get({id:id});
      return person;
    };

    personService.getPersonArray = function() {
      person = getResource.query({id:'all'});
      return person;
    };

    personService.persist = function(person) {
      postResource.persist(person);
    };

    return personService;
});


This is the actual PersonService Factory.  Angular creates Factories one time only, from now on anything needing access to the Person web services can inject this and access them.  This service object makes use of something in Angular known as a promise - I don't really have anything in the Java world to compare this too but you can kind of think of this similar to a Proxy object (but not really).  When your Controller calls the getPerson() or getPersonArray() methods, a promise object is returned while the resource goes off and makes the call to the server asynchronously.  Once the call finishes, it magically populates the promise object with the data from the server.  So, in your View classes you can use the promise object in each of your fields - when the call to the server starts they will all be blank, when the call to the server is finished your View will populate automatically.

As an implementation detail, in the 'PersonServices' Module constructor you have to pass in the 'ngResource' string as a parameter - this is absolutely mandatory for Angular's $resource object to work.  I think of this similar to a java import.

The last thing you have to do is edit the app.js file to include the CreatePersonView and CreatePersonController.

angular.module('angular.demoApp', ['PersonServices'])
  .config(function ($routeProvider) {

    'use strict';
    $routeProvider
      .when('/create', {
        templateUrl: 'views/CreatePerson.html',
        controller: 'CreatePersonController'
      })
});


Now you can create a new Person in the Create View:



And verify that they were created using the Web Service in a browser.




For anybody that's interested in the read section of this, the HTML and Controller code is below.

<div class="hero-unit">
  <div class="row">
    <div class="span2">First Name:</div>
    <div class="span3">{{person.firstName}}</div>
  </div>
  <div class="row">
    <div class="span2">Last Name:</div>
    <div class="span3">{{person.lastName}}</div>
  </div>
  <div class="row">
    <div class="span2">SSN:</div>
    <div class="span3">{{person.ssn}}</div>
  </div>
  <div class="row">
    <div class="span2">Gender:</div>
    <div class="span3">{{person.gender}}</div>
  </div>
  <div class="row">
    <div class="span2">Date of Birth:</div>
    <div class="span3">{{person.dateOfBirth}}</div>
  </div>
</div>

angular.module('angular.demoApp')
  .controller('ViewPersonController', function ($scope, $routeParams, PersonService) {
    'use strict';

    var id = $routeParams.id;
    $scope.person = PersonService.getPerson(id);
});


And the last thing you'd have to do is add the following lines to the app.js file
      .when('/person/:id', {
        templateUrl: 'views/ViewPerson.html',
        controller: 'ViewPersonController'
      })

And this will be your end result:



Saturday, January 19, 2013

Simple Skeleton Setup for a 2D Google PlayN Game


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);
}