Context-sensitive actions

Wed Jan 19, 2022 5:45 pm

I would like to create context-sensitive actions. That is, different things would happen depending on what the player is looking at. Also on the status bar I'd like to tell the player what will happen if they do the action, before they actually do it.

I think the way to implement this is:
repeatedly fire off a line trace, and store the resulting FLineTraceData somewhere.
Put a function in each weapon, like DescribeAction(FLineTraceData) that returns a string telling what will happen if the weapon is fired at that target.
The status bar pulls that stored FLineTraceData and runs in through the player's current weapon's DescribeAction function, then prints that resulting string on the screen somewhere.
Also in the weapon I need to write code to pick up that stored target and act on it in the fire state.
The reason I want to store that LineTrace instead of firing a new one every time is because I don't want the printed description and actual action to ever become de-synchronized.

So my questions are - first, is this the best way to implement this or is there something else? And is there any existing functionality similar to this already happening that I can leverage? If not, where are the best places to fire that LineTrace from and where to store it's FLineTraceData where the statusbar and weapon can pick it up?

For a visual of what I'm going for:
Spoiler:

Re: Context-sensitive actions

Thu Jan 20, 2022 2:13 am

Sir Robin wrote:is there any existing functionality similar to this already happening that I can leverage?

I don't think there is any existing functionality for context-sensitive actions in GZDoom itself, because none of the games it supports out of the box have that feature.

Sir Robin wrote:where are the best places to fire that LineTrace from and where to store it's FLineTraceData where the statusbar and weapon can pick it up?

First of all, note that UI code can only read the playsim data. This means that if you do a line trace from your HUD code, you can only use the trace result to describe what kind of attack (if any) your weapon will peform, but you won't have access to this data in your weapon code when it actually performs an attack. For both the HUD and the weapon to be able to access the trace result and act on it, the code that does the trace has to be on the playsim side. I suggest you put this functionality in your player class code - this way, your HUD will be able to access the result to provide descriptions for generic actions (e.g. look, use etc.), and your weapons will use it both to provide descriptions for their attacks (queried by the HUD) as well as to actually pick an attack in the fire state.

Re: Context-sensitive actions

Thu Jan 20, 2022 6:30 pm

Player701 wrote:
Sir Robin wrote:is there any existing functionality similar to this already happening that I can leverage?

First of all, note that UI code can only read the playsim data. This means that if you do a line trace from your HUD code, you can only use the trace result to describe what kind of attack (if any) your weapon will peform, but you won't have access to this data in your weapon code when it actually performs an attack. For both the HUD and the weapon to be able to access the trace result and act on it, the code that does the trace has to be on the playsim side. I suggest you put this functionality in your player class code - this way, your HUD will be able to access the result to provide descriptions for generic actions (e.g. look, use etc.), and your weapons will use it both to provide descriptions for their attacks (queried by the HUD) as well as to actually pick an attack in the fire state.


That makes sense, thanks for clearing that up. And I've been thinking about it, I'll actually need at least 3 line traces done - because vision, weapon, and use might have different targets. For example if the player is standing next to a useable line def, and there is a monster on the other side of it, and a non-blocking decoration between the monster and player. The the use trace should stop at the linedef, the vision should stop at the decoration and the weapon use trace should go all the way to the monster.

So I think I understand how to do this conceptually, just still not sure where to start coding. I need to add these holders for the line traces, so I found this wiki page:
ZScript custom properties - I think that lets me create variables on the player class and make them available to other functions (like the hud and weapons)

But where do I put the call to LineTrace and how to I make it get called every game frame?

Another question - the FLineTraceData has a property called Vector3 HitLocation that tells where the location where the ray trace stopped. how can I put a marker (I'm guessing a sprite?) at the location for just this frame and hen move it next frame?

Re: Context-sensitive actions

Thu Jan 20, 2022 11:10 pm

So here's what I've got going so far:
I found a function called Tick() that gets called every tic. That's only 35 times a second, so on games running 60 or 90 or 120 FPS, it's only getting called about every 2nd, 3rd, or 4th frame. That seems kinda slow but until I find a better place, I'll try this.
I put the linetrace call in the tick function, and I think it's working. I haven't figure out a way to get that info up on to the status bar yet. I see in SBARINFO a function called DrawString, but the most flexible variable it seems to accept is a global array, and I haven't figured out how to set those yet. Also, SBARINFOis depreciated and I need to learn how to do this in ZScript.
I did figure out how to create custom user CVARs and put my trace data in a string there. Then I can go to the console and print the string manually from there.
But I was getting weird data. Traces didn't seem to be going where I expected them to. Then I figured it out - the trace ray is not originating from the player, it's originating from (0,0) on the map. I see this line on the LineTrace page:
The line originates from the calling actor's exact position
So that tells me that the player actor isn't the one calling the tick() function. So I think I need to put this linetrace call somewhere else where the player actor calls it. But not sure where yet.
I also tried spawning a bullet puff at the hitlocation of the linetrace. I was getting errors saying that the spawn function had too many arguments. I found out that the Spawn function in the wiki is for ACS. For ZScript I need to use the actor static spawn function. Someone should make a note of that in the wiki, it's not obvious.

Re: Context-sensitive actions

Fri Jan 21, 2022 3:35 am

Sir Robin wrote:I found a function called Tick() that gets called every tic. That's only 35 times a second, so on games running 60 or 90 or 120 FPS, it's only getting called about every 2nd, 3rd, or 4th frame. That seems kinda slow but until I find a better place, I'll try this.

Yes, the game logic runs only 35 times per second regardless of the actual frame rate. Because of that, if you want your weapon to select an attack based on the result of a line trace, you cannot call it more often than that. Well, technically, you can, since you can do multiple calls in a single tic, but it would be pointless in this case. If, however, you need to do separate traces with different parameters, you can move those that you will only use in your HUD to your HUD code, since it gets called every frame instead of every tic. But there is a catch: for some reason, you cannot actually use LineTrace from ui context (I don't really know why, as it doesn't seem to modify any game data), you will have to use LineTracer instead.

Sir Robin wrote:I did figure out how to create custom user CVARs and put my trace data in a string there. Then I can go to the console and print the string manually from there.

Note that you can store the data directly in your actor(s), so a CVAR would only be useful for debugging. You shouldn't use a CVAR to store data for later use by your code, since it can be modified by the user at will; also, accessing user CVARs from ZScript code requires extra care to avoid breaking multiplayer.

Sir Robin wrote:So that tells me that the player actor isn't the one calling the tick() function.

You should override Tick() for your player class (the one you inherit from PlayerPawn or DoomPlayer or some other built-in player). If you want call it from an inventory item (e.g. weapon) instead, then you will have to use the owner pointer:

Code:
override void Tick()
{
    Super.Tick();

    if (Owner != null)
    {
        Owner.LineTrace(...);

        // TODO: do stuff based on the trace result
    }
}

Re: Context-sensitive actions

Fri Jan 21, 2022 11:36 am

Player701 wrote:for some reason, you cannot actually use LineTrace from ui context (I don't really know why, as it doesn't seem to modify any game data), you will have to use LineTracer instead.

Okay, I'll look into that linetracer. It seems that the biggest difference is that it takes an origin point as a parameter instead of assuming the caller.
Ok, so here's the function:
bool Trace(vector3 start, Sector sec, vector3 direction, double maxDist, ETraceFlags traceFlags)
start I get from the player, what do I put for sec? And how do I get direction from the player's angle and pitch?

Player701 wrote:Note that you can store the data directly in your actor(s), so a CVAR would only be useful for debugging. You shouldn't use a CVAR to store data for later use by your code, since it can be modified by the user at will; also, accessing user CVARs from ZScript code requires extra care to avoid breaking multiplayer.

I understand that I want to store this info in the player, but I don't yet have a way to retrieve it. So I put it in cvar so that I can check it from the console. That's just until I figure out how to display the variable properly from the HUD. And the player can't break it really. It's getting updated 35 times a second, so even if they change it, it gets changed back 1/35 of a second later. Anyway, this is just for debugging, won't be in the final code.

Player701 wrote:You should override Tick() for your player class (the one you inherit from PlayerPawn or DoomPlayer or some other built-in player).

This is what I'm doing in the player.Tick():
Code:
   bool HasTargetLook;
   FLineTraceData TargetLook;
   
   property HasTargetLook: HasTargetLook;
   property HasTargetAlt: HasTargetAlt;
   
   override void Tick(){
      //This has something to do with Voodoo dolls
      if (!player || !player.mo || player.mo != self){
         Super.Tick();
         return;
      }
      
      Super.Tick();
      
      HasTargetLook = LineTrace(
         angle,
         4096,
         pitch,
         TRF_THRUHITSCAN || TRF_ALLACTORS,
         offsetz: ViewHeight,
         data: TargetLook
      );
      
      CVar UW_PlayerLook = CVar.FindCVar('UW_PlayerLook');

      String LookDescription='[null]';
      
      if (HasTargetLook){
         switch(TargetLook.HitType){
            case TRACE_HitNone: LookDescription='nothing'; break;
            case TRACE_HitFloor: LookDescription='floor'; break;
            case TRACE_HitCeiling: LookDescription='ceiling'; break;
            case TRACE_HitWall: LookDescription='wall'; break;
            case TRACE_HitActor: LookDescription='actor'; break;
            case TRACE_CrossingPortal: LookDescription='portal'; break;
            case TRACE_HasHitSky: LookDescription='sky'; break;
         }
         LookDescription=LookDescription..' distance='..TargetLook.Distance;
         Actor.Spawn("UW_CursorPuff",TargetLook.HitLocation);
      } else {
         LookDescription='not a damn thing';
      }
      UW_PlayerLook.SetString(LookDescription);
      
   }

But even though that linetrace is in the player tick, it's originating from (0,0,0), so that tells me that the player doesn't actually call the tick function.


I think if I could find a mod with a laser sight or enemy health gauge that would get me close to what I'm trying to do. But all the ones I've found so far are laggy and/or written in ACS, and I'm trying to stick to ZScript, if that's the new thing to use.

Re: Context-sensitive actions

Fri Jan 21, 2022 11:58 am

Nevermind the question about sec - this page tells me that every actor has a cursector property, I'm pretty sure that's what I need to use there.

Re: Context-sensitive actions

Fri Jan 21, 2022 12:03 pm

First of all, to address your problem:
Sir Robin wrote:even though that linetrace is in the player tick, it's originating from (0,0,0), so that tells me that the player doesn't actually call the tick function.

The last part of that statement is not correct. The player does call the function because you have defined it there. It simply cannot be any other way. Yet, the trace always originates from (0, 0, 0). How could this be? Let's take a closer look at your code:
Sir Robin wrote:
Code:
HasTargetLook = LineTrace(
         angle,
         4096,
         pitch,
         TRF_THRUHITSCAN || TRF_ALLACTORS,
         offsetz: ViewHeight,
         data: TargetLook
      );

You are using the logical "or" || operator here, which returns true (1) if at least one of its operands is true. Both TRF_THRUHITSCAN and TRF_ALLACTORS are non-zero constants, so they evaluate to true, which means the result is also true, or 1, which also happens to be the value of TRF_ABSPOSITION. This is why the origin of the trace is always at zero. To fix this, use the bitwise "or" | instead of the logical "or" ||.

I've also noticed this part:
Sir Robin wrote:
Code:
property HasTargetLook: HasTargetLook;
property HasTargetAlt: HasTargetAlt;

Actor properties are only useful for setting up inherited classes, they are only used in the Default block of the class definition. Only this part is enough to access the values as well as modify them (except from the ui context):
Sir Robin wrote:
Code:
bool HasTargetLook;
FLineTraceData TargetLook;


Sir Robin wrote:And how do I get direction from the player's angle and pitch?

This is a bit tricky, because it requires some trigonometry. But we can cheat and look into the C++ source code for LineTrace:

Code:
double pc = pitch.Cos();
direction = { pc * angle.Cos(), pc * angle.Sin(), -pitch.Sin() };

Translated to ZScript, that would be (provided you have the angle and the pitch):

Code:
double pc = Cos(pitch);
let direction = (pc * Cos(angle), pc * Sin(angle), -Sin(pitch));

Re: Context-sensitive actions

Fri Jan 21, 2022 8:32 pm

Player701 wrote:You are using the logical "or" || operator here, which returns true (1) if at least one of its operands is true. Both TRF_THRUHITSCAN and TRF_ALLACTORS are non-zero constants, so they evaluate to true, which means the result is also true, or 1, which also happens to be the value of TRF_ABSPOSITION. This is why the origin of the trace is always at zero. To fix this, use the bitwise "or" | instead of the logical "or" ||.

Good catch! Yeah, I would not have found that, I just don't know the syntax well enough yet. It's like = vs == where you can't spot the problem if you don't know to look for it.

Player701 wrote:Actor properties are only useful for setting up inherited classes, they are only used in the Default block of the class definition.

Thanks for the tip. I actually clipped out code that I didn't think was relevant to my question, just to simplify my post. I meant to clip that bit but missed it. Now I'm glad I didn't.

Player701 wrote:This is a bit tricky, because it requires some trigonometry.
Code:
double pc = Cos(pitch);
let direction = (pc * Cos(angle), pc * Sin(angle), -Sin(pitch));

Awesome! Thanks for taking the time to look up that function and give me a functional answer. This whole post has been super helpful. So now the linetrace is working as I expect it to, next I need to work on my status bar. I'll be back with any more questions.

For anyone reading along, wanting to change their statusbar but not sure where to start, the Classes:BaseStatusBar wiki page does not currently have any useful code examples on it, but the Classes:HUDFont page does, of all places. Found that on accident, but very glad I did. Also this post is very helpful. The file that post links for the DI_* flags in broken, here is another one.

Re: Context-sensitive actions

Sat Jan 22, 2022 11:16 am

A few more questions:

Code:
         Actor.Spawn("UW_CursorPuff",TargetLook.HitLocation);

In this line of code I'm spawning a cursor as a puff that exists for only 1 frame. I do this to mark where the LineTrace hit. A few issues with this:
On certain walls (seems to be only 1-sided walls) the puff doesn't show up at all. I haven't been able to determine what walls it works with and what it doesn't - it doesn't seem to be related to direction or height or anything else I can figure out. I can have 2 linedefs, attacked to the same sector, at the same angles, and one works and the other doesn't.
On certain walls (seems to be only 2-sided walls where it hit a lower texture) the actor crawls up the wall, even though the puff vertical speed is set to 0.
What's causing these issues and how to correct them?
Other thing: the actor is always drawn perpendicular to the player. Is there an automatic way to change it's pitch and rotation to match what it hit so that it looks "plastered on"? I think a decal can do this but (from what I'm reading, I haven't tried implementing this yet) decals only apply to walls, not floors or ceilings. I know the long by-hand way would be to determine what was hit and calculate the angles based on that, but is there any build-in function in ZScript for doing that?

A question about functions:
I want to write a function that takes a value between 0.0 and 1.0 and converts it to chars 'A'..'Z'
Pseudocode looks like this: char(toInt(min(max(value,0.0),1.0)*25)+65)
First, does ZScript have a char type or do I just use 1-length strings?
Second what are the function equivalents in ZScript? this page is currently blank, is there a better resource?

Finally, where can I get the world tick count? I want to do an animation manually, so I can calculate the frame number like this:
FrameNumber=mod(toInt(WorldTickCount/TicsPerFrame),FrameCount)
Where do I get that WorldTickCount from, and same as above, what functions do I use in ZScript?

Re: Context-sensitive actions

Sat Jan 22, 2022 1:56 pm

Sir Robin wrote:In this line of code I'm spawning a cursor as a puff that exists for only 1 frame. I do this to mark where the LineTrace hit. A few issues with this:
On certain walls (seems to be only 1-sided walls) the puff doesn't show up at all. I haven't been able to determine what walls it works with and what it doesn't - it doesn't seem to be related to direction or height or anything else I can figure out. I can have 2 linedefs, attacked to the same sector, at the same angles, and one works and the other doesn't.
On certain walls (seems to be only 2-sided walls where it hit a lower texture) the actor crawls up the wall, even though the puff vertical speed is set to 0.
What's causing these issues and how to correct them?

It probably has to do something with the way your UW_CursorPuff class is coded, so if you could post its code here, I might be able to tell you what's wrong with it.

Sir Robin wrote:Other thing: the actor is always drawn perpendicular to the player. Is there an automatic way to change it's pitch and rotation to match what it hit so that it looks "plastered on"? I think a decal can do this but (from what I'm reading, I haven't tried implementing this yet) decals only apply to walls, not floors or ceilings. I know the long by-hand way would be to determine what was hit and calculate the angles based on that, but is there any build-in function in ZScript for doing that?

You are right, decals do not work on floors or ceilings, so you will have to go the hard way. I am unaware of any built-in ZScript functions that would work for your scenario. You will have to use the HitLine field of the trace results to determine the angle. I've never done this myself, but I think you need to use the following formula:

Code:
let l = TargetLook.HitLine;
double angle = atan2(l.v1.p.y - l.v2.p.y, l.v1.p.x - l.v2.p.x)

NB: the above code assumes that HitLine is not null, which may not always be the case. This will get you the angle of the line itself; you have to either add or subtract 90 degrees from it depending on which side was hit (field LineSide of the trace results equals either Line.front or Line.back). I don't have the means to test these formulas now, so you will have to experiment a bit, or wait for someone else to clarify them.

As for the pitch - do you really need to calculate it for your actor? I'm not sure it would have any visual effect... Though I think there is a way to do it, I can look it up if you want.

Sir Robin wrote:A question about functions:
I want to write a function that takes a value between 0.0 and 1.0 and converts it to chars 'A'..'Z'
Pseudocode looks like this: char(toInt(min(max(value,0.0),1.0)*25)+65)
First, does ZScript have a char type or do I just use 1-length strings?

If I remember right, you have to use int for single-bit chars, e.g. int ch = "A". To convert them to a string, use String.format("%c", ch). ZScript code equivalent to your pseudocode would be: (int)(Clamp(value, 0.0, 1.0) * 25 + 65)

Sir Robin wrote:Second what are the function equivalents in ZScript? this page is currently blank, is there a better resource?

I think most, if not all, DECORATE expressions are supported. In addition to that, there are ZScript-exclusive functions too. Most of them can be found in gzdoom.pk3, e.g. zscript/actors/actor.zs for actor-specific functions. There is really a lot of them, so if you're looking for anything specific, don't be afraid to ask. You may also be able to find more info here, particularly in the "ZScript API" section.

Sir Robin wrote:Finally, where can I get the world tick count?

Level.maptime

Re: Context-sensitive actions

Sat Jan 22, 2022 2:42 pm

I'm sorry, can I butt in? What is the difference between time and maptime? I seem to remember that a couple years ago Iron Lich's Whirlwind had level.time in it to determine how often the victim is thrust, and now it's Level.maptime.

Re: Context-sensitive actions

Sat Jan 22, 2022 2:53 pm

Level.time should have been called "hubtime". It is the number of ticks passed since the beginning of the current hub. Level.maptime is the number of ticks passed since first entering the current map within the hub (i.e. exiting and re-entering the map will not reset maptime back to zero).

Re: Context-sensitive actions

Sun Jan 23, 2022 2:51 am

Thanks! I'm sure this info will be very useful.

Re: Context-sensitive actions

Mon Jan 24, 2022 5:15 am

Player701 wrote:It probably has to do something with the way your UW_CursorPuff class is coded, so if you could post its code here, I might be able to tell you what's wrong with it.

Not much to it
Spoiler:


Player701 wrote:As for the pitch - do you really need to calculate it for your actor? I'm not sure it would have any visual effect... Though I think there is a way to do it, I can look it up if you want.

I think I need the pitch to set the angle to match if it hits a ceiling or floor?
Honestly though, this is just cosmetic and for debugging. So if there's not an easy built-in way, I'm not gonna spend too much time on it. Back burner for now.

Player701 wrote:If I remember right, you have to use int for single-bit chars, e.g. int ch = "A". To convert them to a string, use String.format("%c", ch). ZScript code equivalent to your pseudocode would be: (int)(Clamp(value, 0.0, 1.0) * 25 + 65)

Awesome, that works great

Player701 wrote:There is really a lot of them, so if you're looking for anything specific, don't be afraid to ask.

Awesome, you've been great. I ask for resources so I can do some searching myself first, instead of asking every question that pops into my head. I'm sure that would get annoying.

Player701 wrote:Level.maptime

Oh, speaking of levels, how do I get the current level number? Because this one seems to be ACS-only. And the number in the whole game and the number in the cluster?

And I've come up with another problem: I want to predict what will happen if the player does a "use" action, so I'm using the linetrace to find a useable target. I thought that TRF_BLOCKUSE would make it stop at usable linedefs but it does not. It only stops at a linedef if it is 1-sided, or if it's 2-sided when the player is looking at the upper or lower section, but it passes right through the middle section.
Another way to put it: The player stands in front of a linedef, call it linedef1, that is 2-sided and has an action to open door1. Just beyond linedef1 is linedef2, which is 1-sided, and has an action to open door2. My line trace will go to linedef2, but when the player actually uses, it will trigger linedef1. I don't see any combination of flags that will get the trace to stop at that first linedef. Also, and I haven't tested this yet, but in UDMF things can be usable as well, correct? If so then the tracer needs to hit those as well.