GameplayKit is full of famous algorithms, design patterns and utilities broadly used in the game development realm. Right after Apple’s announcement, whispers started to spread: what if we could use this great toolbox for regular app development? Join Sash Zats in repurposing GameplayKit, as he demonstrates its uses with three different demos!
Hi there. When announced in WWDC, GameplayKit was revealed to have incredible technology within it. I don’t work on games typically, but it would be awesome if we could use these great GameplayKit bits to build non-game apps!
GameplayKit Goodies (1:11)
Components & Entities: GameplayKit describes Apple’s take on inheritance vs. composition. Often times they opt for inheritence, but since often times in games different classes share the same responsibilities in different combinations. With inheritence, this causes a mess of either multiple subclassing or a convoluted base class.
Apple’s take in GameplayKit is to use components and entities. For example, in a game, you would create components for firing, moving, and rendering on the screen, and any class that uses those simply adds those components. More often than not, we end up with massive view controllers in which the majority of our code is located. Components can be an effective solution to this issue, creating reusable code that you can distribute and use as necessary.
Random Numbers: GameplayKit includes good things for randomness. I implemented shuffling of array at least twice in my life, and I do not want to do it again. These great classes that come with GameplayKit provide standard, shuffled, and gaussian distributions, and more complex random functions.
State Machines (3:28)
It’s 2015 and no one likes the dreaded “S” word. Nevertheless, these do have important applications. I found the article GameplayKit: State Machine for non-game Apps by Vicente Vicens, who explored how you can reuse state machine in normal apps. GameplayKit’s state machines represent Apple’s vision of state machines as nice, simple and understandable.
Nevertheless, in real life state machine, every single state that you add would exponentially increase the complexity of the state machine.
A specific example of a state machine is the Markov Chain. It models a system that can transition from one state to another with certain probability. It has been widely used to model weather conditions, stock exchange, Twitter bots, and even iOS’s predictive keyboard.
Markov Chain - Swift 3.0 Generation (5:20)
My first demo is an API that has yet to be announced. The app generates new, secret Swift 3.0 API using the + button, creating a new class with a description.
class MarkovChainMachine: GKStateMachine {
let outcomesMap: [[GKState]: [Double: GKState]]
var buffer: [GKState]
func enterNextState() {
let next = nextState()
enterState(next)
buffer.removeFirst()
buffer.append(next)
}
func nextState() -> GKState {
let random = …
return nextState(buffer, random)
}
}
A Markov Chain is a state machine in Soviet Russia: you don’t tell it which state to enter. It tells you which state it wants to enter. To accomplish this, you have to train it with an existing data set giving it an outcomes map. Given a particular array of state, an outcomes map determines what are the possible outcomes, where can I go to, and what are probabilites of me going into one of the states.
After you train it, you can then ask it: “Where do you want to go? What is the state that you want to enter?” The buffer created in code is the memory of the state machine. The buffer contains the 5 previous states that it has been to, and it uses this information to look up what are the possible states going forward. Then it chooses one of them, following whatever random number generation that you chose. Then, by using a circular buffer (dropping the first element and appending the new one), a Markov Chains can generate test data. For instance, you can train your Markov Chains to generate people’s names following the same probabilities that are observed in real life.
MinMax Demo: Smarter App AI (7:49)
MinMax is an algorithm that has been used for turn-by-turn games such as chess. It has been used to calculate to make AI smarter and more believable. It works by predicting all the possible outcomes from the current state. It calculates what is the best possible scenario that will lead you to win, and the computer to lose.
Sol is an open-source weather app that we can improve through a more helpful AI. I do not like those common onboarding tutorials that force the users to swipe between five different screens just to start the app, and this MinMax approach could help improve that experience with a more humane guide.
To do so, I used MinMax in order to judge what’s the next best action for a user to take based on previous actions he has taken. For instance, if the user has no cities add in the weather app, MinMax will determine that the next best action is to add a city, and prompt you to do so. Once you added a few cities, it will guide you through navigating between them, removing them, changing the temperature scales, and more because it remembers your previous actions and determines the next best one based on information you provide it.
suggestions.register(AddCity.self) { history in
switch history.filter({ $0 is AddCity }).count {
case 0:
return best
case 1..<3:
return good + 1
default:
return nil
}
}
When I am initializing the app, I am adding a set of predicates. This is one of them: it tells me to add city. I am adding a predicate that looks at the history of user actions, and if there were no actions of type of add city in the history - adding the city is the best thing that you can possibly do. After that, if user added at least three cities, he knows what he is doing. I do not need to remind him how to do that.
var history: [GKState]
// GKGameModel
func scoreForPlayer(player: GKGameModelPlayer) -> Int {
var maxScore = Int.min
for predicate in predicates {
if let result = predicate.score(history: history) {
if maxScore < result {
maxScore = result
}
}
}
return maxScore
}
The MinMax takes your model, which represents the current state of your app, and takes in all possible states at the same time, and asks you to rate each state using what you think is right. You just tell to the system what are the possible outcomes and rules, register a predicate, and evaluate it, and you can easily create a smart tutorial system that guides users with things to do.
Pathfinding (13:11)
Pathfinding is a set of algorithms that helps characters in games get from point A to point B avoiding obstacles and monsters. I saw a super clever implementation of this by a guy who hacked into Mario and calculates all possible moves, choosing the best one. By creating this ultra smart Mario, he was able to get an amazing high score.
For practical usage, I implemented pathfinding in order to improve in-app navigation in one of our favorite app that confuses me often: Settings.app. Because of the odd heirarchy, there are multiple ways to get to a single setting screen. To possibly improve this, I thought of adding a favorite screen feature that would automatically navigate me there with the press of a button.
Nevertheless I did not want to use to leverage the fact that we are using navigation control, and just to push the favorite screen on top of the stack — that would be cheating, and the user would be confused when pressing back. Instead by using pathfinding, pressing the favorite button navigates you through all the screens through the shortest path to where you want to be.
func setupGraph() {
root.addConnectionsToNodes([privacy, facebook], bidirectional: true)
facebook.addConnectionsToNodes([facebookSettings, facebookAccount], bidirectional: true)
facebookSettings.addConnectionsToNodes([facebookLocation], bidirectional: true)
privacy.addConnectionsToNodes([bluetooth, location], bidirectional: true)
location.addConnectionsToNodes([facebookLocation], bidirectional: true)
graph.addNodes([
root, privacy, facebook,
bluetooth, location,
facebookSettings, facebookLocation, facebookAccount
])
favorite = facebookLocation
}
Because of all the bookkeeping, this code may seem a bit scary, but easy once you examine closely. We have to register all the nodes as each screen is being represented by a node in the graph. First, we tell to the system that you can go from one specific node to another specific node, and you can also tell that it is bi-directional (you may come back the same way).
func goToFavoriteNode() {
let current = currentViewController.node
let path = root.findPathFromNode(current, toNode: favorite)
navigate(path)
}
Then, those are all the nodes - goToFavoriteNode()
leads me to the target node that I want to get to as my favorite settings screen. goToFavoriteNode
takes the currentViewControl that you are at and node associated, and it is asked pathfinding algorithm to get the shortest path. Then, the navigation stack follows this path to take you straight to the screen.
This can be handy for complex apps such as Facebook with multiple screen heirarchies that can be confusing. Also, if you have nonlinear history of changes through an undo manager, you can use this pathfinding algorithms to get from one state of your app to another state much more easily - the algorithm will figure it out for you.
Conclusion: Get Creative (17:16)
Fighting Xcode and development tools all day long, we forget about the creative aspect of software development that allows us to enjoy finding simple solutions to complex problems. Often times, exploring around to find new ways of tackling tasks helps us be better programmers and make more out of our code.
Receive news and updates from Realm straight to your inbox