[ZScript] Using LineTracer

Handy guides on how to do things, written by users for users.
Forum rules
Please don't start threads here asking for help. This forum is not for requesting guides, only for posting them. If you need help, the Editing forum is for you.

[ZScript] Using LineTracer

Postby Talon1024 » Sat Nov 24, 2018 11:42 am

The LineTracer class can be a powerful thing, if you know how to use it to your advantage.

I didn't see much in the way of tutorials or resources written about the LineTracer class for ZScript, so here's my tutorial.

LineTracer and TraceCallback
You probably don't want to use the base LineTracer class, since it just stops on the first hit. You probably don't want the trace to stop when it hits the source actor either.

At minimum, you'll want to add an actor pointer to the source actor, so that your tracer knows to ignore the source.

Code: Select allExpand view
class MyLineTracer : LineTracer
{
   Actor Source;
   override ETraceStatus TraceCallback()
   {
      if (Results.HitType == TRACE_HitActor)
      {
         // Ignore source
         if (Results.HitActor == Source)
         {
            return TRACE_Skip;
         }
         return TRACE_Stop;
      }
      return TRACE_Stop;
   }
}


Start vector

Unless you want the tracer to trace from some arbitrary position, the tracer will usually start from a position relative to an actor on the map. However, bear in mind that an actor's position vector is at its "feet". So, you may want to trace from the actor's weapon or torso rather than its feet.

What I usually do is get the height of the actor, divide it by 2, and add it to the start position Z coordinate.

Code: Select allExpand view
Vector3 startPos = shooter.Pos; // Feet
startPos.Z += shooter.Height / 2;


If you want to get fancy, you can additionally make a vector that represents the offset from the actor to the trace start position. Actor.RotateVector will come in handy here.

Code: Select allExpand view
Vector3 startOffset = (16, 4, 24);
startOffset.XY = Actor.RotateVector(startOffset.XY, shooter.Angle);
Vector3 startPos = shooter.Pos + startOffset;


Direction vector

This can be calculated easily. Just make a new vector3, set it's xy (the 2D part of the vector) to a vector calculated from the shooter's angle, and the z part of the vector to the sine of the shooter's inverse pitch (pitch * -1).

Code: Select allExpand view
Vector3 direction;
direction.xy = Actor.AngleToVector(shooter.Angle);
direction.z = sin(-shooter.Pitch);


Sector

Usually, you can use the same sector as the actor you're tracing from. However, the offset position may be in another sector, depending on how far away it is from the source, and how close the actor performing the trace is to a sector boundary.

In that case, you may be able to spawn an actor at the trace start point, and then get the trace start sector from that.

Code: Select allExpand view
Actor sectorGetter = Actor.Spawn("Actor", startPos, NO_REPLACE);
Sector startSector = sectorGetter.CurSector;
sectorGetter.Destroy();


Using the tracer

Now that you've set up the tracer and all the arguments to the Trace function, you can now use it. Don't forget to set the source actor pointer before you perform the trace.
Code: Select allExpand view
MyLineTracer tracey = new("MyLineTracer");
...
tracey.Source = shooter;
tracey.Trace(startPos, shooter.CurSector, direction, 8192.0, TRACE_HitSky);


Getting more results
Maybe you want the tracer to register the player, or specific types of actors. For example, a Portal-like mod may want to have lasers and special objects which deflect them. In that case, I would store each of these as actor pointers on the tracer.

Code: Select allExpand view
class MyLineTracer : LineTracer
{
   Actor Source;
   Actor DeflectorHit;
   Actor PlayerHit;

   override ETraceStatus TraceCallback()
   {
   ...
      if (Results.ActorHit is "LaserDeflector")
      {
         DeflectorHit = Results.ActorHit;
         return TRACE_Stop;
      }
   ...
      if (Results.ActorHit is "PlayerPawn")
      {
         PlayerHit = Results.ActorHit;
         return TRACE_Skip;
      }
   ...
   }
}

After performing the trace, I would check each of these pointers from the actor performing the trace in order to do what I want to do with them.
Code: Select allExpand view
class TraceActor : Actor
{
   ...
   TNT1 A 0 {
      MyLineTracer tracey = new("MyLineTracer");
      ...
      tracey.Trace(startPos, shooter.CurSector, direction, 8192.0, TRACE_HitSky);
      ...
      if (tracey.PlayerHit)
      {
      ...
      }
      if (tracey.DeflectorHit)
      {
      ...
      }
   }
}


Walls

Obviously, walls hit by a tracer will cause Results.HitType to be TRACE_HitWall. But, for tracers, walls are linedefs, which means that a tracer will register a wall hit, even when you don't think it hit anything at all, and it really just hit the middle section of a linedef!

The middle section of a two-sided line is the only section that can be traversed by actors, bullets, and other things. And even then, some linedefs block players and monsters.

The section of a linedef/wall that a tracer hits will be set as Results.Tier, and it can be either TIER_Upper, TIER_Middle, TIER_Lower, or TIER_FFloor, depending on which section of the linedef the tracer hit. You'll usually want to skip the middle section of a linedef, since most linedefs' middle sections allow things to pass through.

This tracer stops when it hits the middle of a two-sided linedef:
Code: Select allExpand view
class MyLineTracer : LineTracer
{
   ...
   override ETraceStatus TraceCallback()
   {
      if (Results.HitType == TRACE_HitWall)
      {
         return TRACE_Stop;
      }
   }
}

This tracer does not stop when it hits the middle section of a two-sided linedef:
Code: Select allExpand view
class MyLineTracer : LineTracer
{
   Actor Source;
   override ETraceStatus TraceCallback()
   {
      if (Results.HitType == TRACE_HitWall)
      {
         if (Results.Tier == TIER_Middle)
         {
            // Pass through two-sided linedefs
            if (Results.HitLine.Flags & Line.ML_TWOSIDED > 0)
            {
               return TRACE_Skip;
            }
            return TRACE_Stop; // Don't pass through any other linedefs
         }
      }
      else if (Results.HitType == TRACE_HitActor)
      {
         // Pass through source
         if (Results.HitActor == Source)
         {
            return TRACE_Skip;
         }
         // Hit all others
         return TRACE_Stop;
      }
      return TRACE_Stop; // Prevent return type mismatch, and hit anything else
   }
}

This tracer stops when it hits hitscan-blocking linedefs and one-sided linedefs:
Code: Select allExpand view
class MyLineTracer : LineTracer
{
   Actor Source;
   override ETraceStatus TraceCallback()
   {
      if (Results.HitType == TRACE_HitWall)
      {
         if (Results.Tier == TIER_Middle)
         {
            if (
               // Stop on one-sided or hitscan-blocking linedefs.
               Results.HitLine.Flags & Line.ML_TWOSIDED == 0 ||
               Results.HitLine.Flags & Line.ML_BLOCKHITSCAN > 0)
            {
               return TRACE_Stop;
            }
            return TRACE_Skip; // Pass through two-sided linedefs that don't block hitscans.
         }
         return TRACE_Stop; // Don't pass through upper, lower, or 3D floors.
      }
      else if (Results.HitType == TRACE_HitActor) // Source actor check
      {
         // Pass through source
         if (Results.HitActor == Source)
         {
            return TRACE_Skip;
         }
         // Hit all others
         return TRACE_Stop;
      }
      return TRACE_Stop; // Don't pass through anything else
   }
}


3D Floors

3D floors are not fully supported in ZScript now, but Marisa Kirisame is currently working on that.

If the tracer hits one of the sides of a 3D floor, Results.HitType will be set to TRACE_HitWall, and Results.Tier will be set to TIER_FFloor. If the tracer hits the flat on the bottom of a 3D floor, it is registered as a ceiling hit (TRACE_HitCeiling), and if the tracer hits the flat on the top of a 3D floor, it is registered as a floor hit (TRACE_HitFloor).

Polyobjects

If a tracer hits a polyobject, it is registered as a wall hit.

Visualizing the trace

Maybe you're confused by some of the results you're getting. There are some things you can do to visualize the trace so you can ensure the tracer is doing what you want it to do.

Here's some ideas:

  • You can spawn certain actors at the start and hit locations
    Code: Select allExpand view
    class TraceActor : Actor
    {
       ...
       TNT1 A 0 {
          MyLineTracer tracey = new("MyLineTracer");
          ...
          tracey.Trace(startPos, shooter.CurSector, direction, 8192.0, TRACE_HitSky);
          ...
          Spawn("TorchTree", startPos, NO_REPLACE);
          Spawn("TorchTree", tracey.Results.HitPos, NO_REPLACE);
       }
    }
  • Copy A_SpawnActorLine into the actor that is using the tracer, and call it using the trace start and hit locations as arguments
    Code: Select allExpand view
    class TraceActor : Actor
    {
       ...
       TNT1 A 0 {
          MyLineTracer tracey = new("MyLineTracer");
          ...
          tracey.Trace(startPos, shooter.CurSector, direction, 8192.0, TRACE_HitSky);
          ...
          A_SpawnActorLine("TorchTree", startPos, tracey.Results.HitPos, 10);
       }
       ...
       action void A_SpawnActorLine(string classname, Vector3 pointA, Vector3 pointB, double units = 1)
       {
          // get a vector pointing from A to B
          let pointAB = pointB - pointA;

          // get distance
          let dist = pointAB.Length();

          // normalize it
          pointAB /= dist == 0 ? 1 : dist;
          
          // iterate in units of 'units'
          for (double i = 0; i < dist; i += units)
          {
             // we can now use 'pointA + i * pointAB' to
             // get a position that is 'i' units away from
             // pointA, heading in the direction towards pointB
             let position = pointA + i * pointAB;
             Spawn(classname, position);
          }
       }
    }
Last edited by Talon1024 on Sun Dec 02, 2018 5:13 pm, edited 5 times in total.
Talon1024
 
 
 
Joined: 27 Jun 2016
Github ID: Talon1024
Operating System: Debian-like Linux (Debian, Ubuntu, Kali, Mint, etc) 64-bit
Graphics Processor: nVidia with Vulkan support

Re: [ZScript] Using LineTracer

Postby Nash » Sat Nov 24, 2018 11:51 am

Talon1024 wrote:Copy A_SpawnActorLine into the actor that is using the tracer, and call it using the trace start and hit locations as arguments


No such thing exists in the engine. Did you forget to post something?
User avatar
Nash
 
 
 
Joined: 27 Oct 2003
Location: Kuala Lumpur, Malaysia
Github ID: nashmuhandes
Operating System: Windows 10/8.1/8 64-bit
Graphics Processor: nVidia with Vulkan support

Re: [ZScript] Using LineTracer

Postby Talon1024 » Sat Nov 24, 2018 2:22 pm

Yeah, I forgot to link the ZDoom wiki article where A_SpawnActorLine is.
Talon1024
 
 
 
Joined: 27 Jun 2016
Github ID: Talon1024
Operating System: Debian-like Linux (Debian, Ubuntu, Kali, Mint, etc) 64-bit
Graphics Processor: nVidia with Vulkan support

Re: [ZScript] Using LineTracer

Postby Apeirogon » Sat Nov 24, 2018 4:27 pm

Well, this tutorial is more like a pile of "linetracer can do this...and this...and this...oooo I almost forgot that linetracer can also do this. linetrace is definitely you bro". Add more examples and explanations for non zscript folks.
Because, as example, source pointer in tracer class, if not read manual carefully, "magically" point to actors which "shoot" it.
Apeirogon
I have a strange sense of humour
 
Joined: 12 Jun 2017


Return to Tutorials

Who is online

Users browsing this forum: No registered users and 1 guest