Video games are composed of lots of components that must interact with each other. This can lead to a large amount of complexity when connecting them. Each component must find and hold references to other components that it is interested in. In other words, each component will depend on multiple other components.
In a way, this is inevitable. All projects tend to grow in complexity, which means more components need to interact with other components.
Let's look at a simple example: You're creating a survival game, and just implemented a PlayerHealth script. You're happy because now your player can take damage, which is a key part of the genre.
You do some play-testing and come to the conclusion that players need a way to see how much health they currently have, so you add a health bar, and make it so the PlayerHealth script updates the health bar.
Unfortunately, the happy music that plays during the game doesn't fit in when the player is low on health. So you add a reference to your MusicSystem, and whenever you take damage, tell it to change the music.
In two relatively simple features, you've added two new dependencies. If you forget to assign a reference to the health bar, or to the Music System, your player code will raise an error.
While not assigning a reference in the inspector is a (extremely) common occurrence, you might think that this will never happen with a player, since you will test your game and make sure all references are assigned.
What if you want to add the option for the player to disable the in-game music. If the in-game music is disabled, you can just remove the MusicSystem, since it's no longer needed. Should you happen to test your game, you will catch the problem immediately. But, if you are currently struggling to reach a deadline, you might not test, and thus miss the flaw in your architecture. Your PlayerHealth depends on the MusicSystem.
When inevitably your player does take damage, the PlayerHealth script will try to access the MusicSystem, which doesn't exist. This will cause an exception, even though your references were correctly assigned in the inspector.
In other words, your PlayerHealth script is coupled with your MusicSystem, and your health bar.
Changing a component that is coupled with others can be dangerous, because each change might break other systems that you had already finished.
Despite the downsides, coupling is inevitable. Your components need to interact with other components, otherwise your game wouldn't be... well, a game. You can, however, use Okto, which provides an event system to reduce the level of coupling in your games.
After you've had some time to try out Okto, please consider leaving a review on the Asset Store. It makes a world of difference and allows us to keep improving Okto and creating new assets for the community to use!
Sylvan Glade/Okto/Demos
directory).
Okto is an event system for game developers. It allows you to send events through a topic and receive them in other scripts which you have indicated are interested in the same topic. Okto operates without requiring those who send events to know about whoever is interested in receiving events. This results in cleaner, more modular and decoupled code.
Okto is based on objects called Communication Entities. These are the only objects you need to use Okto, and fall into three different Event Patterns: PubSub (Publishers and Subscribers), Services (Clients and Providers) and Event Queues (Producers and Consumers).
All communication entities must be created with an owner
.
The owner can be an object of any type.
The purpose of providing an owner when creating a new communication entity is so that the Visualizer can group your communication entities.
PubSub (short for Publisher-Subscriber) is one of the 3 communication patterns supported by Okto. Publishers send events through topics, which are then received by all interested subscribers.
In PubSub, events are synchronous. This means that after publishing an event, all interested subscribers will immediately (i.e. in the same frame) call their callbacks.
Okto.Publisher<TEvent>(object owner, string topic)
Publishers can broadcast events of type T
to all
Subscribers interested in the
topic
.
To publish an event, you can use the following method:
Publisher.Publish<TEvent>(TEvent @event)
Events can be published at any point in a C# script, so long as the Publisher has been created. One common pattern is to publish your events inside a subscriber callback.
If for whatever reason you want to delay your publishing, Okto Publishers provide a utility method:
Publisher.Publish<TEvent>(TEvent @event, MonoBehaviour monoBehaviour, float delay)
This method will create a coroutine running on the given MonoBehaviour, which means that running "StopAllCoroutines" will prevent the event from being published.
To remove a Publisher, you can use the RemovePublisher method:
Otko.RemovePublisher(Publisher publisher)
Subscribers receive events published to their registered topic
,
and execute their corresponding callback
.
Okto.Subscriber<TEvent>(object owner, string topic, Action<TEvent> callback, Func<TEvent, bool> filter = null)
Keep in mind subscribers will receive all events published through
the topic
. As such, you must either be specific
with your topic naming, or use filters.
For example, you could have the following topic, published by your PlayerHealth script: "player_health_changed". Then, your UI could subscribe to the same topic and update a health bar.
However, if you have a system that handles player death, you're not interested in receiving an event if the player's health changes from 10 to 9. So, you can pass in a filter when creating a callback:
Okto.Subscriber<int>(gameObject, "player_health_changed", MyCallback, health => health <= 0)
This way, only messages which match the filter health <= 0
will invoke
MyCallback
.
An alternative would be to split your topic into two smaller topics: "player_health_changed" and "player_died". Inside your player health script, you'd need to emit an event every time the health changes, and another when the health reaches 0.
Both approaches are valid. You should find a system that works best for you and be consistent throughout your project.
Keep in mind that the effect of calling Publish when there are no subscribers is, for most cases, negligible, but not null, so you should try to ensure all events are useful.
If you are publishing an event every frame, or multiple times per frame, you should always consider if it is something you really need, or if there is another way to organize your events.
To remove a Subscriber, you must use the RemoveSubscriber method:
Otko.RemoveSubscriber(Subscriber subscriber)
The service pattern (named this way because providers provide a service to clients) is the second of the 3 communication patterns supported by Okto. Clients send requests through topics, which are then processed by one of the available providers. The result of this processing is then sent back to the requesting Client.
Similarly to the PubSub pattern, requests in the Service pattern are synchronous. This means that when a request is made, the selected provider will immediately (in the same frame) execute its callback and return its response.
Clients can be used to send requests and receive synchronous responses from providers.
Okto.Client<TRequestEvent, TResponseEvent>(object owner, string topic)
Since requests are synchronous, this means that you should be careful when making requests that might trigger expensive operations. To avoid this, you can consider using a request queue, with response callbacks.
To remove a Client, you must use the RemoveClient method:
Otko.RemoveClient(Client client)
Okto.Provider<TRequestEvent, TResponseEvent>(object owner, string topic, Func<TRequestEvent, TResponseEvent> requestCallback)
To remove a Provider, you must use the RemoveProvider method:
Otko.RemoveProvider(Provider provider)
Both Consumers and Producers can be given an optional instance of OktoQueueParameters. These parameters are what controls queue creation, and as such are used only when the queue is created.
Okto.Producer<TEvent>(object owner, string topic, OktoQueueParameters queueParameters = default)
Producers are part of the Queue communication pattern. You can use a producer to produce a message into a queue for later consumption. This can be especially useful when you want to make sure that work is split into different frames. For example, if you have 5 enemies that need to path-find, but doing so in one frame causes a lag-spike, you can produce the messages into a queue, and have a consumer read them, one per frame, and do the required calculations.
To remove a Producer, you must use the RemoveProducer method:
Otko.RemoveProducer(Producer producer)
Consumers can be used to get events from Queues.
Okto.Consumer<TEvent>(object owner, string topic, OktoQueueParameters queueParameters = default)
Consumers provide a utility method to check if their corresponding Queue has events:
consumer.HasPendingEvents()
To get an event from a Queue, you can call Consume.
consumer.Consume()
To remove a Consumer, you must use the RemoveConsumer method:
Otko.RemoveConsumer(Consumer consumer)
To access the visualizer window go to the top menu:
Tools > Okto > VisualizerThe visualizer is composed of two sections: the controls and the graph view.
Currently, the controls contain only one button which allows you to disable the automatic layout system. The automatic layout system is force directed, as such, it will never achieve perfect results. Our suggestion is to let the graph layout itself as much as it can, disable the automatic layouter, and then manually make changes to the graph so you can interpret it more easily.
Keep in mind there are still a lot of features and improvements we want to make to the visualizer, so expect changes to the system.
The graph view shows you all your game objects that own communication entities as nodes.
Publishers and clients are shown as outputs (on the right side) of the nodes. Subscribers and providers are shown as inputs (left side).
Double-clicking a node in the graph view will highlight the corresponding game object in the hierarchy.
This section contains general advice, answers to common questions, or issues that occur regularly.
We suggest the following format for topic naming:
<ProjectName>/<Subject>
The purpose of these topic naming guidelines is to avoid conflicts between different libraries/packages. Okto, for example, publishes events to the following topic:
Okto/InternalEvents
These events are critical for Okto to function as expected, and any extra events would cause issues. As such, by following the provided guidelines, you avoid any conflicts with Okto's internal events.
As time progresses, you might build or utilize other libraries or packages that use Okto. These packages should in turn have their own <ProjectName>, which will prevent collisions with your own, or Okto's internal topics.
We provide a helpful code snippet that helps you organize your topics by using enums. This way you can keep your topics centralized, and you can navigate through them, by finding all references to a given topic using your IDE.
One of the most common issues faced by developers when using Okto is forgetting to remove entities before disabling a GameObject. Whenever a GameObject is disabled, any communication entities that were created inside any script attached to that GameObject must be removed from Okto.
If these communication entities are not removed, unexpected behaviour might occur. Okto will try to execute the callbacks for any entities which are interested in that given topic, regardless of their owning GameObject being enabled or not.
To combat this, whenever you exit play mode, Okto validates if there are any leftover entities. If there are, a warning will be logged to the console, so you can unregister those entities.
This section is a work in progress. As Okto is developed & the community grows, this section will grow and evolve.
// Topics.cs
// Create an enum which will contain your topics
public enum Topics
{
FirstTopic,
SecondTopic
}
// Place this static class after the enum
public static class TopicsExtension
{
// Make sure to change "ProjectName" to match your project name
private const string ProjectName = "ProjectName";
// Now you can use Topics.FirstTopic.AsTopicString() when creating any of the communication patterns
public static string AsTopicString(this Topics topic)
{
return $"{ProjectName}/{topic}";
}
}