If you are a Kotlin developer, there is a high chance you already know that coroutines are the new way of composing your asynchronous, concurrent and parallel workflows. It is also the recommended solution by Google to use for asynchronous programming in Android.
In this blog post I will discuss the importance of asynchronous architecture. I will also explain how I combined Ably and Kotlin coroutines to develop a multiplayer game room SDK.
Asynchronous message architecture
These days it’s a rare occasion to see applications that don’t contain asynchronous code as it is used for all sorts of reasons. Running an expensive computation in the background, reading from a disk without blocking the user interface, retrieving data from a network resource are just some common use cases where asynchronous programming models are used.
Point of contacts, message passing and event handling
It is helpful to draw a clear picture of an application that runs asynchronous code to make sure that it runs correctly. For an application that downloads a file in the background, it’s critical to notify the user exactly when the download has finished, not before, some time after or in the middle of the process.
If you isolate your application’s message flow architecture, you can imagine a network-like structure where messages are transmitted through edges and points of contacts that act as message processors and sometimes as message forwarders.
Message passing between different components is generally dealt with using interfaces and lambda functions because, for a strongly typed language like Kotlin, they provide the necessary type safety and establish a consistent contract when communicating between two or more components.
In event-driven architectures, event handling is the essential function in composition of asynchronous applications. As events are the points where program control flows are effected, it's also important to have a reliable event flow.
How do coroutines help?
Kotlin Coroutines provide convenient constructs, functions and primitives to deal clearly with complicated asynchronous workflows:
- They are a good alternative to Java threads that are more costly to create and more complicated to manage.
- Coroutine scope creates a predictable life cycle and execution context for work that run inside it.
- Coroutine Job makes it easy to maintain related / dependent work through its hierarchical structure.
- Suspending functions allows developers to call asynchronous functions in a sequential manner.
- Coroutine channel is a primitive where streaming of multiple values is the case. * Flows provide higher level APIs that simplify asynchronous stream flows.
There are a lot more features of coroutines that are outside of the scope of this post.
How does Ably fit into the equation?
Ably provides edge messaging infrastructure that follows the publish / subscribe pattern to deliver data at low latency. You can use Ably in use cases where realtime data is important.
Multiplayer games need a reliable and fast messaging layer for good performance. Sending a message to a player, joining a room and sending over game data - all this has to happen as quickly as possible for minimal latency. We also want to focus on developing the functionality of our own multiplayer game rooms. Developing a scalable infrastructure for end-to-end messaging is not our priority. Let’s use Ably for that.
Realtime architecture and the bigger picture
If you take a look at the overall architecture of an application that has a realtime layer behind it, you will see that the application itself has evolved into something bigger and more capable than its individual nodes. This brings the need to have more cohesive abstractions between different actors in the network.
For example, in a multiplayer game room scenario when someone joins the game, you may want to invite them into a game session. There would be a publisher-subscriber relationship between the game events and the application logic, and message flow architecture would necessitate consideration of both publisher and subscriber details.
Ably uses channels to maintain communication between publishers and subscribers. In our case our actors can be both subscribers and publishers.
- When a player enters the game or sends a message to another player, we play the publisher role. We are publishing to interested actors about our actions.
- When we want to observe what is happening in the game, for example observing people entering into the game, registering to receive messages from other players, we are playing a subscriber role.
While Ably improves the external communication through channels, coroutines simplify and improve the internal communication between asynchronous interfaces. Ably channels provide a generic structure so that they can be used by multiple applications for multiple reasons. By using coroutines we use similar semantics for internal and external communication, which helps us build the bigger picture.
Message flow / translation
Message translation is almost inevitable for any sized application. Whether you receive a message from a network, from a local database or from your local file store, you will almost always have to translate the messages to serve the domain clients. Reactive programming paradigm gained popularity as the need to simplify design of those message workflows. In our case we will translate between Ably channel messages and our game concepts.
How to use Ably
Ably provides client side SDKs to make it simple to use. We will use ably-java to access Ably in a Kotlin Project.
Asynchronous Ably functions are callback-based. We will use suspendCoroutine
to turn them into suspending functions and transform them into our domain models. This will simplify our APIs by exposing our domain models, while allowing asynchronous functions to be called in a sequential manner.
Project objective
The overall objective of our project is to create an SDK for multiplayer game room scenarios. We want to create simple to use interfaces for game developers by abstracting some channel concepts into game concepts like GameRoom
, GamePlayer
, GameMessage
.
We also want to abstract communication scenarios into easier to use functions such as sendMessageToPlayer
, enterRoom
. Additionally we want to take advantage of Kotlin coroutines for internal message communication.
You can check the code I created in the GameRoomSDK repo on Github for documentation and full implementation of the SDK which comes with an example Android application. You can also use this SDK for your Kotlin based multiplayer games for game room scenarios.
In the following, I will explain three common scenarios to explain how we
- Transformed callback based functions into suspending functions
- Translated Ably models to our domain models.
Let's get started...
Send a message to a player
'Sending message to a player' is a case where a message is sent and a result is retrieved back only once. We can use the following code to transform such a function into a suspending one.
override suspend fun sendMessageToPlayer(from: GamePlayer, to: GamePlayer, message: GameMessage):
MessageSentResult {
val ablyMessage = message.ablyMessage(from.id)
val channelId = unidirectionalPlayerChannel(from, to)
println("Sending message over $channelId")
return suspendCoroutine { continuation ->
ably.channels[channelId]
.publish(ablyMessage, object : CompletionListener {
override fun onSuccess() {
continuation.resume(MessageSentResult.Success(to))
}
override fun onError(reason: ErrorInfo?) {
continuation.resume(
MessageSentResult.Failed(
toWhom = to, exception = AblyException
.fromErrorInfo(reason)
)
)
}
})
}
}
- By using
suspendCoroutine
we transform a callback based function into a coroutine. - We translate our domain objects to Ably channel objects. As you can see
GamePlayer
toGamePlayer
communication turns into a unique Ably channel. While we translateGameMessage
to Ably'sMessage
. We also useMessageSentResult
as a return type, which is a sealed class. Sealed classes are perfect candidates to use for internal message communication as they make code flow more intuitive.
You can call this function as follows:
someCoroutineScope.launch {
val result = controller.sendMessageToPlayer(who, toWhom, GameMessage(messageContent = message))
when(result){
is MessageSentResult.Success -> // TODO: success
is MessageSentResult.Failed -> //TODO failed
}
}
List all players in a room
Our second case is where we transform a blocking call to a suspending function. We can use presence feature of Ably to get all players in a room. Ably's method ably.channels["channelName"].presence.get()
is a blocking call. Turning a blocking call into a suspending call might be useful in a case where we do not want to block calling threads, such as a UI thread
override suspend fun allPlayers(room: GameRoom): List<GamePlayer> {
return suspendCoroutine { continuation ->
val presenceMessages = ably.channels[roomChannel(room)].presence.get()
presenceMessages?.let {
continuation.resume(it.toList().map { DefaultGamePlayer(it.clientId) })
}
}
}
With the above code, as with sending message to player, we also translate from Ably's presence message to our own model while we expose it as a suspending function.
Register to presence events
Our third case is where we need to register to an event stream. Observing players entering or leaving the room represents a continuous flow of events. With coroutines we can use flows to represent such a message stream .
To transform callback based API to a flow, we can use callbackFlow function.
override fun registerToPresenceEvents(gameRoom: GameRoom): Flow<RoomPresenceUpdate> {
return callbackFlow {
val observedActions = EnumSet.of(PresenceMessage.Action.enter, PresenceMessage.Action.leave)
ably.channels[roomChannel(gameRoom)].presence.subscribe(observedActions) {
when (it.action) {
PresenceMessage.Action.enter -> trySend(RoomPresenceUpdate.Enter(DefaultGamePlayer(it.clientId)))
PresenceMessage.Action.leave -> trySend(RoomPresenceUpdate.Leave(DefaultGamePlayer(it.clientId)))
}
}
awaitClose { cancel() }
}
}
The code above transforms the callback based ably.channels[roomChannel(gameRoom)].presence.subscribe(observedActions)
function into a Flow<RoomPresenceUpdate>
. It also transforms Ably models into our domain models like RoomPresenceUpdate
and DefaultGamePlayer
For more implementation details and usage of SDK please visit the GameRoomSDK repo.
Conclusion
We have seen that, while Ably simplifies sending and receiving external messages, coroutines simplify internal messaging workflows. While messages are transported externally through Ably channels, internal transportation can happen through suspending functions, flows and other coroutine enabled constructs.
When zooming into the bigger picture of your distributed application, it is much easier to see how compatible Ably and coroutines are!
I hope you enjoyed this post. Happy programming!
About Ably
Ably’s platform provides a suite of APIs to build, extend, and deliver synchronized digital experiences in realtime for millions of simultaneously connected devices across the world. We solve hard engineering problems in the realtime sphere every day and revel in it. You can find us on Twitter or LinkedIn, and we'd love for you to get in touch if you have any questions.
Further reading
- The Ably async/await post we promised
- Ableye: How we visualized an Ably SDK with Go and Ebiten
- Vue.js and Node.js tutorial: a realtime collaboration app hosted in Azure Static Web Apps
- How to use pub/sub in C# .NET to build a chat app
- Build a live multiplayer game in Unity with Ably