Morpheus Ability System

Overview

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).

Building abilities

Abilities derive from UMorpheusGameplayAbility. They are plain UObjects, 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.

Lifecycle events

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:

  1. Select a target

  2. Pack ability parameters

  3. 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.

Networking implications

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.

Time syncing

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.

Cooldowns

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.

Other helper functions

  • 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

Using Abilities

Through the inventory system

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.

Using abilities manually

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.

Last updated