How to cleanly use Combine with SpriteKit?

I am building a SpriteKit game and am making heavy use of Combine as a way to communicate state changes between the objects in my game. For example, I have many situations in which a class is responsible for creating certain SpriteKit nodes (for example SKSpriteNode units on a map), and the class subscribes to certain publishers to be notified when changes to units occur (new units, unit movements, etc).

Does anyone out there have any experience with using Combine with SpriteKit? One specific problem I am having is how to cleanly subscribe to an array publisher (specifically a CurrentValueSubject<[MyCustomType], Never>([]), and to know in the sink receiveValue which item in the array was the one that caused the publisher to fire its notification.

I am also looking to see if anyone has some more general architectural advice on how to cleanly use Combine and SpriteKit together. I have not been able to find any resources online that discuss this at an architectural level, which makes me wonder if this is possibly just not something that SpriteKit developers do, even though it would seem like Combine would be a great way to build a SpriteKit based game.

Hi @jsclev ! While I’m very familiar with Combine, I’m not with SpriteKit. So, that said, here is my contribution to your post:

  1. Don’t use Combine unless you have to. Combine best works in scenarios where some long running (non-GUI) process happens and feeds data to some other process over time, it is especially good when that data needs to be converted. Combine can definitely feed GUI elements, but you need to sync your sink to the main thread.
  2. Evaluate your callbacks, that’s a good spot to do async work. Maybe you get a callback response and if it does some work, you feed it to a publisher? It also works if you have multiple things reacting to a single event (in this case you will have multiple subscribers).
  3. Feed events and actions to publisher that updates your game’s internal state. I can also imagine several pass-through subjects for different elements of your game.
  4. Write your game without Combine, see where the game gets “stuck” and needs async operations, then use Combine as a tool to un-stick those sore spots.

As for you original question with regards to CurrentValueSubject, could you elaborate what you are trying to achieve?

Oh wow, thank you so much for the info, that is all very helpful. So…I may have to rethink how I am doing some things based on what you said. I’m very curious about your statement “Don’t use Combine unless you have to.”

I am basically using Combine all over my codebase as the primary means of state change communication between my game objects. My game is a map-based strategy game with military units. So, for example, I have a Unit class which represents a single military unit such as a tank, which contains a position property, which represents the specific map coordinates where the unit is currently located. Since there are various entities that need to know about changes to a unit’s position, I made the position property a publisher via CurrentValueSubject, and then every other object that needs to know when a unit’s position has changed, they subscribe to the unit’s position property. One example subscriber is a custom SpriteKit SKNode object that renders the unit’s sprite on the map. I come from a MVC and MVVM background, and was looking for the Swift equivalent of VueJS and Vuex and FRP (functional reactive programming) seemed to fit that nicely.

So I’m not using Combine for the async features (although I may at some point), rather as a way to break up a large set of objects that all need to be able to communicate cleanly with each other via state changes. It’s headed down the path of being a fairly complex game, hundreds of classes, so I do need some sort of architectural pattern to manage the complexity.

I’ll have to think over what your wrote, and do some more reading. Thanks so much for the input.

I see the logic in separation of concerns and logic. However, state management and updates are usually a single or a small set of arithmetic operations, consuming little to no time, at least less than the amount to enable multiple threads within an application (threads consume resources). That said, if that is what you feel comfortable with, it doesn’t hurt either.

Share your progress as you go and good luck!

I spent the last several days really diving into Combine, and reading and re-reading the best documentation I could find on it (Ray Wenderlich in “Expert Swift” for example). I’m even a little more confused, as I have now had multiple people tell me: don’t use Combine (basically). However, I’m wondering that this is just due to confusion over a misunderstanding of what Combine is, and Apple’s documentation does not seem to help. It is advertised as a framework to enable publishers of data to publish their data to interested subscribers, over time. Which is an incredibly abstract concept, seems to me.

And that seems to be the confusion to me: the part about “over time”. It seems like people think of Combine as only useful in situations in which they are polling some network resource or something, and there is some response every now and then, that changes local state. But I come from a VueJS and ReactJS (web development) background, and the idea of reactive programming just makes sense. When you’re building large complex user interfaces, I don’t even know how else you would do it. Decoupled reactivity is the only way I would know how to construct a complicated UI (I’m sure there are other ways, I just don’t know that they would be).

Concrete example from my game: I have an “End Turn” button in my game. The user will tap this button at the end of each turn, which occurs every 1-3 minutes during normal game play. I have it wired to increment the game.turnOrdinal property, which is a publisher (CurrentValueSubject). I have about a dozen SpriteKit nodes all over my game that need to update themselves when the turnOrdinal property changes. So these are all Combine subscribers on the Game object’s turnOrdinal property. It seems to work flawlessly, and all those SpriteKit nodes have no idea that the “End Turn” button was pressed, they just know that the game.turnOrdinal property changed.

So I think I’ve come to the conclusion that Combine is actually an excellent fit to provide a clean decoupled architecture for a SpriteKit-based game, despite the fact that I still can’t find much on the web talking about how to use Combine with SpriteKit. It’s a shame, since those of us who have come from VueJS and/or ReactJS know just how awesome those frameworks are. We can achieve very similar functionality as those web frameworks using Swift with Combine. I do wish Apple would publish a little more information on Combine, because it seems like it is a very misunderstood framework.

I am still struggling with the concept that when I set up a property as a CurrentValueSubject that is a collection (an array for example), when I subscribe to changes to that property, the receiveValue handler only gives me the updated entire collection. I have no idea which item in the collection was the item that caused the collection to be updated. So, for example, if I add a new item to the collection, the subscriber does not know which item was the new item. So I’m sure I’m just still doing something wrong, or I’m not thinking about it in a clear enough “functional reactive programming” way. Right now I jump through a lot of hoops in my custom Spritekit node classes by storing a local collection, then when the subscriber receives an update, it walks through a comparison between the CurrentValueSubject's updated collection and compares it to the local collection, to determine which item was added or deleted. For example, the class that handles the rendering of units on the map uses that technique to know when to remove a unit from the map. Not sure if any of that makes sense, but if you have any suggestions, I’d be much appreciated, definitely feeling like I’m still missing something about Combine.

A few things:

  1. When I said (and I assume it goes for others), don’t use Combine, it was meant for the purpose of converting SpriteKit into a reactive framework. It’s not that it is impossible, it is that with good game logic and state, you don’t need to, the game loop works.
  2. Combine is absolutely amazing, an async framework for processing and combining data results. Even with async/await out, it has a huge place and purpose. The biggest user? SwiftUI.
  3. Combine can be used with SpriteKit as you are doing: running data, events and updating state.
  4. You should probably look into GameplayKit for state management.
  5. You mentioned a “local collection”, see GameplayKit above.
  6. I want to help with your CurrentValueSubject question, but I’m not clear on what you want to do.
  7. If your game is multiplayer, using it to handle the incoming feed from other players is a job for Combine.

I can’t thank you enough for your help, this is so so helpful to get some input from an experienced Combine developer.

I was told by several developers in my local area to stay away from Combine, as if it is a bad framework or something, and now after multiple days of heavy research I’m a little unclear why. As you said, it seems like a really amazing framework. For example, my game map is composed of Tile objects (custom class), and each Tile object has around 10 properties that it publishes. I can easily have 10,000 Tile objects, so that is a lot of publishers, and performance seems amazing. So I was really confused about what I was being told in my local area.

The issue with CurrentValueSubject is that most of my CurrentValueSubjects in my game are properties that are collections, for example:

public class Player: ObservableObject {
    public private (set) var units = CurrentValueSubject<[Unit], Never>([])  
    ...
}

I have several dozen classes that are subscribers to the units property, things that need to know when either a unit has been added or a unit has been removed (i.e. killed). So I have code like the following in lots of places in my game:

public class UnitsMapLayer {
    public init(player: Player) {

        // Bunch of initialization stuff here
        
        player.units
            .sink(receiveValue: { units in
                DispatchQueue.main.async {
                    // Do SpriteKit rendering stuff that updates 
                    // the display of the units that are on the map
                }
            })
            .store(in: &cancellable)
    }
}

So when the subscriber above receives the updated units collection, the subscriber gets the whole collection, but it has no idea which item in the collection was the item that caused the publisher to fire its change. Was a new unit added? Was a unit removed? Unless I keep a private/local copy of the units collection within the UnitsMapLayer, I have not figured out any way with Combine for the subscriber to know. So that’s what I’ve been doing: I keep a local copy of the units property, then loop through both the local units property and the new units collection in the sink, comparing them, to find whether a unit was added or removed, then either add a new SKNode to my SpriteKit node graph if it was a new unit, or go find the already existing SKNode and remove it if a unit was removed. Technically that technique works, but there is no way that is the “normal” way of doing things with Combine, it just doesn’t seem right, and I’m sure I’m just not thinking about it in a “functional programming way” or something.

Based on continuing research online, I found this: shareup.app → Designing and writing a custom, generic Combine Publisher in Swift. The key sentence in that article is “The core business logic of the application modifies the application state in response to those actions and delivers a new immutable snapshot of the application state to the UI, which re-renders itself.” So I refactored so that each time the units collection changes, the rendering code inside UnitsMapLayer completely removes all unit SKNodes, then re-adds every unit, based on the new immutable collection of units received in the sink. Again, this also technically works, it’s just that now if I have 500 units (a definite possibility) and a single new unit is added, I am removing 500 nodes from the SpriteKit node graph and adding back 501, all just because I have a single new unit to add. It just seems really wasteful and non-scalable in terms of CPU usage. But that technique is precisely what the person above is describing. It is essentially the concept of “nuke and pave” (completely remove everything and add back everything fresh). If this is the Combine and SpriteKit way to do things, I suppose that is fine, it just doesn’t seem quite right. What if I had 100,000 units? This technique just doesn’t seem to scale, which is why it feels wrong to me.

So I’m still stumped, and still very confused as to why I can’t seem to find anyone else asking this question anywhere online, it seems like such a basic fundamental thing. I have hundreds of situations all over my codebase like this, so it’s very problematic for me right now. I’ve been searching for weeks now and have not found a solution. Maybe this is one of the reasons why my local developers told me not to use Combine, I don’t know.

I also found this: How to create and emit custom event with Combine and Swift? - Stack Overflow which the developer describes how to emit custom events with Combine, so I am going to try to learn how to do that next, as that may work.

But I’m still confused as to why I can’t seem to find an answer to this question. I think what doesn’t make sense to me is: how would you not run into this problem if using a CurrentValueSubject? It seems like lots of publishers would be collections, not just simple single/scalar values, and it would be highly beneficial to the subscriber to know the pertinent item that caused the collection to change. That’s why I still think I’m just not understanding something fundamental about Combine and functional reactive programming.

I feel like I’m wanting something like what the Swift reduce function does: it passes in an accumulator plus the new value to its closure. Is there some way to tell the Combine sink to pass in both the entire new collection, plus the item that caused the collection to change?

One final comment in case anyone out there is reading this and does not use/know SpriteKit: SpriteKit is very much a state management framework (at least that’s how I think of it). I don’t tell SpriteKit how to actually render sprites on the screen, I add SKNodes to a huge node graph, and set certain properties on each SKNode such as position, then hand that node graph off to the SpriteKit engine, which then figures out how to actually render the pixels on the screen. So it abstracts away the step of actually doing the rendering. So all of my “rendering” code is just adding SKNodes to the SpriteKit node graph. Which is awesome by the way. So that’s why I thought Combine would be such a good companion to SpriteKit.

With regards to the quote above. I can only explain in analogy to the pattern known as The Singleton. Most people don’t know how to use, or use it incorrectly which leads to a lot of problems. So, there is a group of people that will just vilify it. But used properly it will not only simplify code and make it more efficient, it will make it easily understandable - Apple itself uses it in many places and very efficiently.

The same goes to Combine. Which is an amazing framework, lightweight and when used properly, efficient and very readable.

With regards to SKNode, itself does not draw any content, so you are operating on data, there is even a remove all children call (which essentially just prunes nodes, if you remove the parent, it removes all the leaves!). But why not just addChild or insertChild?

Thanks for that information, and just in general thanks for the continued dialog, it is really helpful to me.

I have continued to work heavily with Combine over the last week and I’ve come to basically the same conclusion as you: it’s a great framework if used properly. Your example of Singleton is spot on. And I will admit it did not come easy learning how to use Combine properly. Functional reactive programming is very abstract. But it is a really natural fit for SpriteKit. I actually plan on writing some blog articles about using Combine with SpriteKit, since they work so well together. SpriteKit is really at its heart a state-management-related framework, you just construct a tree of nodes, and you let SpriteKit figure out how to render its node tree. You as the programmer don’t have to worry at all about any actual rendering, double buffering, etc etc. You just manipulate your node tree. So, for that reason, it works perfectly with Combine. My only thought as to why I have not been able to find anything online about this relationship is that it seems like there are very few SpriteKit developers out there. It’s a real shame. My experience (one year) with SpriteKit is that it is a phenomenal framework. Now that I think I’ve mostly gotten through this issue, I can keep bumping out the complexity of the code for my game, and I’m not going to eventually hit that wall where the code is just too complicated to keep working in. I’ve worked on a few projects like that, so I was trying to avoid that problem on my own project. I am making a game akin to Civilization II, so it’s a fairly complex system.

I am still fuzzy on when is the best time in an application to use @Published (i.e. publish will set changes) vs CurrentValueSubject (i.e. publish did set changes), but other than that, I think I’ve got a good handle on all this now. I am now using CurrentValueSubject exclusively everywhere in my code, as I want my subscribers to know of changes after their publishers have changed, but still not sure if I am thinking of that correctly.

By the way, I basically use the technique now of “nuke and pave” so to speak (from that shareup.app article). Whenever a re-rendering is needed due to a state change, I basically do a node.removeAllChildren() to wipe out the specific part of the screen I need to update, then use the state passed into the receiveValue to add back in all the correct nodes into the node tree, then SpriteKit takes care of rendering those updates on its next rendering pass. It seems to work flawlessly for now, and the code is actually very clean and simple and easy to follow. Performance seems just incredible, there is no way I could have gotten the performance so good doing all this wiring up myself.

1 Like