Quod Soler ☀️

Quod Soler ☀️

@quodsoler.com

Lead Gameplay Programmer for Lords of the Fallen at Hexworks • I will teach you Unreal Engine • Gameplay Ability System expert, opinions are my own

262 followers40 following410 posts

Cooldown reset on kill in GAS needs no timer API. Cooldowns are tags granted by a duration GE. Remove the cooldown effect and the tag disappears with it: ability ready. I like this a lot as a design decision. State lives in one place and everything else reads it.

The nastiest GAS failures are effects that apply successfully and change nothing. The unset SetByCaller is the canonical case: one log line, magnitude 0, every gate passes. Nothing crashes, so it survives into playtests. I check the spec before I suspect the effect asset.

Naming variables well is really important. In the age of AI agents with Claude Code or Codex some people may decide to let agents decide all the names and don't even look at the code. But actually proper naming, which conveys intent, is useful to both humans and AI agents.

GAS tag gates fail open, not closed. HasAll on an empty container passes, an unpopulated tag query matches everything. A designer leaving one field blank has silently disabled your gate. I treat empty containers as design decisions that must be written down.

One surprising feature of Raylib is that it supports DualSense controller out of the box! Sometimes some engines can have trouble supporting it, but somehow this small library makes interacting with it trivial!

In Unreal GAS sometimes you need to change aan ability behavior based on character tags granted by other abilities. There is one issue though. In ActivateAbility those tags may no longer be there. The activation canceled a previous ablity that granted them. What do you do?

That is what the PreActivate function in the ability is there for. It gets called when your ability is about to activate but before cancellung other abilities. You can use it to initialize state that can be then used in ActivateAbility. It's pretty useful!

Everyday I'm considering going back to doing some small C projects. Not even C++. I still have to try Raylib as well, so maybe I can use this as an opportunity to do so.

The attack starts blending out. Should the next action already be allowed? I'd decide that before wiring PlayMontageAndWait. OnBlendOut marks the start of blend-out. OnCompleted marks an uninterrupted finish. They are different moments on the timeline.

A mage fires a projectile, then equips a stronger staff before it lands. Should that projectile deal more damage? I'd settle that rule before setting up GAS attribute captures. Snapshotting decides whether a captured stat stays fixed or is evaluated live.

Teams tend to discover it the same way: a playtest hit clips through a wall and the server just agrees. Not a bug. GAS target data is client authoritative by default, validation is left to you on purpose. Decide per ability how much you trust the client.

Half of GAS debugging is realizing the engine already knows the answer. An ability that refuses to activate is not a mystery: Log LogAbilitySystem VeryVerbose prints the exact check that failed, from blocked tags to net policy. One console line before any breakpoint.

Want an evolving world in your game? Start with Game Flags: killed_dragon = true met_merchant = true village_burned = true Every NPC, cutscene, and door checks the flags. That's it. Silent Hill ships this. Simplest system you'll ever build. Most power you'll ever get from one.

I want a spear's bonus damage to reward spacing. In my setup, the pole is rank 0 and the tip is rank 1, in the same hitbox group. Lower rank wins. Touch both: normal damage. Touch only the tip: bonus damage. The hitbox data defines the reward.

If a hitbox looks right in the editor but misses in-game, the preview stops being useful. My preview and runtime sweeps use the same transform code, weapon meshes and sockets. I want designers adjusting the attack, not checking which view to believe.

The whole input buffer runs on the owning client: the queue, the gate check, the drain. Only the resulting gameplay event touches the network, and it goes through the same activation and prediction path as a live press. Buffered and live input can never behave differently.

For knockback, I want designers to author the distance an enemy should move. My code derives the root motion strength from that distance, the duration and the curve's average. They can reshape the shove without manually retuning its strength each time.

Symptom: veteran save files have quietly inflated stats and nobody can say when it started. Cause: the save system persisted CurrentValue, so every buff active at save time was baked in. Fix: save BaseValue, reapply persistent effects on load. Then audit your old saves.

You queue a dodge during an attack, then change direction before it starts. My buffer reads the stick again when the dodge fires. Above half deflection, it uses the new direction. Otherwise, it keeps the original one. A light nudge should not redirect the roll.

The frames where an early press is remembered, and the frame where the follow-up may start, are animation decisions. So they live on the montage as notify states, next to damage and i-frame windows. If retiming a combo window needs a programmer, it is authored in the wrong place.

An animator retimes a dodge. Now the safe frames need to move too. In my framework, an anim notify state controls the invulnerability effect: apply it on begin, remove it on end. The animator can adjust that window alongside the animation.

Hardcoding numbers into Gameplay Effects doesn't scale. The fix takes 3 steps: 1. Set the modifier magnitude to SetByCaller 2. Tag it, e.g. Data.Damage 3. Pass the value into the spec before applying it Now one effect serves every weapon, and the numbers live in a data table.

Hardcoding numbers into Gameplay Effects doesn't scale. 10 weapons means 10 damage effects. 50 weapons means 50. SetByCaller lets the caller send the magnitude at runtime, from a data table or the weapon asset. One effect definition. As many uses as you have data rows.

Gameplay programmers: if your dodge roll has an IFrameDuration float, it's already a bug. The animator retimes the roll. Nobody updates the float. Players get hit mid-roll and blame netcode. My C++ has no such float. I-frames are the notify state in the animation.

If GAS warns about too many live AbilityTasks, I would check cleanup before raising the limit. Start with cancellation and early returns. Does each path reach EndAbility? A higher cap can hide a leak for longer.

A player dies while a buff still has 20 seconds left. Should respawning remove it? I'd answer that before choosing where the ASC lives. A PlayerState-owned ASC can keep state across a Pawn change. The design tells you which lifetime you need.

If your GAS ability works once and breaks on the second use, check what it remembers. InstancedPerActor reuses the same object. A cached target or bHasHit can survive the last activation. Reset per-run state when the next one starts.

You don't need to use GAS to benefit from Gameplay Tags. You can use Gameplay Tags on their own. They are incredibly useful to track state, communicate events or even as a simple save system. The only thing I would recommend taking from GAS is the Gameplay Tag Count Container.

Why don't GAS Abilities have a Tick function? It catches almost everyone learning GAS. The system is event driven by design, so Gameplay Events fire when you need them and you skip the per frame work. If you do need to tick, a small Ability Task covers it.

If you must make sure of one thing is this: Saves must NEVER break. A good lead once engraved this into my mind and it has lived there forever. So if you need to improve your save system, don't hesitate to do so.

Why does an unset SetByCaller become 0 instead of failing loudly? The number never lived on the UGameplayEffect. It belongs to the spec, supplied at creation. No entry, nothing to evaluate, fallback is 0. And if that 0 feeds a divide op, you built a division by zero.

GAS tags are reference-counted, not boolean. If two effects grant State.Stunned, removing one effect must not clear the state. Only once all sources of that tag have been removed the Stunned state ends.

Small input buffering detail that matters: timestamp entries in real time, not game time. Hitstop and slow motion stretch the game clock, and how long a press stays valid should follow the player's wall clock. That one does not slow down with the game.

Sent the Mass data model to the newsletter yesterday: entities as 8 byte handles, fragments and tags, and the archetype layout that makes it all fast. The full article version is on the blog, with the code and the which-element-type decision list. Link below.

I said it before but it bears repeating. NEVER put gameplay logic in Gameplay Cues. Cues are cosmetic. They can be lost over network. Executing logic is not a Gameplay Cue job. They are there for player feedback.

Symptom: a stacked "+50% damage" buff deals +100%, but the balance sheet assumed +125%. Cause: Multiply ( Additive ) was used. Fix: Switch to Multiply ( Compound ) when buffs should add up in this way.

Tip when working with equipment buffs. If your equipment grants passives, store the FGameplayAbilitySpecHandle for the abilities they grant. Later when the equipment is unequipped, use that handle to remove the ability. Removing abilities by class may not be a good idea.

Silent Hill controls all of its progression, cutscenes, and game flow with simple Game Flags. No complex state machines. No quest engine. Booleans. If you're building an RPG or any evolving world, this is the highest leverage-to-effort system you can steal.

Choose an Execution Calculation on your effect if you need side effects. Choose a Modifier Magnitude Calculation if you need one of the modifiers of your effect to be derived from a complex formula. One is used for input, the other for output.

If every grunt in the level shares the same movement tuning, do not store MaxSpeed 600 a thousand times. A const shared fragment in Mass stores one instance and every entity references it. But never register per-entity-unique values: different shared values can not share a chunk.

A buffered dodge stores the stick direction at press time. The player keeps steering while it waits. Fire it with the stored direction and it feels like a bug; re-sample the stick at consume time and it feels like the game read their mind. It is one small function.

Keep Mass fragments small. Every byte multiplies across thousands of entities, and cold data mixed into a hot fragment wastes cache bandwidth for every query touching it. Split per-frame data and rarely-touched data into separate fragments. Mass handles tons of small ones well.

After a GAS execution runs, the engine multiplies every output modifier by the effect's stack count, unless you call MarkStackCountHandledManually. A stackable DoT at three overlapping stacks deals triple damage per hit, and single-stack testing never shows it.

Why is an ECS fast? In Mass, entities of the same composition pack into 128 KB chunks, one parallel array per fragment type. Reading transforms is a linear walk over packed data of one type, exactly what the prefetcher wants. Fragments you did not ask for are never touched.

I think we all can learn a lot from old games like Zelda Ocarina of Time. The way character movement colliding with objects is handled in that game is to me superior than default Unreal Engine. They use "soft" collisions for objects. The player never gets stuck when colliding.

Instead it simply get's pushed back. The more intersection there is the more it gets pushed back. This allows the player to navigate crowded environments easily. And this also helps players be able to push enemies or for enemies to push the player. Nintendo devs are awesome!

Once it clicks that GAS cooldowns are just tags, good things open up: a chill debuff that extends cooldowns, gear that resets them on kill ( remove the GE, the tag goes with it ), UI reading remaining time from the effect itself. The timer you were looking for never existed.

Mass fragments must be trivially copyable: no FString, no TArray, nothing that heap-allocates, because entities move between chunks with raw memcpy. There is an escape hatch trait literally named AuthorAcceptsItsNotTriviallyCopyable. The name is the warning.