I’d like to continue moving straight through the flow to the setup screen, but there are several pre-requisites we will need to address first. Among them is the creation of a database connection that can be easily consumed by all of our systems. After connecting to the database, there are a few things we can do to make fetching and working with its data a little easier. These will be implemented as extension methods.
Data Controller
Update the script located at Scripts/Controller/DataController.cs with the following:
using System; using System.Collections; using System.Collections.Generic; using UnityEngine; using SQLite4Unity3d; [Serializable] public class DataController { #region Fields public static readonly DataController instance = new DataController(); public DatabaseController pokemonDatabase; #endregion #region Public public void Load (Action complete) { pokemonDatabase = new DatabaseController("Pokemon.db"); pokemonDatabase.Load(SQLiteOpenFlags.ReadOnly, delegate(DatabaseController obj) { if (complete != null) complete (); }); } #endregion #region Constructor / Destructor private DataController () { } ~DataController() { pokemonDatabase.connection.Close(); } #endregion }
There are a number of “System” classes that will need easy access to our data. Therefore, I created this single “DataController” class to be responsible for all of our app’s data, and made it a “Singleton” for easy accessibility. This pattern ensures that I always have exactly one instance of a class – the shared instance is “static” and “readonly” so that everything can see it, but nothing can destroy it. The “constructor” is also “private” so that other classes can’t create new instances. Having only one instance means I only need to maintain one connection to the database, and only create one copy of any future data it will hold such as the game or battles, etc.
This class is marked as [Serializable]. At present you won’t see the benefits of this step, but in the future when we add more data models you will be able to see and even tinker with the values it holds. Normally you can’t see a static object like this in the Unity inspector, but for debug purposes, I will give the “FlowController” a direct reference to the shared instance so that it can appear.
Remember that the “Load” of a database connection is potentially an async call – on some platforms like Android it may not complete immediately. In order to make sure that everything is available before I attempt to use it, we can perform the load method with a completion block. We will update the “FlowController” in a moment to initiate this process and then set its first state at the completion callback.
Flow Controller
As I mentioned earlier, the “FlowController” will need a couple of small updates. First, we will add a reference to the “DataController” singleton so that its data will be inspectable within Unity. We will add a serializeable field for this and assign it in the “Start” method. Next, we need to instruct the database to begin loading, and to not initiate our flow until after it has completed:
[SerializeField] DataController dataController; void Start () { dataController = DataController.instance; dataController.Load (delegate { stateMachine.ChangeState(IntroState); }); }
Component Extensions
Now that we have a way to access the database connection anywhere, it will be easy to add some additional extensions for our components. Basically for each component I want to add an extension method that goes both ways: to get an entity reference from the component and to get a component(s) reference from the entity. For example, here is the extension for the “Encounterable” component:
public static class EncounterableExtensions { public static Encounterable GetEncounterable (this Entity entity) { var connection = DataController.instance.pokemonDatabase.connection; var entityComponent = connection.Table<EntityComponent>() .Where(x => x.entity_id == entity.id && x.component_id == Encounterable.ComponentID) .FirstOrDefault(); if (entityComponent == null) return null; var component = connection.Table<Encounterable>() .Where(x => x.id == entityComponent.component_data_id) .FirstOrDefault(); return component; } public static Entity GetEntity (this Encounterable item) { var connection = DataController.instance.pokemonDatabase.connection; var entityComponent = connection.Table<EntityComponent>() .Where(x => x.component_id == Encounterable.ComponentID && x.component_data_id == item.id) .FirstOrDefault(); var entity = connection.Table<Entity>() .Where(x => x.id == entityComponent.entity_id) .FirstOrDefault(); return entity; } }
I added this code in the same script as the “Encounterable” class, still inside the ECS namespace, but outside the model’s class. For the “Evolvable” and “SpeciesStats” you can actually copy and paste the entire extension, and then use Monodevelop’s “Replace” (from the menu bar choose “Search->Replace…”) feature to swap out the class names. Getting the “Move” components requires something new, because we want to fetch a list of objects instead of just one:
public static List<Move> GetMoves (this Entity entity) { var connection = DataController.instance.pokemonDatabase.connection; var entityComponents = connection.Table<EntityComponent>() .Where(x => x.entity_id == entity.id && x.component_id == Move.ComponentID); if (entityComponents == null) return null; List<Move> retValue = new List<Move>(); foreach (EntityComponent ec in entityComponents) { var component = connection.Table<Move>() .Where(x => x.id == ec.component_data_id) .FirstOrDefault(); if (component != null) retValue.Add(component); } return retValue; }
While we are at it, I’ll also provide some extensions for the “Type” and that will complete our ECS extensions:
public static class TypeExtensions { public static Type GetPrimaryType (this SpeciesStats stats) { return GetType(stats, stats.typeA); } public static Type GetSecondaryType (this SpeciesStats stats) { return GetType(stats, stats.typeB); } static Type GetType (SpeciesStats stats, int id) { var connection = DataController.instance.pokemonDatabase.connection; var match = connection.Table<Type>() .Where(x => x.id == id) .FirstOrDefault(); return match; } public static List<SpeciesStats> StatsWithType (this Type type) { var connection = DataController.instance.pokemonDatabase.connection; var matches = connection.Table<SpeciesStats>() .Where(x => x.typeA == type.id || x.typeB == type.id); var result = new List<SpeciesStats>(matches); return result; } }
The “SpeciesStats” component held the “Type” id’s for an entity, so in this case we will use that to get a type or vice versa. When creating gym’s I will randomly choose to use a certain type of Pokemon like the “Bug” type to populate it, so here I can query for all of the species of a certain type.
Demo
Now that we have implemented the data controller and some extensions, I’d like to demonstrate how they can be used. Create a new scene and script for this demo and then attach the script to an object in the scene. Copy the code below into your demo script:
using System.Collections; using System.Collections.Generic; using UnityEngine; using ECS; using System.Text; public class PokemonDatabaseDemo : MonoBehaviour { void Start () { DataController.instance.Load (DoStuff); } void DoStuff () { var connection = DataController.instance.pokemonDatabase.connection; var table = connection.Table<Entity> (); Debug.Log ("Found items " + table.Count().ToString()); foreach (Entity entity in table) { StringBuilder sb = new StringBuilder (); sb.AppendLine ("Loaded entity named: " + entity.label); var encounterable = entity.GetEncounterable (); if (encounterable != null) { sb.AppendLine ("encounterable rate: " + encounterable.rate); } else { sb.AppendLine ("not encounterable"); } var evolvable = entity.GetEvolvable (); if (evolvable != null) { sb.AppendFormat ("evolves into: {0}, with a cost of: {1}\n", evolvable.entity_id, evolvable.cost); } else { sb.AppendLine ("can't evolve"); } var moves = entity.GetMoves (); sb.AppendLine ("Can use the following moves:"); foreach (Move m in moves) { sb.AppendLine (" " + m.name); } var stats = entity.GetSpeciesStats (); sb.AppendFormat ("Attack: {0}, Defense: {1}, Stamina: {2}\n", stats.attack, stats.defense, stats.stamina); var type1 = stats.GetPrimaryType (); sb.AppendLine ("Type 1: " + type1.name); var type2 = stats.GetSecondaryType (); sb.AppendLine ("Type 2: " + type2.name); Debug.Log (sb.ToString ()); } Debug.Log ("Done"); } }
This script shows how to loop over all of the entities in our database and then for each entity it attempts to load each of our different types of components and display some of its values. Remember that not all of the entities will have each of the components, so in some cases I had to check for null after attempting the fetch.
Assuming you have successfully populated your database with some data, running the scene should produce output that looks something like the image below. If your console window isn’t already visible you can open it through the menu bar Window -> Console. You can select the various logs to see an expanded output in the lower section of the window.
Summary
In this lesson we created a shareable data controller using the Singleton architecture. I explained a bit about how that pattern is implemented and why it is useful. Then we created several extension methods for the components of our database. These components can make it far easier to fetch an entity from a component or to fetch component(s) from an entity. Likewise, I added some methods to help fetch other data such as the Types of a Pokemon species.
Don’t forget that there is a repository for this project located here. Also, please remember that this repository is using placeholder (empty) assets so attempting to run the game from here is pretty pointless – you will need to follow along with all of the previous lessons first.
I am having issues where the pokemon DB isn’t opening for the demo. it says it can;t be open from the streaming assets folder. I did import it from the reference project and then added in the data but I think it should work.
I can’t be certain why, but one option that could help is to use the reference project to work from. If you use version control software such as SourceTree, you can look at the project as it was at any step of the way. Checkout a commit related to this lesson and then work from there.