Re: Context-sensitive actions

Mon Jan 24, 2022 5:20 am

Player701 wrote: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).

Good question, Kzer-Za. I'm going to guess (haven't tested) that maptime only ticks while you're on the map. So if you leave the map, it will be on the same tic when you return (or maybe 1 tic later).

Doesn't matter which I use, I just need a heartbeat to sync my animations to. But this is good to know.

Re: Context-sensitive actions

Mon Jan 24, 2022 5:43 am

Here's what I've got so far:
UW1.PK3
If you want to see the issue with the cursor:
From the start spot, look down and to the right. There is a brown sack on the ground. Look at the lower texture just below that. The cursor slips on that wall. I thought this was only happening on 2-sided walls, but move to the left, the next wall is 1 sided and the cursor slips on it, but only if you aim lower than the floor next to it.
Next, face North (you'll know you're facing North because of the handy compass in the status bar) and walk until you see a wall. Notice the cursor doesn't appear on this wall at all. Look to the right, there is a wall there, the cursor appears on this one sometimes yes and sometimes no, seems to depend on distance.
Turn around 180, facing Southwest, there is a triangular shelf with a plant on it. Look at the lower left side of the shelf, no issue. Look at the right side, the cursor is slipping. Move to the right and it continues slipping on the 1-sided wall, and even continues around the corner.
Spoiler:

Re: Context-sensitive actions

Mon Jan 24, 2022 5:57 am

Sir Robin wrote:Not much to it
Spoiler:

True, nothing wrong here, and I could not reproduce the "crawling up the wall" issue with this code. But there is indeed a problem that the sprite may be partially or entirely hidden when you spawn the actor directly on a line. To fix this, you can use the HitDir field of the trace results. The length of it is always 1, so if you subtract it from HitLocation, you will get a position that is exactly 1 map unit closer to the player. Adjust as necessary by multiplying HitDir by an arbitrary value, e.g.:

Code:
Spawn('UW_CursorPuff', TargetLook.HitLocation - 2 * TargetLook.HitDir);

Sir Robin wrote:I think I need the pitch to set the angle to match if it hits a ceiling or floor?

No, you don't need the pitch, but you can use the plane normal data to derive the angle. You will also need to handle the 3D floor case as well. Though I haven't done this myself, I may be able to provide example code in case you need it later.

Sir Robin wrote:Awesome, that works great

Yes, and, of course, I meant "single-byte", not "single-bit". Sorry for the typo.

Sir Robin wrote:Oh, speaking of levels, how do I get the current level number?

I think it's Level.levelnum.

Sir Robin wrote: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.

If I remember right, there is a special flag called "block use" that can be set on a line, and LineTrace tests for it specifically. To check for usable lines, you will probably need a custom LineTracer-derived class.

Sir Robin wrote:Good question, Kzer-Za. I'm going to guess (haven't tested) that maptime only ticks while you're on the map. So if you leave the map, it will be on the same tic when you return (or maybe 1 tic later).

Correct. Sorry if I wasn't clear enough in my explanation.

Sir Robin wrote:f you want to see the issue with the cursor

OK, I'll try to check it out later today.

Re: Context-sensitive actions

Mon Jan 24, 2022 6:08 am

nevermind the levels question, found it here

Re: Context-sensitive actions

Mon Jan 24, 2022 8:20 am

OK, so I honestly have no idea what happens with that cursor in your map. It changes position on the second tick only, and since there's no function being called from user code, it looks like the engine is doing something on its own there. Not sure if a bug or not, but the movement code is known to have a lot of quirks that may result in unusual behavior from time to time. Unfortunately, as far as I'm aware, most of them aren't going away due to backwards-compatibility reasons.

I suggest resolving this by adding the NOINTERACTION flag to your actor to render it completely non-existent for the purposes of all collision/physics checks. NOBLOCKMAP and NOGRAVITY are not necessary if NOINTERACTION is set.

Re: Context-sensitive actions

Mon Jan 24, 2022 1:08 pm

Player701 wrote:
Code:
Spawn('UW_CursorPuff', TargetLook.HitLocation - 2 * TargetLook.HitDir);

That works great - Thanks!

Player701 wrote:No, you don't need the pitch, but you can use the plane normal data to derive the angle. You will also need to handle the 3D floor case as well. Though I haven't done this myself, I may be able to provide example code in case you need it later.

If you have example code Ill try it out. I could futz with the trig and eventually figure something out, but I've got 100 features I want to implement and just don't want to get hung up on this one right now.

Player701 wrote:If I remember right, there is a special flag called "block use" that can be set on a line, and LineTrace tests for it specifically. To check for usable lines, you will probably need a custom LineTracer-derived class.

So I need to write my own linetrace code? How involved is that? I suppose I can look at the source for the existing one. It's probably testing for lines that intersect the trace vector, then testing if they pass a test based on the given flags. If so, I just need to duplicate that code and write my own test.

Player701 wrote:OK, so I honestly have no idea what happens with that cursor in your map. It changes position on the second tick only, and since there's no function being called from user code, it looks like the engine is doing something on its own there. Not sure if a bug or not, but the movement code is known to have a lot of quirks that may result in unusual behavior from time to time. Unfortunately, as far as I'm aware, most of them aren't going away due to backwards-compatibility reasons.

I suggest resolving this by adding the NOINTERACTION flag to your actor to render it completely non-existent for the purposes of all collision/physics checks. NOBLOCKMAP and NOGRAVITY are not necessary if NOINTERACTION is set.

I added the nointeraction flag. No need though as your code above fixed my issue. I'd rather the cursor doesn't exist in the second tick, but it has to because it doesn't get drawn in the first tic. I had to do this:
UCSR A 2 Bright;
because with this:
UCSR A 1 Bright;
you'll never see it. It must be spawning in the tick but behind the draw loop, because it never draws that first frame.
If I do this:
TNT1 A 1;
UCSR A 1 Bright;

there is no noticeable change.
As to why it's crawling, I read somewhere that if an actor is exactly on a 2-sided linedef then the engine has to decide if it's on the floor height of the sector of first or second sidedef of that linedef. I thought this was related to that, but since it also happens on 1-sided linedefs, I think not. Also, I'd think that setting nogravity would mean the engine doesn't care where it is and leave it alone, but I guess not.

I like your trick to move the spawn location back a unit by using the hitdir. I can't center the cursor on a pixel because the sprite offsets are integers and I need to offset a half pixel. So I can "fix" that by spawning the sprite 0.5 units up and over from the actual hit. I fixed the z by doing this:
Code:
Vector3 CursorLocation=TargetLook.HitLocation - 2 * TargetLook.HitDir;
CursorLocation.z+=0.5;
Spawn('UW_CursorPuff', CursorLocation);

Do you have a clever trick to fix the x/y? Basically I need to shift it 0.5 on the x/y perpendicular to hitdir.

Re: Context-sensitive actions

Mon Jan 24, 2022 1:48 pm

Sir Robin wrote:So I need to write my own linetrace code? How involved is that? I suppose I can look at the source for the existing one. It's probably testing for lines that intersect the trace vector, then testing if they pass a test based on the given flags. If so, I just need to duplicate that code and write my own test.

Here's an example (NB: may be buggy and also doesn't account for the check switch range setting:

Code:
class UseLineTracer : LineTracer
{
    override ETraceStatus TraceCallback()
    {
        if (Results.HitType == TRACE_HitCeiling || Results.HitType == TRACE_HitFloor)
        {
            // cannot go further from here
            return TRACE_Abort;
        }

        if (Results.HitType != TRACE_HitWall)
        {
            // not a wall, go through
            return TRACE_Skip;
        }

        let l = Results.HitLine;
        bool found = false, abort = false;

        if (!(l.flags & Line.ML_TWOSIDED) || (l.flags & (Line.ML_BLOCKUSE|Line.ML_BLOCKEVERYTHING)) || Results.Tier != TIER_Middle)
        {
            // This wall is blocking, so go no further
            abort = true;
        }

        if (l.Activation & SPAC_USE)
        {
            // Line can be used from the front
            found = Results.Side == Line.front;
        }
        else if (l.Activation & SPAC_USEBACK)
        {
            // Line can be used from the back
            found = Results.Side == Line.back;
        }

        return found ? TRACE_Stop : abort ? TRACE_Abort : TRACE_Skip;
    }

    Line GetUseLine(Actor source)
    {
        bool ok = Trace(source.Pos, source.CurSector, GetDir(source), 4096, 0);
        return ok && Results.HitType == TRACE_HitWall ? Results.HitLine : null;
    }

    private vector3 GetDir(Actor source)
    {
        double pitch = source.Pitch;
        double ang = source.Angle;

        double pc = Cos(pitch);
        return (pc * Cos(ang), pc * Sin(ang), -Sin(pitch));
    }
}

Usage:

Code:
let t = new('UseLineTracer');
let useLine = t.GetUseLine(...);

if (useLine != null)
{
    // TODO: Do stuff
    ...
}

I will try to answer the rest tomorrow, unless someone else beats me to it.

Re: Context-sensitive actions

Mon Jan 24, 2022 3:39 pm

Awesome! Ok, so first thing I do is cap the range:
Code:
    Line GetUseLine(PlayerPawn source)
    {
        bool ok = Trace(source.Pos, source.CurSector, GetDir(source), source.UseRange, 0);
        return ok && Results.HitType == TRACE_HitWall ? Results.HitLine : null;
    }

It works when I'm standing in front of the line, and that range is dead-on. However if there is another line between the player and the use line, it seems to block the use trace.

Looking at the logic, I don't see any obvious reason why that is happening.

Re: Context-sensitive actions

Tue Jan 25, 2022 1:38 am

Sir Robin wrote:Awesome! Ok, so first thing I do is cap the range:
Code:
    Line GetUseLine(PlayerPawn source)
    {
        bool ok = Trace(source.Pos, source.CurSector, GetDir(source), source.UseRange, 0);
        return ok && Results.HitType == TRACE_HitWall ? Results.HitLine : null;
    }

It works when I'm standing in front of the line, and that range is dead-on. However if there is another line between the player and the use line, it seems to block the use trace.

Looking at the logic, I don't see any obvious reason why that is happening.

Yeah, like I said, there may be bugs. In this particular case, the problem is that you should increase the Z coordinate of the trace position, otherwise it starts too low on the floor and may not hit what you expect. Spawning an actor (like your cursor) at the trace results' HitPos is helpful to debug these issues.

As for the previous questions... let me show you how to derive the angle from sector planes. Two points to consider here:

  1. If you have access to the plane normal vector N, then atan2(N.Y, N.X) will give you the 2D angle this normal vector is facing on the map. Note that this will obviously not work if both X and Y are zero (i.e. the floor/ceiling must be sloped).
  2. You also need to know whether you hit the plane from the bottom or from the top. If you hit it from the bottom, then the angle has to be reversed. It seems all plane normals face upwards (Z > 0), so if you hit it from the bottom, then the angle between the direction of the hit and the normal will be less than 90 degrees. By definition of Euclidean dot product, this means that the dot product of both vectors is greater than zero.
To get the plane, we can use a simple LineTrace, since it also accounts for 3D floors. Complete code:

Code:
private static double GetHitAngleFromPlane(FLineTraceData t)
{
    bool is3dfloor = t.Hit3DFloor != null;
    SecPlane p;
   
    if (is3dfloor)
    {
        p = t.HitType == TRACE_HitFloor ? t.Hit3DFloor.top : t.Hit3DFloor.bottom;
    }
    else if (t.HitType == TRACE_HitFloor)
    {
        // NB: ternary operator doesn't work here for some reason
        p = t.HitSector.floorplane;
    }
    else // if (t.HitType == TRACE_HitCeiling)
    {
        p = t.HitSector.ceilingplane;
    }

    let n = p.Normal;
   
    if (n.XY.Length() > 0)
    {
        double ang = atan2(n.Y, n.X);
       
        if (t.HitDir dot n > 0)
        {
            ang += 180;
        }
       
        return ang;
    }
   
    return 0;
}

(Note that it is also possible to simply add 180 degrees only if you hit a ceiling or a bottom of a 3D floor. The above code is a bit more compact, although not by much.)

For completeness' sake, I'm also providing the code to derive the angle from a line:

Code:
private static double GetHitAngleFromLine(FLineTraceData t)
{
    let delta = t.HitLine.delta;
    double ang = atan2(delta.Y, delta.X);

    return ang + (t.LineSide == Line.front ? -90 : 90);
}

Glue code to combine both methods depending on what was hit:

Code:
private static double GetHitAngle(FLineTraceData t)
{
    switch (t.HitType)
    {
        case TRACE_HitFloor:
        case TRACE_HitCeiling:
            return GetHitAngleFromPlane(t);
        case TRACE_HitWall:
            return GetHitAngleFromLine(t);
        default:
            return 0;
    }
}

Sir Robin wrote:Do you have a clever trick to fix the x/y? Basically I need to shift it 0.5 on the x/y perpendicular to hitdir.

LineTrace has an argument called offsetside that does exactly what you want. Positive values shift to the right, and negative values shift to the left. Note that shifting the source position is more desirable because otherwise you risk spawning your actor behind a wall. If you still want that, or if you're using LineTracer (since you don't have offsetside there), trigonometry is here to help us again! Given ang as the angle of the source actor, adding (sin(ang), -cos(ang), 0) shifts the source/target position to the right by 1 unit. Multiplying this vector by negative values shifts to the left instead.

Re: Context-sensitive actions

Tue Jan 25, 2022 12:45 pm

That's great. If I make that a wall sprite it matches up to the walls perfectly. On the floor though it needs to lay flat, or match the slope, so that's where the pitch comes in. I have to make it a flatsprite to enable the pitch control. All wall hits get pitched up 90. Also on the floor or ceiling I have to roll it to correct the orientation.
I've wrote functions to do all that, just now tweaking them so they look exactly how I want them. Every time I fix something I break something else. This is the futzing I was trying to avoid doing ;-)
For testing I've switched to a non-symmetrical cursor image so that I can see exactly the orientation.

Re: Context-sensitive actions

Tue Jan 25, 2022 1:29 pm

Yeah, I forgot that pitch works on flatsprites. The equation is -asin(n.Z) where n is the plane normal. When used in combination with the angle (see above), you will also have to negate it if you hit a ceiling or a bottom of a 3D floor (again, see my previous post for some example code). Finally, add 90 degrees to the resulting value, since the default orientation is horizontal instead of vertical.

Re: Context-sensitive actions

Tue Jan 25, 2022 6:42 pm

Player701 wrote:Yeah, I forgot that pitch works on flatsprites. The equation is -asin(n.Z) where n is the plane normal. When used in combination with the angle (see above), you will also have to negate it if you hit a ceiling or a bottom of a 3D floor (again, see my previous post for some example code). Finally, add 90 degrees to the resulting value, since the default orientation is horizontal instead of vertical.

aha, sin - I was using atan2(n.XY.length(), n.Z) and actually before I realized what xy.length() was, I was calculating that with a square root. But since the normal is always a one-unit, sin does the same job for less work. Noted.

Re: Context-sensitive actions

Tue Jan 25, 2022 8:54 pm

So I've got it "working" - the pitch angle and roll all give me a sprite that looks correct, but was having weird trouble trying to get it to hit exactly on the center. I changed the cursor for a non-symmetrical one so I could get a better idea of what's happening with the sprite. Here's what I'm seeing:
Spoiler:

As a wall sprite, your rotation routine is working perfectly. The sprite is orientating at (0,0), you can tell by the bright green dot, my HUD crosshair.
Spoiler:

When I change it to a flat sprite and pitch it up, the orientation is now (xMax,0) for some reason
Spoiler:

and on the floor it's still orientated at (xMax,0) but also drawn x-flipped

Any idea why these things might be happening?

Re: Context-sensitive actions

Tue Jan 25, 2022 9:58 pm

Player701 wrote:Yeah, like I said, there may be bugs. In this particular case, the problem is that you should increase the Z coordinate of the trace position, otherwise it starts too low on the floor and may not hit what you expect. Spawning an actor (like your cursor) at the trace results' HitPos is helpful to debug these issues.

In the trace for the look I used viewheight. I've noticed that it doesn't scale when crouched like AttackZOffset does. is there another way to get the accurate view height?

And it just hit me - all this working trying to predict the use function - can't we just call the actual function that finds a use target, without performing the use? If not, we could at least copy that code into a new function without the use action? That seems a smarter route than trying to emulate what it's doing by hand like we're doing here. This feels like we're trying to re-invent the wheel and just hope it's similar enough to the first wheel to work.

Re: Context-sensitive actions

Tue Jan 25, 2022 11:28 pm

viewheight is a property more than a variable, it indicates the player's default camera height, not the current one.

If you want the actual player camera height, use viewz, which gives the absolute z position of the camera (including bobbing, crouching, climbing stairs, etc). viewz - pos.z will get you the relative camera height from the player's current position.

8-)