Always ask the lead programmer if you're in doubt.
Remember, we gotta network stuff.
Use _variableName for private variables.
Use VariableName for public, const and static variables.
Never use underscores.
Never use abbreviated variables, except (if any missing or you'd like to suggest one please contact the lead programmer): i, j, rb, ui, gui, id, x, y, z, args, config.
Avoid singletons at all costs.
Avoid numbers in variables.
Always unsubscribe from events.
Document everything you can, the idea is: anyone with beginner understanding of code should read and understand the code, knowledge is everything and we got to provide that to newcomers and instruct them. This is the way.
Don't leave Debug.Logs, and always remove unused commented code.
Do not touch other people's code, if you're not directly working with it.
View
A view is a controller for an user interface, it sets the parameters, controls buttons, etc. A view's existence is never known by systems.
public sealed class RoundTimeView : Actor
{
// add time logic
}
Always add the postfix "View"in the name of the class for your views.
Actor
Actor is our substitute for MonoBehaviours, it optimizes and creates shortcuts for useful stuff.
Moving an object
Moving object with MonoBehaviours
transform.position = new Vector3(0, 0, 123);
Moving an object with Actor
Position = new Vector3(0, 0, 123);
The reason we do that is because transform.position goes into C++ every single time to get the transform, which can lead to performance issues later in development. Also it is a nice shortcut.
You can use the same idea for Rotation, and for getting the directions relative to that transform, like Forward,Up, Down.
Callbacks
The standard callback methods also have been changed, you'll use OnStart instead of Start, OnAwake instead of Awake, OnDestroyed instead of OnDestroy.
Another think you need to know is HandleUpdate, HandleLateUpdate and HandlePreUpdate. All these methods have one thing in common: they listen to an
Networking
For networked objects, instead of NetworkBehaviour, you will use NetworkActor. They work the same way as Actor.
System
This refers to a technical concept, not as a generic "container system" or a "interactions system"
A system is a classthat manages something, it should work on its own, the only exception is when it needs information about other system, but it is preferable that you do that by using eventsor event buses or network messages.
A system can be networked or not, for that you can inherit your class from System or NetworkSystem.
Here's a snipped of now you can declare a system that creates explosion somewhere.
It is also important that you know all the systems work with the class
Always add the postfix "System"in the name of the class for your systems.
System Locator
Class used to get game systems, using generics and then making cache of said systems.
The SystemLocator is a class used to easily find any system that has been registered on it.
It helps us avoid falling into the Singleton pattern, and does something similar to the Service Locator pattern. Also good for performance reasons, It can handle over a million calls every frame with no issues.
Using the SystemLocator
To register a new system you can call the SystemLocator.Register(this) method, it'll add that system into a dictionary of systems, preventing two of the same system from existing.
Then you can get this system from somewhere else using SystemLocator.Get<T>()
⚠️ IMPORTANT!
Note that is done automatically by classes inheriting System and NetworkedSystem. They register on Awake:
// Ends the round, regardless of how many objectives were completed SystemLocator.Get<GamemodeSystem>().EndRound();
to be called.
The reason being that in standard Unity environment, the engine searches through the entire instantiated object's list and tries to find the Update method declared somewhere. That's very slow.
Instead, our friend from the CS Framework decided to create an event bus for the player loop timing, so Unity only needs to worry about one object with the Update method.
Every method that listens to the UpdateEvent event busand its variants will then be called.
event bus
public sealed class ExplosionSystem : NetworkSystem
{
public void CreateExplosionAt(Vector3 position, float size)
{
// boom.
}
}
public class System : Actor
{
protected override void OnAwake()
{
base.OnAwake();
SystemLocator.Register(this);
}
}
Event Bus
The event bus is a design pattern, it serves the purpose of a "global event system" as it can be invoked from anyone, anywhere and can be listened by anyone, anywhere.
The implementation we use was created by the CS Framework and we use it to create new event buses. To understand further on the design pattern itself, you can read this article.
We use these events when we want something that will be used in more than one place, without having to care about object relation.
We usually want events to be fired by systems when we want to inform other objects something has happened, like a round timer being updated, a player that spawned, an object that was picked up.
Creating
To create a new event, we use partial structs that inherit from the IEvent interface. The reason for the partial keyword is that the uses the to create the rest of the necessary logic on that struct.
It is also interesting and kind-of but not obligatory standard to create constructors for these events, they can become quite large lines at times.
Pretty straight-forward isn't it? Well, now for the usage.
Invoking
Invoking is the easiest part. You just need to know what you want to invoke and when. The code below creates a new instance of our event, using the constructor we just defined and then invokes it. We also have to specify who called that event, in our case we use the this keyword to say that the class's instance that called it.
All event invoking should happen after OnAwake.
Listening
Listening is the most complex of them, because it contains scary words and more complex management.
It is advised that you add event listeners on OnAwake, to avoid initialization issues.
base.Awake() calls the parent class's Awake function. Then we add the method that will be called when the event is invoked. We'll declare the method as follows:
Examples
One thing we have to keep in mind is that, with networking, we have to use a data-driven design, as we cannot depend on one-time of events only, a user has to be able to disconnect and reconnect back later with no issues, if we did not use data-driven, the user would lose that information.
So, we can have the best of both worlds, already has callbacks when SyncVars are changed, and they are changed to the correct value when the user enters the server.
In the ReadyPlayersSystem, we have:
Action
An Action is an event that depends on an object to exist.
The Action event should be used when we don't need something to be accessed globally, like a button being pressed, or when we need to listen from something from a specific object.
Declaring
Actions have to be declared as the first thing inside a object with the event keyword.
Only the object that declares the event can invoke it.
It is noted that you can use an Action to send data too:
Using an Action
Let's imagine you have a granade in your character's hand. Pretty common sight in a Low RP server. In the code, usually we would have something like:
The OnPinRemovedevent will be used to to tell listeners that subscribed to that granade's event that the granade's pin was removed. The relevance of that OnPinRemovedevent is usually for UI related purposes, or running a local animation of a granade pin flying through your screen.
Something like:
You might even think it would be easier to call it directly, and depending on your logic it might be, but we are assuming that in this case its better to keep the classes decoupled.
Tweening
public class ButtonView : Actor
{
public event Action OnButtonPressed;
}
public event Action<int> OnNumberUpdated;
public event Action<Item> OnItemUpdated;
public class Granade : Explosive
{
public event Action OnPinRemoved;
public void RemovePin()
{
// Do pin removal logic.
OnPinRemoved?.Invoke();
}
}
The EventContext is usually unused but it gives us information about the invoking itself. Then we have the event we created, it is the struct that we previously created.
The ref and in keywords are there for optimization reasons but they are required, so always add them if your IDE doesn't autocomplete correctly.
One thing to note here is that you have to remove the listeners eventually, you have to do it on the OnDestroyed callback, but if you're using any class that inherits from Actor or NetworkActor, it already unsubscribes from it if you use the AddHandle method when listening to the events.
This is a SyncList, it updates automatically for all clients, and it as a OnChange callback. It makes us able to do this:
When the list is changed, we call the HandleReadyPlayersChanged method.
The method then calls the SyncReadyPlayersMethod, calling, then, our event.
All this trajectory is to ensure our client doesn't lose anything when he rejoins a game and can load stuff back correctly, not to mention this is way easier than doing RPCs all over the code.
Anything in the codebase can listen to that event, in that case we use for updating the UI, that changes the player name's color if they are ready or not.