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 all
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;
}
}
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 all
Vector3 startPos = shooter.Pos; // Feet
startPos.Z += shooter.Height / 2;
Code: Select all
Vector3 startOffset = (16, 4, 24);
startOffset.XY = Actor.RotateVector(startOffset.XY, shooter.Angle);
Vector3 startPos = shooter.Pos + startOffset;
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 all
Vector3 direction;
direction.xy = Actor.AngleToVector(shooter.Angle);
direction.z = sin(-shooter.Pitch);
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 all
Actor sectorGetter = Actor.Spawn("Actor", startPos, NO_REPLACE);
Sector startSector = sectorGetter.CurSector;
sectorGetter.Destroy();
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 all
MyLineTracer tracey = new("MyLineTracer");
...
tracey.Source = shooter;
tracey.Trace(startPos, shooter.CurSector, direction, 8192.0, TRACE_HitSky);
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 all
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;
}
...
}
}
Code: Select all
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)
{
...
}
}
}
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 all
class MyLineTracer : LineTracer
{
...
override ETraceStatus TraceCallback()
{
if (Results.HitType == TRACE_HitWall)
{
return TRACE_Stop;
}
}
}
Code: Select all
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
}
}
Code: Select all
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 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 all
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 all
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); } } }