Core Data ECS

As a native iOS developer I have found a wide range of amazing tools and libraries that are freely available. Some of this is designed toward a specific end goal, but much of it is very flexible and could even be used for a variety of purposes including game development. With this in mind, I decided to explore what an Entity Component System (ECS) might look like when backed by Core Data. So far I am enjoying the results, and welcome you to try it for yourself.

What is an ECS?

Entity Component System (ECS) is an architecture pattern that defines a way to build something complex using the combination of those three elements. You can think of an entity as a “thing” – it could represent a person, weapon, or even a non physical item like an event trigger. A component is used to describe a single aspect of an entity. A system controls a set of actions related to entities with a particular type of component or combination of components. For a more in-depth description of the pattern, I would recommend checking out Adam Martin’s work on the topic.

Apple has its own take on an ECS via GameplayKit. Although I wont be using their framework at present, I felt it still deserved an honorable mention.

Getting Started

You can grab a copy of the completed demo project here. For reference sake, it was created using Xcode Version 9.2 via the “Single View App” iOS Application template, using Swift 4, and with the “Use Core Data” checkbox enabled.

Data Model Editor

Select your Core Data model file – in my project it is named “CoreDataECS.xcdatamodeld”. Inside of the Core Data model editor, one may create something called “Entities” that define the structure of managed object models. In order to avoid confusion between a Core Data Entity, and the ECS Entity, I decided to use another naming convention. From now on, the ECS Entity will be called a “GameObject”, chosen in part because of my history with Unity. There were no naming conflicts with “Component”, so that convention will remain the same. For this demo, I have provided three Core Data Entities: GameObject, Component, and Stat.

The “GameObject” represents the “Entity” portion of our ECS. Functionally it serves as a sort of container for components. Normally, the ECS Entity should not hold any data of its own, but in this case I have added a “name” field with an attribute type of “String”. The GameObject’s name can be handy for debug purposes, or to quickly generalize what a “thing” will be.

In this implementation, the “Component” represents a pairing of a “GameObect” to some other kind of data which describes a unique aspect of the object. It has a single field called “representation” with an attribute type of URI. The value it will hold is the uriRepresentation of any other managed object’s objectID. According to Apple’s documentation, this representation is “an archiveable reference” and so should be just fine for our needs.

A relationship has been established between the “GameObject” and the “Component”. A GameObject has a “To Many” link to its components indicating that it can hold more than one component at a time. The inverse relationship is only “To One” indicating that a component can only exist on a single GameObject at a time.

The final Core Data Entity I included is for demo purposes only – it isn’t a required part of the architecture and merely represents some sample data of a component. I created something generic, called “Stat” which holds a String called “name” and an Integer called “value”. If you were making a role-playing game (RPG), a GameObject may include a variety of stats such as for its hit points, strength, magic, experience, etc. A player character may require a different combination of stats than an enemy monster, so by making Stats modular your game can allow for any configuration you wish.

Why?

You might be wondering a few questions at this point. First, why do you need a Component as a “middle-man” between a GameObject and the other Data types like our Stat? Probably lots of reasons, but here are a few off the top of my head:

  1. Greater flexibility. There are a variety of reasons where you may want to work with all of the components of an entity, and this allows you to grab them all, regardless of type, with a single property.
  2. Better clarity. Something about the way that each “potential” relationship would already be modeled into a GameObject bothers me. Perhaps because it might indicate that all GameObjects “should” actually have all of the aspects listed where in reality it may only have one.
  3. Reduced need of manual configuration. As you develop a large project, particularly a game, the amount of component types you will need will continue to grow. In Core Data, every relationship must be manually specified, have a “type” indicating whether more than one reference can be included or not, and also must have an inverse relationship. Granted, all of this would only have to be configured once, but it is still a lot of up-front effort, and requires you to treat each component type in the way that you define its relationship.

Since Core Data allows you to model Entities with a “Parent” Entity (basically class inheritance), why not just make every type of data a subclass of the Component? This is a particularly good question, and would actually have been my preferred approach, except for one small issue – a warning note in the documentation:

Be careful with entity inheritance when working with SQLite persistent stores. All entities that inherit from another entity exist within the same table in SQLite. This factor in the design of the SQLite persistent store can create a performance issue.

GameObject

Because I am using CoreData, you wont see a manual implementation of the GameObject class. However, there is functionality I would still like to add, so I created an “extension” file to serve this purpose. My first goal was to make it easy to create instances of the class:

[csharp]
class func create(name: String, moc: NSManagedObjectContext) -> GameObject {
let instance = NSEntityDescription.insertNewObject(forEntityName: String(describing: GameObject.self), into: moc) as! GameObject
instance.name = name
return instance
}
[/csharp]

Even though I said the “name” of a “GameObject” was really only for Debug purposes, I thought it might be convenient to be able to fetch GameObjects by name:

[csharp]
class func fetch(name: String, moc: NSManagedObjectContext) -> [GameObject] {
let request: NSFetchRequest = GameObject.fetchRequest()
request.predicate = NSPredicate(format: “name == %@”, name)
let results = try! moc.fetch(request)
return results
}
[/csharp]

Next I wanted an “instance” level method that would allow me to add a “Component” to a given “GameObject” in order to pair it with some data (any other Managed Object).

[csharp]
func addComponent(with data: T, moc: NSManagedObjectContext) -> Component {
let component = Component.create(data: data, moc: moc)
addToComponents(component)
return component
}
[/csharp]

Since we can “add” components now, it makes sense that we will also want to be able to “get” them later. By passing in the “type” of the data we are interested in, this method will be able to return an array of all of the components attached to the GameObject that pair it with the same type of data.

[csharp]
func getComponents(ofType dataClass: T.Type) -> [Component] {
guard let components = components else { return [] }
let description = String(describing: T.self)
let matches = components
.flatMap({ $0 as? Component })
.filter({ $0.representation?.absoluteString.contains(description) ?? false })
return matches
}
[/csharp]

We may also need the ability to remove a component and the data it points to. In this case, I made the last parameter optional, but defaulted to true, such that if we remove a component it will also delete the data that the component pointed to.

[csharp]
func removeComponent(with data: T, deletingData: Bool = true) {
let description = data.objectID.uriRepresentation()
guard let match = components?
.flatMap({ $0 as? Component })
.filter({ $0.representation == description })
.first else { return }
removeFromComponents(match)
managedObjectContext?.delete(match)
if deletingData {
managedObjectContext?.delete(data)
}
}
[/csharp]

In many cases, I prefer to forget that the “Component” is even there. Ideally we should just be able to get the data we want directly. In this case, I am able to return the data from the component with the first matching data type. For that I created another generic method:

[csharp]
func getData() -> T? {
guard let match = getComponents(ofType: T.self).first,
let data: T = match.fetchData() else { return .none }
return data
}
[/csharp]

Component

Much like I did for the GameObject, I wanted to add a couple of simple class level methods for convenience. First, I want to create a Component based on any other Managed Object. Note that I check to make sure the object id is not a temporary ID, because those will change when the context is saved.

[csharp]
class func create(data: T, moc: NSManagedObjectContext) -> Component {
let instance = NSEntityDescription.insertNewObject(forEntityName: String(describing: Component.self), into: moc) as! Component
if data.objectID.isTemporaryID {
try! moc.obtainPermanentIDs(for: [data])
}
instance.representation = data.objectID.uriRepresentation()
return instance
}
[/csharp]

Normally, I don’t want to think about the Component itself, but it may be a helpful step if I want to obtain a GameObject from the Data that was paired with it. So, I created a “fetch” method to allow me to pass any object id, and find a component with a matching representation:

[csharp]
class func fetch(managedObjectID: NSManagedObjectID, moc: NSManagedObjectContext) -> Component? {
let request: NSFetchRequest = Component.fetchRequest()
request.predicate = NSPredicate(format: “representation.absoluteString MATCHES %@”, managedObjectID.uriRepresentation().absoluteString)
let results = try! moc.fetch(request)
return results.first
}
[/csharp]

Given an instance of a Component, you will almost certainly want to fetch the data that it represents. If you know the generic type ahead of time, you can also cast it:

[csharp]
func fetchData() -> NSManagedObject? {
guard let url = representation,
let objectID = managedObjectContext?.persistentStoreCoordinator?.managedObjectID(forURIRepresentation: url),
let object = managedObjectContext?.object(with: objectID) else { return .none }
return object
}

func fetchData() -> T? {
let object = fetchData() as? T
return object
}
[/csharp]

Stat

In the same way as I created convenience factory methods and fetches for the GameObject and Component classes, I also added them for the Stat:

[csharp]
class func create(name: String, value: Int32, moc: NSManagedObjectContext) -> Stat {
let instance = NSEntityDescription.insertNewObject(forEntityName: String(describing: Stat.self), into: moc) as! Stat
instance.name = name
instance.value = value
return instance
}

class func fetch(name: String, moc: NSManagedObjectContext) -> [Stat] {
let request: NSFetchRequest = Stat.fetchRequest()
request.predicate = NSPredicate(format: “name == %@”, name)
let results = try! moc.fetch(request)
return results
}
[/csharp]

Demo

Don’t bother runing the project, all you will see is a blank white screen. To really “see” something working, try checking out the unit tests which are also included in the sample project. Besides seeing how to actually work with the code, it can be a little rewarding to run the tests and see a lot of green checkmarks appear.

Summary

Apple has created a variety of valuable resources. The Core Data framework is one such example, and one which until now I had not considered using in game development. Although the patterns I must use don’t match exactly to Adam Martins ECS, I have found the end result to be powerful, flexible, and a pleasure to work with.

Leave a Reply

Your email address will not be published. Required fields are marked *