Movement Code With Smooth Turning?

Requests go in THIS forum!
Forum rules
The Projects forums are only for projects. If you are asking questions about a project, either find that project's thread, or start a thread in the General section instead.

Got a cool project idea but nothing else? Put it in the project ideas thread instead!

Projects for any Doom-based engine (especially 3DGE) are perfectly acceptable here too.

Please read the full rules for more details.
User avatar
Enjay
 
 
Posts: 26868
Joined: Tue Jul 15, 2003 4:58 pm
Location: Scotland

Movement Code With Smooth Turning?

Post by Enjay »

I have seen some mods do this with ACS that was complicated enough to make my brain bleed (and way too complicated for me to implement).
I've also had a search around to see if there is a library anywhere that someone might have made, but I came up blank.

What I would really like is something that allows actors to turn more slowly ans smoothly instead of snapping to the usual 8 directions of Doom actor travel. Perhaps a custom alternative to A_Chase would do it? Preferably it would be something in ZScript that would then be reasonably easy to plug into any actors that I wanted to edit.

I'd only want to use it on actors that are replaced by models because it looks a bit janky when a fully 3D creature (and, even worse, a vehicle) suddenly snaps 45° degrees from one direction to another. I know that GZDoom has a built-in interpolation effect that does help with this (+INTERPOLATEANGLES flag), but something even smoother would be nice.

So, does such a thing already exists?
If so, can anyone point me to it?
If not, is it something that someone would be willing to have a go at creating? (I know it's beyond my capabilities.)

Thanks for looking. :)
User avatar
Cherno
Posts: 1334
Joined: Tue Dec 06, 2016 11:25 am

Re: Movement Code With Smooth Turning?

Post by Cherno »

https://pastebin.com/Uym2dpZn
smoothmoveturn.pk3

Add mixin SmoothMoveTurnMixin to monster actor.
Call ChaseSmooth() instead of A_Chase.
Last edited by Cherno on Sun Feb 23, 2025 9:15 am, edited 1 time in total.
User avatar
Enjay
 
 
Posts: 26868
Joined: Tue Jul 15, 2003 4:58 pm
Location: Scotland

Re: Movement Code With Smooth Turning?

Post by Enjay »

Oh wow, thank you. I honestly thought this might be too big an ask.

However, I've come up with a couple of problems. The first one is probably easy:

I've never used mixins before, so I'm not sure about any peculiarities of them. I tried putting your code into a files called smoothturn.zs and then loading it from my main zscript file like so:

Code: Select all

 #include "zscript/smoothturn.zs"
GZDoom keeps telling me it can't find the file. I've checked and double checked the path, and filename, put it in different locations with different filenames and so on but no joy. Can mixins be loaded this way?

Anyway, I've worked around that by just pasting the code into my main zscript file for the moment.

The second problem involves a VM Abort. Just to see if I can get the code working, I have tried to use it on a simple zombieman clone that replaces the normal one (made to look smaller so I know it has loaded). When I fire up GZDoom, I see the replacement mini-zombies standing there with their backs to me at the start of MAP01 (as usual) but, when I fire my weapon, GZDoom gives me the following message.

Code: Select all

VM execution aborted: tried to read from address zero.
Called from ZombieTest.ChaseSmooth at zscript.zs, line 75
Called from state ZombieTest.2 in ZombieTest
Called from state ZombieTest.1 in ZombieTest
Here's the ZScript. What I added is nothing fancy. (The zombie replacement starts after the mixin code.)

Code: Select all

Mixin class SmoothMoveTurnMixin
{
    //ChaseSmooth works by  storing the state ChaseSmooth was called from, and using that information along with
    //the actor's tics value to determine if the ChaseSmoth call each tic should have the chance to result in an attack, or just moving & turning.
    //The smooth turning functions work in a similar way.
    Actor smtActor;//the actor instance this mixin belongs to
    State smtChaseState;
    bool smtChasing;//controls wether to call A_Chase or A_Wander in ChaseSmooth
    int smtChaseFlags;//stores any A_Chase flags for use during Tick()
    
    State smtTurnState;
    double smtAngleTarget;
    double smtAngleTurnSpeed;//the difference between starting angle and smtAngleTarget divided by smtTurnState.tics
    double smtAngleTurnSpeedMax;//if > 0, if smtLimitAngleTurnSpeed is true and when chasing or wandering, limit smtAngleTurnSpeed to to this
    bool smtLimitAngleTurnSpeed;//set to true when chasing or wandering
 
    bool affectPitch;
    double smtPitchTarget;
    double smtPitchTurnSpeed;//the difference between starting Pitch and smtPitchTarget divided by smtTurnState.tics
    double smtPitchTurnSpeedMax;//if > 0, if smtLimitPitchTurnSpeed is true and when chasing or wandering, limit smtPitchTurnSpeed to to this
    bool smtLimitPitchTurnSpeed;//set to true when chasing or wandering
 
    property ChaseAffectsPitch : affectPitch;
    
    void InitSmoothMoveTurnMixin(Actor other, double AngleTurnSpeedMax = 0, double PitchTurnSpeedMax = 0)
    {
        smtActor = other;
        smtAngleTurnSpeedMax = AngleTurnSpeedMax;
        smtPitchTurnSpeedMax = PitchTurnSpeedMax;
    }
    
    //To be called from the smtActor's Tick()
    void UpdateSmoothMoveTurn()
    {
        if(smtActor == null)
        {
            return;
        }
        //this chase behavior is called when the monster is in a state that called A_ChaseSmooth, 
        //but is not the first tic in that state
        if(smtChaseState && smtActor.curState && smtActor.tics != smtActor.curState.tics)
        {
            if (smtActor.curState == smtChaseState)
            {
                ChaseSmooth(melee : null, missile : null, chaseFlags : smtChaseFlags, chase : smtChasing);
            }
            else
            {
                smtChaseState = null;
                smtChasing = false;
                smtChaseFlags = 0;
            }
        }
        //this is used for turning smoothly.
        if (smtTurnState && smtActor.curState && smtActor.tics != smtActor.curState.tics)
        {
            if(smtActor.curState == smtTurnState)
            {
                TurnTowards();
            }
            else
            {
                smtTurnState = null;
                smtLimitAngleTurnSpeed = false;
                smtLimitPitchTurnSpeed = false;
            }
        }
    }
    
    //Smooth chasing code by Boondorl
    //Smooth turning code by phantombeta
    void ChaseSmooth(StateLabel melee = '_a_chase_default', StateLabel missile = '_a_chase_default', int chaseFlags = 0, bool chase = true)
    {
        smtChasing = chase;
        smtChaseState = smtActor.curState;
        smtChaseFlags = chaseFlags;
 
        Vector3 tempPos = smtActor.pos;
        double angleTemp = smtActor.angle;
        smtLimitAngleTurnSpeed = true;
        double pitchTemp = smtActor.pitch;
        if (affectPitch)
            smtLimitpitchTurnSpeed = true;
        
        if(chase)
        {
            if(smtActor.target == null)
            {
                return;
            }
            A_Chase(melee,missile,chaseFlags);
        }
        else
        {
            A_Wander(chaseFlags);
        }
 
        Vector3 diff = (smtActor.pos - tempPos);
        Vector3 dir = !(diff ~== (0, 0, 0)) ? diff.Unit() : (0, 0, 0);
        smtActor.SetOrigin(tempPos + dir * smtActor.speed,true);
        
        smtActor.A_SetAngle(angleTemp);
        if (affectPitch)
        {
            smtActor.A_SetPitch(pitchTemp);
        }
        //only rotate for the initial ChaseSmooth call, and not the automatic ones from Tick()
        if(smtActor.tics == smtActor.curState.tics)
        {
            FaceAngleSmooth(smtActor.moveDir * 45, smtActor.moveDir * 45);
        }
    }
 
    //Shortcut for smooth wandering.  
    void WanderSmooth(int flags = 0)
    {
        ChaseSmooth(null, null, flags, false);
    }
    
    //Smooth variant of A_Face(). Pass target as a parameter to replicate A_FaceTarget.
    void FaceSmooth(Actor other = null,int flags = 0)
    {
        if(other == null)
        {
            return;
        }
        double angleTemp = smtActor.angle;
        smtActor.A_Face(other,0,0,0,0,flags);//for pitch setting
        smtActor.A_SetAngle(angleTemp);
        FacePosSmooth(other.pos + (0,0,flags == FAF_MIDDLE ? double(other.height) / 2 : 0));
    }
    //for facing a position
    void FacePosSmooth(Vector3 targetPos)
    {
        Vector3 sCoords = LevelLocals.SphericalCoords(smtActor.pos,targetPos,(smtActor.angle,smtActor.pitch));
        FaceAngleSmooth(smtActor.angle - sCoords.x, smtActor.pitch - sCoords.y);
    }
    
    void FaceAngleSmooth(double a, double p = 0)
    {
        smtTurnState = smtActor.curState;
        smtAngleTarget = a;
        double angDiff = DeltaAngle(smtActor.angle,smtAngleTarget);
        smtAngleTurnSpeed = abs(angDiff / (smtTurnState.tics > 0 ? smtTurnState.tics : 1));
        smtPitchTarget = p;
        double pitchDiff = DeltaAngle(smtActor.Pitch,smtPitchTarget);
        smtPitchTurnSpeed = abs(pitchDiff / (smtTurnState.tics > 0 ? smtTurnState.tics : 1));
        if(smtLimitAngleTurnSpeed && smtAngleTurnSpeedMax > 0)
        {
            smtAngleTurnSpeed = Clamp(smtAngleTurnSpeed,0,smtAngleTurnSpeedMax);
            smtPitchTurnSpeed = Clamp(smtPitchTurnSpeed,0,smtPitchTurnSpeedMax);
        }
        TurnTowards();
    }
    //the base smooth turning function, turns smoothly towards any angle. 
    //This is only  to be called from Tick(), ChaseSmooth, or the other smooth rotating functions.
    protected void TurnTowards()
    {
        double angDiff = DeltaAngle(smtActor.angle,smtAngleTarget);
        double angleChange = angDiff < 0 ? max(-smtAngleTurnSpeed, angDiff) : min(smtAngleTurnSpeed, angDiff);
        smtActor.A_SetAngle(smtActor.angle + angleChange);
 
        if (affectPitch)
        {
            double pitDiff = DeltaAngle(smtActor.pitch,smtpitchTarget);
            double pitchChange = pitDiff < 0 ? max(-smtpitchTurnSpeed, pitDiff) : min(smtpitchTurnSpeed, pitDiff);
            smtActor.A_Setpitch(smtActor.pitch + pitchChange);
        }
        //console.printf("[SmoothMoveTurnMixin/TurnTowards] smtAngleTarget = %f, angDiff = %f, smtAngleTurnSpeed = %f, angleChange = %f, angle = %f",smtAngleTarget,angDiff,smtAngleTurnSpeed,angleChange,smtActor.angle);
    }
}



class ZombieTest : Actor replaces Zombieman
{
	mixin SmoothMoveTurnMixin;
	
	Default
	{
		Scale 0.8;
		Health 20;
		Radius 20;
		Height 56;
		Speed 8;
		PainChance 200;
		Monster;
		+FLOORCLIP
		SeeSound "grunt/sight";
		AttackSound "grunt/attack";
		PainSound "grunt/pain";
		DeathSound "grunt/death";
		ActiveSound "grunt/active";
		Obituary "$OB_ZOMBIE";
		Tag "$FN_ZOMBIE";
		DropItem "Clip";
	}
	States
	{
	Spawn:
		POSS AB 10 A_Look;
		Loop;
	See:
		POSS AABBCCDD 4 ChaseSmooth();
		Loop;
	Missile:
		POSS E 10 A_FaceTarget;
		POSS F 8 A_PosAttack;
		POSS E 8;
		Goto See;
	Pain:
		POSS G 3;
		POSS G 3 A_Pain;
		Goto See;
	Death:
		POSS H 5;
		POSS I 5 A_Scream;
		POSS J 5 A_NoBlocking;
		POSS K 5;
		POSS L -1;
		Stop;
	XDeath:
		POSS M 5;
		POSS N 5 A_XScream;
		POSS O 5 A_NoBlocking;
		POSS PQRST 5;
		POSS U -1;
		Stop;
	Raise:
		POSS K 5;
		POSS JIH 5;
		Goto See;
	}
}
Any ideas?
User avatar
Cherno
Posts: 1334
Joined: Tue Dec 06, 2016 11:25 am

Re: Movement Code With Smooth Turning?

Post by Cherno »

Override the monster's PostBeginPlay and call InitSmoothMoveTurnMixin(self,5,5);
Override the monster's Tick and call Super.Tick() and UpdateSmoothMoveTurn();
User avatar
Enjay
 
 
Posts: 26868
Joined: Tue Jul 15, 2003 4:58 pm
Location: Scotland

Re: Movement Code With Smooth Turning?

Post by Enjay »

Thanks again.
I just wanted to check that the following is correct. Certainly, the game starts and my little mini zombies are running around in a very non-Doom-like way. So, it seems to be OK, but I haven't done this before.

I just used examples from the Wiki and adapted them:

Code: Select all

	override void PostBeginPlay()
	{
		InitSmoothMoveTurnMixin(self,5,5);
	}

	Override Void Tick()
	{
		Super.Tick();
		If (IsFrozen()) Return;
		UpdateSmoothMoveTurn();
	}
The example for overriding PostBeginPlay had Super.PostBeginPlay(); in it but that doesn't seem to be needed. Is that correct?
The If (IsFrozen()) Return; line, according to the wiki, isn't needed, but is advised.
User avatar
Enjay
 
 
Posts: 26868
Joined: Tue Jul 15, 2003 4:58 pm
Location: Scotland

Re: Movement Code With Smooth Turning?

Post by Enjay »

OK, I got it working on an actual model-based actor.
The smooth turning and walking in general looks great.

However, the actor seems to find it very easy to get stuck on slopes and drop-offs - to the point that every time I have tried it
- even in a relatively simple area, it has managed to get stuck. :(

I'll tweak some settings to see if I can figure out a way to minimise this, but it is problematic enough to be prohibitive at the moment.
User avatar
Cherno
Posts: 1334
Joined: Tue Dec 06, 2016 11:25 am

Re: Movement Code With Smooth Turning?

Post by Cherno »

Keep in mind that the actor's speed value needs to be set equal to [original speed / movement tics], otherwise monsters are far too fast and might have trouble navigating. Also, for vanilla Doom maps, a lot of monsters have spawn locations that make them move into walls and get stuck the very first time they call A_Chase with different speed values.
User avatar
Enjay
 
 
Posts: 26868
Joined: Tue Jul 15, 2003 4:58 pm
Location: Scotland

Re: Movement Code With Smooth Turning?

Post by Enjay »

OK, that helps. Thank you. :)
User avatar
Enjay
 
 
Posts: 26868
Joined: Tue Jul 15, 2003 4:58 pm
Location: Scotland

Re: Movement Code With Smooth Turning?

Post by Enjay »

I've been trying this on a few enemies and it's working very nicely for most of them. If the enemy is fairly standard, then it does the job very nicely. I still find a few getting stuck even on slight slopes or against walls - even very slow moving ones, but I have been able to work around it with careful monster placement for those affected.

However, I have a few enemies that have more complex behaviour, or can switch from walking to flying (e.g. I have one very similar to the Strife Inquisitor) and this can end up creating some weirdness. e.g. the enemy flying away from you, but facing in your direction with the spawned actors that look like a jet effect coming out of their front instead of their back.

In those specific cases, I think all that would be needed would be to only slow down how quickly the actor turns and leave the rest of the movement logic alone. I know that would mean it moving only in the standard 45 degree angles, but that would be acceptable in the cases that I have in mind.

So, it it possible to only slow the turning? If so, how?
User avatar
Cherno
Posts: 1334
Joined: Tue Dec 06, 2016 11:25 am

Re: Movement Code With Smooth Turning?

Post by Cherno »

Set smtTurnSpeedMax to 15 or something.
User avatar
Enjay
 
 
Posts: 26868
Joined: Tue Jul 15, 2003 4:58 pm
Location: Scotland

Re: Movement Code With Smooth Turning?

Post by Enjay »

I'm sorry to be a pain, and I really appreciate the input, but I just can't figure out how to do that.

I've tried it in the default block, in override void PostBeginPlay(), and in Override Void Tick() but I'm either entering it wrong, or that's not how it's done. Does this require a simple mixin with only that change or something?
User avatar
Cherno
Posts: 1334
Joined: Tue Dec 06, 2016 11:25 am

Re: Movement Code With Smooth Turning?

Post by Cherno »

Not sure what to tell you. If you want to limit the turn speed, you set smtTurnSpeedMax from aynwhere.
It can get set when calling InitSmoothMoveTurnMixin in PostBeginPlay() and is set to 0 if you don't pass a value for turnSpeedMax.
User avatar
Enjay
 
 
Posts: 26868
Joined: Tue Jul 15, 2003 4:58 pm
Location: Scotland

Re: Movement Code With Smooth Turning?

Post by Enjay »

Ah, OK, I misinterpreted what you said. I thought you meant I could add it without the rest of the mixin because I was asking about using standard A_Chase behaviour and only changing the turning speed.

However, even with that knowledge, I take it that you meant smtAngleTurnSpeedMax instead of smtTurnSpeedMax? Because smtTurnSpeedMax doesn't appear in your original code and gives errors.

In case you want to see it, here's a quick example of a pretty standard enemy demonstrating one of the problems I have encountered. The enemy is struggling to cope with a simple, small slope. (Just to be clear, when using normal A_Chase behaviour, the slope causes no problems.)

(Check your volume - it's a bit loud)


I've also had this enemy walk up to an alcove on the wall that is 32units above the floor (i.e. thay cant get into it) but then get stuck on the wall - unable to go up (as expected) but also unable to pull away from the wall. It doesn't happen all the time, but it does happen.

And, like I said, more complex enemies show other undesirable effects too. So, I was trying to lower my ambitions and only have the smooth turning happen and have all the rest of the walking/moving logic kept as close to vanilla as possible.
User avatar
Cherno
Posts: 1334
Joined: Tue Dec 06, 2016 11:25 am

Re: Movement Code With Smooth Turning?

Post by Cherno »

I have used the smooth moving / turning code in almost all my mods and for some reason never encountered these problems... Not sure what to say. I didn't test it extensively with slops, though. Maybe slope traversing becomes a problem when the speed is very slow, as with your robot.
User avatar
Enjay
 
 
Posts: 26868
Joined: Tue Jul 15, 2003 4:58 pm
Location: Scotland

Re: Movement Code With Smooth Turning?

Post by Enjay »

It certainly happens with faster enemies too. They tend to get stuck for less time though.

I'll try and tweak a few things to see if I come up with anything that works.

Thanks very much for your input. It's appreciated.

Edit: I just tried your Voxel Chibi Doom in the same map and your enemies have no problem with the slope. I'll definitely have to keep poking to see if I can figure out the difference.
Edit2: and I just tried lifting the code and setup from that mod and applying it to my robot and... it still got stuck. :shrug: Strange.

Return to “Requests”