After a short break this round started with a playtest and a long list of things that felt wrong. The rule for everything below is the same one the whole project follows: stay as close to the original as we possibly can. When the original does something, we measure it and match it.
The goblins that would not attack
Standing in front of a pack of goblins for fifteen seconds without being hit is not how that fight goes. The AI log gave it away straight away: every goblin won its turn to attack, walked in, and then gave up with "couldn't reach the target", over and over for the whole fight.
The cause was a small geometry detail. MoveToActor with stop on overlap adds both capsule radii to the acceptance radius, so a goblin (radius 30) closing on the Arisen (radius 42) with a 90 cm acceptance stopped at about 162 cm. Its swing only fires inside 150 cm, measured in 3D from a shorter capsule centre. It stood just out of reach until its 5 second timeout. The fix measures reach flat and takes the radii off the approach:
// Swing reach is measured flat, centre to centre; a target far above or below (a ledge) is never in reach
const bool bSameLevel = FMath::Abs(Target->GetActorLocation().Z - Monster->GetActorLocation().Z) < 150.0f;
const float Reach = bSameLevel ? FVector::Dist2D(Target->GetActorLocation(), Monster->GetActorLocation()) : TNumericLimits<float>::Max();
// MoveToActor stops on overlap, which adds both capsule radii to the acceptance radius
const float Radii = Monster->GetSimpleCollisionRadius() + Target->GetSimpleCollisionRadius();
MoveTowards(Target->GetActorLocation(), FMath::Max(Monster->AttackRange * 0.5f - Radii, 5.0f), Target.Get());Goblins also stopped strafing. In the original they always turn and run where they are going and only face you when they stop, so that is what they do now, with a single hop back after they swing. A goblin that dies while already knocked down now dies where it lies instead of standing up to play its collapse, and goblins that come across a body of their own kind or of a human raise the alarm and search the area.
A shield that works like a shield
Blocking used to let 15% of every blow through, and every blocked hit counted towards a stagger. The original works differently: a shield blocks any blow up to its blocking ability, as often as your stamina allows, and a stronger blow breaks the guard outright. Stamina does not recover while you hold the guard. A blocked hit turns you to face the attacker, and the attacker recoils off the shield.
float ADDArisen::ResolveGuardedKnockdown(float Power, AActor* Attacker)
{
if (GetGuardFactor(Attacker, true) >= 1.0f)
{
return Power; // not guarding, or hit from outside the shield's arc
}
if (Power <= ShieldBlockingAbility)
{
return 0.0f; // fully blocked, no build-up
}
bPendingGuardBreak = true; // plays the guard-broken reaction
return FMath::Max(Power, Stability);
}The Arisen also finally reacts to being hit: a flinch for light blows, a heavy push for strong ones, falling back and getting up for a knockdown, each from the front or behind. How long you lose control after each one matches the original, down to the frame.
Hits that land on the right frame
Some attacks connected before the weapon arrived. For goblins it was up to a full second early. Every attack now uses the original's own hit windows, stored per clip in a small data table, and the hit check stays live through the whole window at 30 checks per second, the same rate the original ran at. Each target is hit once per blow, so multi-hit swings still work.
// One more check 1/30 s later while the window is open; a later swing makes pending checks stale
const int32 Serial = StepSerial;
World->GetTimerManager().SetTimer(Handle, FTimerDelegate::CreateWeakLambda(this, [this, Serial]()
{
if (Serial != StepSerial || !IsActive())
{
return;
}
PerformHit(ActiveSteps()[CurrentStep]);
ScheduleWindowSample();
}), FMath::Max(Next - Now, 0.001f), false);The same pass fixed the light combo, which only chained into the next swing at 0.77 s. The original accepts it at 0.43 s, so it does now. Sprint, guard walk and the jump arc also moved to the original's numbers, and the goblin's swing whoosh now plays just before each blow lands instead of on a fixed timer.
A jog that does not hitch
The jog had a small stumble every second step. It turned out that each of our looping animation clips carried one stray first frame with the knees nearly straight, so once per cycle the legs snapped for a couple of frames. Instead of editing every clip by hand, the anim instance now detects it from the data and skips it:
// A defective loop: the second key matches the last key, but the first key is far from both
if (FMath::RadiansToDegrees(Second.AngularDistance(Last)) < 2.0f &&
FMath::RadiansToDegrees(First.AngularDistance(Second)) > 20.0f)
{
Start = static_cast<float>(Frame); // the clean loop is [one frame, end]
}The jog and walk speeds also now match the clips' own travel, so the feet no longer play faster than the ground moves under them.
Small things that added up
- Enemy health bars no longer show from across the map, and a full bar never shows at all.
- The menu sound picker used to save every sound you auditioned, so the last one you heard became the menu sound. It previews now, and you keep a sound on purpose.
- Dragging the brightness slider no longer machine guns the tick sound.
Next: all of Gransys
The big one is starting. The open world is being rebuilt at full scale, about 2.9 by 3.0 km, on Unreal 5.8's new Mesh Terrain instead of a classic Landscape, because the world's cliffs and overhangs would not survive a heightmap. The ground arrives as 417 tiles of 100 by 100 m, and the good news is that the whole terrain build can be driven from our own C++ editor module: create the World Partition map, feed each tile in at its real position, add remesh and detail modifiers, paint the ground layers, place every tree and bush, then build. No editor panel clicks.
It starts with a 3 by 3 tile pilot around the first village, which gets walked and tuned by eye, and then grows to the whole map with Unreal's water system for the sea, lakes and rivers.






