[ZScript] Using LineTracer

Handy guides on how to do things, written by users for users.

Moderators: GZDoom Developers, Raze Developers

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.
Post Reply
Talon1024
 
 
Posts: 374
Joined: Mon Jun 27, 2016 7:26 pm
Preferred Pronouns: He/Him
Graphics Processor: nVidia with Vulkan support
Contact:

[ZScript] Using LineTracer

Post by Talon1024 »

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 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;
	}
}
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 all

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 all

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 all

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 all

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 all

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 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;
		}
	...
	}
}
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 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)
		{
		...
		}
	}
}
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 all

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 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
	}
}
This tracer stops when it hits hitscan-blocking linedefs and one-sided linedefs:

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

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);
    		}
    	}
    }
    
Last edited by Talon1024 on Sun Dec 02, 2018 4:13 pm, edited 5 times in total.
User avatar
Nash
 
 
Posts: 17434
Joined: Mon Oct 27, 2003 12:07 am
Location: Kuala Lumpur, Malaysia
Contact:

Re: [ZScript] Using LineTracer

Post by Nash »

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?
Talon1024
 
 
Posts: 374
Joined: Mon Jun 27, 2016 7:26 pm
Preferred Pronouns: He/Him
Graphics Processor: nVidia with Vulkan support
Contact:

Re: [ZScript] Using LineTracer

Post by Talon1024 »

Yeah, I forgot to link the ZDoom wiki article where A_SpawnActorLine is.
User avatar
Apeirogon
Posts: 1605
Joined: Mon Jun 12, 2017 12:57 am

Re: [ZScript] Using LineTracer

Post by Apeirogon »

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.
Post Reply

Return to “Tutorials”