// log

Problems & Fixes

A running log of notable commits with problems, fixes, and decisions made while building Comply. They also serve as a reference for useful multiplayer and GAS patterns. Each entry links to the commit. View all commits, annotated →

filter
sort

Animation not showing for other players when played by clients

The animation was only being played locally via the ability flow, so simulated proxies never saw it. Now playing the animation authoritatively in a target data callback for replication while still playing it locally for the ability flow.

Rubber banding when getting slowed

Clients were rubber banding when entering slowing zones because the movement speed reduction was being applied to the character movement component via an attribute change delegate, which can only be applied after a round trip (due to modifying CMC values), giving the server time to correct the client's position before the slow took effect locally. Fixed by immediately applying and removing the speed reduction locally on the client so the client and server stay in sync. The sprint ability uses the same approach of applying speed changes locally first.

BTTask member variables being shared across all instances of that enemy

Previously the cached variables needed for activating abilities, binding, etc. were stored as member variables. When there are multiple instances of the enemy, they would all share one variable because the member variables are shared across all enemies running the same BT asset. Fixed by moving all per-enemy state into a per-instance memory struct allocated in NodeMemory, so each enemy maintains its own isolated state.

Jittery projectiles on high latency

On high latency, projectiles were jittery due to replication constantly updating and correcting the projectile's position. Fixed by passing the launch velocity into the projectile movement component locally via an OnRep, so clients simulate the throw themselves for smooth movement. Replicated movement and the projectile mesh replication are both disabled to prevent fighting against the local simulation. The same approach was applied to all projectiles in the game.

Server-side logic not visible to clients (ragdoll)

The Die function runs from the server, so a replicated bIsDead bool with a rep notify is used to trigger the OnRep when the character dies. The OnRep is called directly on the server since it doesn't fire for the machine that sets the value. This is a general pattern for any server-side change that needs to trigger visual or local behavior for clients — the OnRep must be called manually on the server so the server sees it too.

Character selection

1. Character selection is handled by a widget added to the viewport when the player interacts with the selection actor. Selecting a class sends a server RPC that unpossesses and destroys the current character, then spawns and possesses the selected one. The previous class's abilities are always cleared first so the new character starts with the correct ability set.

2. The selected class is stored in the game instance mapped to each player's player state ID. The GameMode overrides GetDefaultPawnClassForController to spawn the correct character per player when server traveling.

Double gameplay cues playing on clients

The client was getting the RPC from the replicated cue after the round trip time, so it was playing two cues due to also playing the previous predicted one. Fixed by using a scoped prediction window when playing the replicated cue, so that the cue RPC won't broadcast to the client if the same prediction key used when the ability was activated is used for the replicated cue.

Ability not committing for clients due to a client callsite

The ability was previously being committed in an animation callback, but the whole ability callstack leading up to it runs locally only, so the commit was not being called on the server. Fixed by moving the commit to the server RPC that handles spawning the buff totem actor so it runs authoritatively on the server. This is a general pattern and any GAS commit must be called on the server to take effect correctly.

Stale task pointer causing crashes (shotgun reload loop)

EndTask only marks the montage task as pending kill rather than destroying it immediately, so the next reload shell iteration could bind to a stale pointer before the task was fully cleaned up. At high ping the server could also end the ability while the client was still mid-loop attempting to call the next shell load, leaving the ability in an invalid state. Fixed by validating the ability is still active and the task is valid before binding each iteration, and by letting GAS manage task lifetime automatically rather than manually ending and nulling the task.

Looping gameplay cue cutting out on clients due to unconfirmed ability prediction

AddGameplayCue was being called on a locally predicted ability before the server confirmed it, so with latency the cue would cut out when the prediction window expired before server confirmation arrived. Fixed by bypassing ASC replication entirely and manually triggering the WhileActive and OnRemove cue events through the cue manager directly, so the cue lifetime is no longer tied to the prediction window. This only triggers the cue locally.

Executing non-replicated cues locally for responsiveness, then replicated on the server

At high latency, a non-replicated cue should be executed locally for responsiveness, while a replicated cue can be executed right after if it's the server. For the clients, their replicated cue can be executed from the server in the target data received callback, since at that point the server has the correct hit information and can replicate it to all simulated proxies.

Unpredicted GAS ability cancellation causing delay in (zoom and rotation mode)

Zooming and rotation mode were both driven by a gameplay tag applied through an ability, with the ability cancelled on input release which removes the tag. Since ability cancellation (and ending) is not predicted in GAS, clients had to wait for server confirmation before the tag was removed, causing a visible delay on high ping. Fixed by setting a local bIsAiming bool immediately on input press and release, bypassing the tag entirely for anything that only needs to affect the local client.

Double execution due to code running on both client and server

The animation callback was running on both server and client, so both were independently spawning the grenade resulting in two grenades per throw due to double execution. Fixed by separating the spawn paths. The client spawns via server RPC, while the server spawns directly, which prevents the RPC from triggering a second spawn on top of the direct one on the server. The locally controlled check accounts for a dedicated server setup too and is used to be more explicit in this case. A similar solution can be used in other cases of double execution problems.

Unexpected rotation behavior due to all machines running rotation logic

The rotation update function was running on all machines, including simulated proxies, causing them to fight between local interpolation and the server-replicated rotation. Fixed by early-returning for simulated proxies so only the server and owning client run the logic. The server computes and replicates rotation for simulated proxies to display to other players, while the owning client computes it locally for responsiveness since SetActorRotation is not predicted, so it would lag behind by a round trip. Without the early return, simulated proxies would calculate their own rotation locally while also receiving the server-replicated rotation, causing them to fight between the two values and produce jittery behavior.

Capturing attributes and using them in execution calculations

Armor is an attribute on the players that reduces incoming damage, with diminishing returns, and with an effective cap. Armor penetration is an attribute on enemies that reduces effective armor by a certain percentage. Both attributes are used in the damage ExecCalc before determining final damage.

Custom gameplay effect context

A custom gameplay effect context adds extra data that the default context doesn't support. In Comply this is used for dealing damage, such as to pass shield hit information into the execution calculation, so the ExecCalc can apply the correct damage multiplier depending on whether the trace went through a shield. All custom fields are net serialized for multiplayer.

No entries for this tag.