In this lesson we will start by creating a class to manage the connection to our database. Then we will create a few models to represent the data we want to work with. Once all of that is in place, we will finish by creating a short demo that will allow us to grab an entity by typing its ID. The demo will then tell us information about our entity such as its label and the kinds and counts of the components it contains.
Data Manager
Copy the code snippet below into a new code file as the starting place for our Data Manager:
import Foundation import SQLite class DataManager { // MARK: - Fields private var database: Connection! }
The Connection class comes from the SQLite library we installed via Cocoapods in the previous lesson. Our manager will maintain this database connection and by using it we will be able to execute a variety SQL statements including fetching, and updating data etc. We won’t actually be required to write any SQL directly and instead can pass chainable helper functions and expressions from which the SQLite library will build executable SQL statements.
// MARK: - Public func prepare(table: Table) -> [Row] { do { let result = try database.prepare(table) return Array(result) } catch { } return [] } func run(update: Update) { do { try database.run(update) } catch { } }
Using the database connection we can fetch (the “prepare” method) and update (the “run” method) data in our database. Since an attempt to access the database can throw errors, I created some helper methods that already wrap access with the “do”, “try”, and “catch” commands so I wont have to clutter up the rest of my code with them. In addition I went ahead and created an array from the results of a fetch so I have more options on enumeration, filtering, etc.
// A path to the default database included in the app bundle private var defaultDataPath: String { get { guard let retValue = NSBundle.mainBundle().pathForResource("Zork", ofType: "db") else { fatalError("Invalid path to default database File") } return retValue } } // A path to a saved game in progress private var userDataPath: String { get { guard let documentsPath = NSSearchPathForDirectoriesInDomains(.DocumentDirectory, .UserDomainMask, true).first else { fatalError("Cannot access documents directory") } let retValue = NSString(string: documentsPath).stringByAppendingPathComponent("Zork.db") return retValue } } // A path to an un-saved game in progress private var tempDataPath: String { get { let retValue = NSString(string: NSTemporaryDirectory()).stringByAppendingPathComponent("Zork.db") return retValue } }
Currently our database only exists as a project resource. When your project builds it will be included in the application’s main bundle. While located there, you are restricted to write only access, which of course isn’t suitable for our needs. What we need to be able to do is copy the database to a temporary location which does allow both reading and writing. In the event that we want to save, we can then copy to a more permanent but still editable location. When loading a game, we will check that alternate location for a file first, and only copy from the application resource when one isn’t found, or when we intentionally want to reset and start a new game. The three string properties above show the paths we will use for each of these three potential database locations.
func save() { do { let userDataURL = NSURL.fileURLWithPath(userDataPath), tempDataURL = NSURL.fileURLWithPath(tempDataPath) var newURL: NSURL? try NSFileManager.defaultManager().replaceItemAtURL(userDataURL, withItemAtURL: tempDataURL, backupItemName: .None, options: .UsingNewMetadataOnly, resultingItemURL: &newURL) load() } catch { } }
The “save” method creates a file system URL from our “user data path” and “temp data path”. Then it attempts to replace any item appearing at the “user data path” with whatever our database currently looks like in the temporary directory. According to the documentation this handy method will insure no data loss occurs. I suppose this means that if something should go wrong while attempting to overwrite a previous save file (suppose the game crashes while saving for example), then at least it wont have corrupted your old save file.
func load() { // If the user has a game in progress, start with that, otherwise begin from the default database let path = NSFileManager.defaultManager().fileExistsAtPath(userDataPath) ? userDataPath : defaultDataPath // Create a copy of the database from the bundle to the users documents folder if necessary do { if NSFileManager.defaultManager().fileExistsAtPath(tempDataPath) { try NSFileManager.defaultManager().removeItemAtPath(tempDataPath) } try NSFileManager.defaultManager().copyItemAtPath(path, toPath: tempDataPath) } catch { fatalError("Unable to copy database") } // Connect to the copied database so we are not restricted by read-only mode do { database = try Connection(tempDataPath) } catch { fatalError("Unable to connect to database") } }
The “load” method selects a path to copy from based on whether or not a previous saved game exists. When a game is in progress it will use the user data path, otherwise it will use the default data path. Next, we copy the database at the indicated location into a temporary directory. This way, a user can play the game without actually commiting updates – perhaps they wanted to test a “stupid” path out of curiosity but didn’t want it commited as part of their real play through. It also acts sort of like a buffer to help keep the real database from getting corrupted. Assuming the database is copied successfully, we use the SQLite library to make a read / write connection to it.
func reset() { do { if NSFileManager.defaultManager().fileExistsAtPath(userDataPath) { try NSFileManager.defaultManager().removeItemAtPath(userDataPath) } } catch { } load() }
The “reset” method deletes a user’s save file (if it exists) and then calls “load” which will end up copying the database from the main bundle. This way, the game will be reset to the state it would have been at in a new game.
// MARK: - Singleton static let instance = DataManager() private init() {}
Many classes are going to need access to the data in the database. In addition, this class is maintaining its own fields and properties, so in my opinion the Singleton pattern is again appropriate for this scenario.
Models
Now that we have connected to the database, we will need a way to start actually using it. Although we could access data in the database using strings which map to tables and columns, it isn’t a great idea. It would be easy to introduce errors by typos and mis-spelled or mis-remembered labels. In order to get compile time checking, code hints, etc, it can be a great idea to create a class or struct for each type of data you have created.
Entity
Copy the following into a new code file in order to implement the data structure for the “Entity” table.
import Foundation import SQLite struct Entity { // MARK: - Fields static let table = Table("Entities") static let entity_id_column = Expression<Int64>("id") static let label_column = Expression<String?>("label") let row: Row // MARK: - Properties var id: Int64 { get { return row[Entity.entity_id_column] } } var label: String? { get { return row[Entity.label_column] } } }
I decided to make an Entity a struct for efficiency sake. There are a few static fields listed here. First is a “Table” to which I can append other expressions and filters to find the specific entities I want to use. Next are “Expressions” which are used like keys to help get the data I want from a “Row” of content that was returned in a fetch.
If you look at the Zork.db file you can see that the Entity table had two columns, one was a primary key called “id” which was based on an integer, and another called “label” which was based on text. The “Expression” is built using a generic syntax where “Int64” represents the integer and “String” represents the “text”. In addition you can use the “Bool” generic expression to wrap an “integer” which will be either “0” (false) or “1” (true). No matter what type of data you are storing in the database you can choose whether or not you intend to support nullable data. If so, you need to use the optional (“?”) version of the equivalent data type in swift. For my implementation I decided that the “id” should never be “null” but it would be okay to have a “null” label.
Because I implemented the Entity as a “struct” you are required to use an initializer that configures all of the fields. I only actually store the “Row” of a table from which you can ask for any of the column’s data. I created properties that wrap the column names using the static expressions.
Component
Copy the following into a new code file in order to implement the data structure for the “Component” table.
import Foundation import SQLite struct Component { // MARK: - Fields static let table = Table("Components") static let component_id_column = Expression<Int64>("id") static let name_column = Expression<String>("name") static let description_column = Expression<String>("description") static let table_name_column = Expression<String>("table_name") let row: Row // MARK: - Properties var id: Int64 { get { return row[Component.component_id_column] } } var name: String { get { return row[Component.name_column] } } var description: String { get { return row[Component.description_column] } } var tableName: String { get { return row[Component.table_name_column] } } }
This looks very similar to the implementation of the “Entity” – I created static fields for the Table, and for an expression that maps to each column of the relevant table just like we did before. I also created properties to wrap the access to the row’s columns.
Entity Component
Copy the following into a new code file in order to implement the data structure for the “Entity Component” table.
import Foundation import SQLite struct EntityComponent { // MARK: - Fields static let table = Table("EntityComponents") static let id_column = Expression<Int64>("id") static let entity_id_column = Expression<Int64>("entity_id") static let component_id_column = Expression<Int64>("component_id") static let component_data_id_column = Expression<Int64>("component_data_id") let row: Row // MARK: - Properties var id: Int64 { get { return row[EntityComponent.id_column] } } var entityID: Int64 { get { return row[EntityComponent.entity_id_column] } } var componentID: Int64 { get { return row[EntityComponent.component_id_column] } } var dataID: Int64 { get { return row[EntityComponent.component_data_id_column] } } }
Again, this looks very similar to the implementation of the “Entity” – I created static fields for the Table, and for an expression that maps to each column of the relevant table just like we did before. I also created properties to wrap the access to the row’s columns.
Fetching Data
To stay true to Adam Martin’s training, my entities and components etc are represented as simple structs – they are just data. All of the methods for components are supposed to appear in a System. In this case, I wanted to create a few simple static methods (for fetching) which are related to the specific data type, so it made sense to me to create them as extensions within the same data type namespace. It would be very easy to refactor these into a separate system if you wanted, or felt it was a more correct implementation of an ECS.
Note that you can place these extensions anywhere you like. They could appear in a new code file, or could be appended to an existing file. For now, I left the extension in the relevant class (i.e. an Entity extension appears in the same code file as the Entity struct – but outside of the struct itself).
extension Entity { static func fetchByID(id: Int64) -> Entity? { let table = Entity.table.filter(Entity.entity_id_column == id) guard let result = DataManager.instance.prepare(table).first else { return .None } return Entity(row: result) } static func fetchByComponentID(componentID: Int64, dataID: Int64) -> Entity? { let componentTable = EntityComponent.table.filter(EntityComponent.component_id_column == componentID && EntityComponent.component_data_id_column == dataID) guard let result = DataManager.instance.prepare(componentTable).first else { return .None } let component = EntityComponent(row: result) return Entity.fetchByID(component.entityID) } static func fetchAllByComponentID(componentID: Int64, dataID: Int64) -> [Entity] { let componentTable = EntityComponent.table.filter(EntityComponent.component_id_column == componentID && EntityComponent.component_data_id_column == dataID) let matches = DataManager.instance.prepare(componentTable).map({ $0[EntityComponent.entity_id_column] }) let entityTable = Entity.table.filter(matches.contains(Entity.entity_id_column)) let result = DataManager.instance.prepare(entityTable).map({ Entity(row: $0) }) return result } }
In the first extension method, “fetchByID”, I have provided a way to fetch an “entity” by its unique id. On the first line we filter the table of Entities using a predicate which requires an id match. The syntax here may look a little misleading at first because you might interpret this as passing the result of a boolean comparison – is the “entity_id_colum” equal to the “id” parameter that we passed? It may feel a bit like magic but it will use this expression for each entry of the table to select only the items which match. Since the “id” is a primary key there can only be up to one match. Keep in mind that you could still pass an “id” which doesn’t exist in the database, so I decided to make the return type an optional.
In the second extension method, “fetchByComponentID:dataID”, I have provided a way to fetch the entity to which a component is attached. You could think of this as the inverse relationship for an entity’s component. Note that it is technically possible to have a component shared on more than one entity, so it will be up to you to use this only where it is appropriate. It is also possible for a component to not currently be attached to an entity, therefore I also made this method return an optional.
In the last method, “fetchAllByComponentID:dataID”, I have covered a scenario where a single component can be shared by many entities. One example for this is with the use of “noun” and “adjective” components. The same word can be used to describe potentially many game objects, and rather than have duplicate words in the database, I felt that it made more sense to approach it as a shared component instead.
extension Component { static func fetchByID(id: Int64) -> Component? { let table = Component.table.filter(Component.component_id_column == id) guard let result = DataManager.instance.prepare(table).first else { return .None } return Component(row: result) } }
Similar to the extension method for an entity, I felt that we may want to fetch a component by its ID. I added this extension to the “Component” code file.
extension EntityComponent { static func fetchByEntityID(entityID: Int64, componentID: Int64) -> [EntityComponent] { let table = EntityComponent.table.filter(EntityComponent.entity_id_column == entityID && EntityComponent.component_id_column == componentID) let result = DataManager.instance.prepare(table).map({ EntityComponent(row: $0) }) return result } }
The “fetchByEntityID:componentID” method allows you to grab all of a specific type of component that are attached to a specified entity. In many cases you may expect there to only be a single occurence of any particular component type, but it is easy to grab the “first” element of the array when that is the goal.
extension Entity { func getComponents(componentID: Int64) -> [EntityComponent] { return EntityComponent.fetchByEntityID(id, componentID: componentID) } func getComponent(componentID: Int64) -> EntityComponent? { return EntityComponent.fetchByEntityID(id, componentID: componentID).first } }
Unlike the previous extension methods, these last two are implemented to feel like instance methods even though they are actually static. I don’t know how Adam would feel about this sort of approach, since from what I have read he tends to shy away from OOP. Personally I like the way it feels to be able to ask an entity for its components rather than have to ask a system for the components of an entity. Its a subtle difference but one which I feel keeps things more streamlined.
Demo
The following code is for demo purposes only and wont exist in the final project. It allows you to verify that you are able to connect to the database and fetch an entity and its components. Add the following snippet to the GameViewController:
// TODO: Delete demo code extension GameViewController { private func examine(text: String) { guard let entityID = Int64(text), entity = Entity.fetchByID(entityID) else { LoggingSystem.instance.addLog("There is no entity with the id: \(text)") return } if let label = entity.label { LoggingSystem.instance.addLog("This entity has the label: \(label)") } for i in 1...14 { let componentID = Int64(i) guard let component = Component.fetchByID(componentID) else { continue } let components = EntityComponent.fetchByEntityID(entityID, componentID: componentID) if components.count > 0 { LoggingSystem.instance.addLog(" * \(components.count) \(component.name) component(s)") } } } }
This method will take a string of text and convert it into an integer from which we will attempt to fetch an entity from our database. If we find a match, then it adds information about the entity to the logging system. First it will add the entity’s debug label, then it will loop through all of the types of components and check whether or not they are present on the entity. If so, it will log how many of that component are attached.
Add the following line of code to the “textFieldShouldReturn” method just after the line where we submit the typed message to the LoggingSystem:
examine(input)
One more step and we’re done. We need to make sure the database connection has been created so we will need to tell our data manager to load. I will use the viewDidLoad method for this:
override func viewDidLoad() { super.viewDidLoad() DataManager.instance.load() }
Now you can build and run the project. Type in any integer id and you should see output like the following:
Summary
In this lesson we created a manager for our database. The manager handles the connection through the SQLite library and allows us to fetch and update the data inside. This manager also handles the ability to save and load saved games or start a new game as the user so desires.
We also created the first of several models we will need. Ultimately we will want one model per table in the database so that we have a strongly typed way to refer to all of the data it contains. The patterns we used here will be reused for all future models.
We created some extension methods, which could be a stand-alone system, which allow us to perform specific fetches based on the entity, component, and entity component data that we have created.
Finally, we created a quick demo to verify that everything actually works. By typing an id for an entity we can fetch it and all of the components it contains. We print all of the details to the screen (much like we will for actual gameplay) via the Logging System.
Don’t forget that this project is accompanied by a repository HERE. There will be one commit per lesson so you can see exactly how the project would have been cofigured and how the code would look at each step.