Morpheus Ability System
Last updated
Last updated
Morpheus Ability System(MAS) is a replacement for the Unreal Gameplay Ability System(GAS) with Morpheus support. The goal is to provide similar building blocks as in GAS, like GameplayAbilities and GameplayEffects which can be created purely in BP by tech designers, hiding away all the complexity of the underlying networking mechanisms.
It is a gameplay system which lets developers give + replicate "abilities" to players (e.g. ability to force push other players around).
Abilities derive from UMorpheusGameplayAbility
. They are plain UObject
s, so they have no inherent replication functionality. All necessary replication is handled via the UMorpheusAbilityComponent
. Generally, when building abilities you handle everything locally and don't need to care about replication except for a few situations.
Note that this section is focused on building abilities in blueprint since that is the main use case. Everything mentioned here can also be done by using the matching C++ functions.
Note: Generally, you should NOT call the parent implementation when overriding any of these events, especially not HandleExecute
as that would immediately confirm the execution.
HandleActivate
This event is optional and only called on the authoritative client. It should be overridden if you expect the ability to be used with active targeting, i.e. if you want to have an "activation" or "targeting" phase where you can select a target actor/direction/point before starting the ability's execution. If an ability is always provided with a target from some other system, you don't need this event.
An example could be a Polymorph ability. It could be used actively, i.e. you as a player have to actively select the target that should be polymorphed. In this case you would override HandleActivate
and implement targeting in there. It could also be used as part of an interaction. Imagine you have a mystery box in the world that, upon interaction, selects a random ability and casts it on the interacting player. In this case the mystery box would provide the target directly to HandleExecute
(on the server) and would not call HandleActivate
(which is only used on the authoritative client).
Here's an example of a HandleActivate
implementation that spawns a target actor for selecting a location in the world:
HandleDeactivate
This event is also optional and handles cancelling the targeting state. It's recommended to override this to clean up if you have overridden HandleActivate
and spawned a target actor.
HandleExecute
This event is always called on the authoritative client in response to UMorpheusAbilityComponent::ExecuteAbility(ById)
. It has an OverrideTargetHandle
parameter which can be passed in from the outside. In most cases this will be empty but in some scenarios (like executing an ability as part of an interaction in the example above) the system that executed the ability can already choose a predetermined target.
Your HandleExecute
implementation should do three things:
Select a target
Pack ability parameters
Confirm or cancel execution
Selecting a target depends on your use case. If you didn't override HandleActivate
to spawn a target actor, just pass the OverrideTargetHandle
into ConfirmExecute
. If you did implement HandleActivate
, you now need to finish targeting and get the target handle from your target actor. Also, you have to make a decision whether to use the target handle of your target actor or the provided OverrideTargetHandle
. You can use the helper function GetFirstValidTargetData
to prioritize whichever target handle you pass into TargetData A
(it will pick the other one if A is empty). Whether to prioritize the override or your target actor's handle is up to you. See the image below for an example.
You can pass arbitrary parameters to the server and other clients when executing an ability. To do that, create a new struct, fill its properties, pass it into the PackStruct
function and pass the result into ConfirmExecute
. Your struct can contain any types that can be foreground replicated in Morpheus, so e.g. you can replicate MorpheusActor pointers but not regular Actor pointers. Properties that can't be replicated will arrive with their default value. See the image below for an example.
The last thing to do is to call ConfirmExecute
. This sends the execution request to the server. Note that until this point the ability has technically not been executed yet, it has just gathered parameters and targets. If you don't call ConfirmExecute
, nothing happens.
ExecuteOwner
This event is called on the authoritative client immediately after ConfirmExecute
. Here's where you implement the actual ability behavior from the authoritative client's point of view. Note that this is called while requesting execution from the server, so whatever you do in here is a predicted execution. There is no support for cancelling a predicted execution yet but this will come in the future. For now just implement your behavior directly, e.g. play a montage and then change state as desired. See also Time syncing for synchronizing your predicted execution with other clients if desired.
Passed into this event are PackedParameters
and a TargetHandle
. To unpack the parameters, call UnpackStruct
and select the same struct type that you used when you called PackStruct
during HandleExecute
.
To do anything useful with the target handle, use the functions in the Morpheus | TargetData
category. These allow you to extract actors, locations and/or directions from the handle.
ExecuteRemote
This event is called on the server and on all non-authoritative clients that received the ability execution. Here's where you implement the actual ability behavior from their point of view. This event only happens after the ability execution was validated by the server, i.e. it can never be cancelled.
Otherwise the same considerations apply as for ExecuteOwner
, i.e. how to deal with parameters and targets.
CanExecute
This function is called before HandleExecute
on the auth client and before ExecuteRemote
on the server. If you return false, the subsequent function is not called, i.e. you can cancel the ability execution.
Note that returning false on the server does currently NOT inform the auth client that its predicted execution should be cancelled. This will come in a future update to MAS. It does however stop the server from executing the ability and from forwarding it to the other clients.
In its current, early implementation MAS only sends ability executions in the networking foreground. This has implications on how you should build your abilities to ensure correct behavior. Let's say we have three actors, Source
, A
and B
. A
has Source
in its networking foreground, B
does not.
Here's what happens:
Optional: Source
calls HandleActivate
Source
calls HandleExecute
Source
calls ExecuteOwner
The server calls ExecuteRemote
A
calls ExecuteRemote
B
does nothing
If all of the ability's logic can be implemented solely in ExecuteOwner
and the server's ExecuteRemote
, you're fine. The ability will work for everyone. An example would be an ability where the server spawns a new MorpheusActor. This new actor would always be visible to everyone. However, if you want to play a montage on Source
on each client (to show Source
"using" the ability), the montage would only be played on clients that have Source
in their foreground.
If the ability requires logic to be executed on each client, then it will only work for clients that have Source
in their foreground. Examples would be an ability that can displace other players (since movement is client-authoritative, so clients have to displace themselves) or if you want to spawn a regular client-only actor. Especially displacement abilities could look erratic if only some players are moved and others are not. If they are close together, it's reasonable to assume that they will all have Source
in their foreground but this can't be guaranteed.
So for now you have to work around these limitations by making clever design decisions, e.g. by giving displacement abilities only to actors that are always in the foreground for everyone.
In the near future there will be a new class of abilities that can be executed in the midground which will give you more flexibility.
The request to call ExecuteRemote
on the server and all other clients will arrive at different times for everyone. This might generally be ok for your ability and might only have an impact on synchronization of the initial montage (animation time syncing is a future task). However, for some abilities it might look erratic. Suppose you want to displace all players within a cone. Without any kind of syncing, each player would be displaced at a different time (depending on their latency), giving the whole displacement a rather chaotic feel. If that's not what you want, you can make the ability sync to a certain time in the future and ensure everyone executes it at exactly the same time (unless they have an abnormally high latency).
To use this, set the SyncDelay
property on the ability. By default it's set to 0
which means "no syncing, execute immediately". When calling ConfirmExecute
, the SyncDelay
is added to the current server time to get the "sync time" and this is sent along with the execution request. When the request arrives on the server and remote clients, they can now calculate how much time is left until that sync time. This can also be requested via the GetRemainingSyncTime
function. You can use this to calculate timings for visuals, speed up montages etc. GetTimeSinceSyncTime
returns how much time has elapsed since the sync time was reached. This is also useful if no SyncDelay
was set to get the time since ExecuteRemote
was called.
If you just want to wait until the sync time is reached, use the WaitForSync
function and attach an event to it.
Each ability can have its individual cooldown which can be set in its properties. There's also a global cooldown configured on the ability component. The global cooldown prevents all other abilities on the same component from being used while the maximum of the individual and the global cooldown is applied to the ability that was just executed.
GetOwner
returns the actor that executed the ability
IsOnClient
returns whether you're on the auth client or on the server during calls to CanExecute
and whether you're on a remote client or the server during calls to ExecuteRemote
Our most common use of abilities is through gadgets, in the inventory system. This handles "activating" abilities when they are selected, providing controls to execute the abilities, and provides management to select which ability to equip. See Inventory for more details.
You can also grant, remove, and execute abilities manually, without using the inventory system, if you have access to the MorpheusAbilityComponent
. We don't currently have any examples in our codebase, but this would be a rough example of how it could work:
Register the ability on the server: This needs to be done ahead of using an ability, to inform your client it is able to use said ability.
Listen to OnAbilityGranted
on the client: This will be triggered when the ability's registered state is replicated down to the client.
Call HandleActivate
on your ability when you want it to be "active"
For example, if you want to activate your ability automatically, you can do the following:
Call ExecuteAbilityById
when you want to actually trigger the ability, e.g. as a result of user input.
(Clean up if necessary: Similar to OnAbilityGranted
, there is an OnAbilityLost
event, where the ability should be torn down (calling HandleDeactivate
). Equally, if you want to manually stop using the ability, you can call HandleDeactivate
when you want.)
(If you want to visualise cooldowns or the like, you can listen to the events on the MorpheusAbilityComponent
)
NOTE: The ability system assumes that any abilities are uniquely controlled in one place (i.e. an ability granted manually isn't also used by an item in your inventory). If this is not true, errors may occur, such as unequipping your item breaking your manually granted ability.