Target with Mouselook (Tracelines)
Author: rrowland (sc2mapster.com)Tags: advanced trigger  
Source: http://forums.sc2mapster.com/resour...Added 13 years ago 

Introduction
Hello all, I've been hanging out for a while using SC2Mapster's resources and sharing the progress of one of my maps with the community. One of the questions I get asked the most is how to detect where the player is looking via MouseLook. The answer is: I use a custom-made traceline function. I want to take the time to explain to the community what a traceline is and how to make one. Please note that although I wrote my code for Reaper Madness in pure script (no triggers via galaxy editor) this tutorial will be explaining how to set it up in Galaxy Editor.


Setting Up Variables and Triggers
First, because Galaxy does not support bulk copy or pointers, and because Galaxy Editor does not support structs, we will need to set up the values that our traceline function calculates as global variables. We want to calculate the three following variables:

1. Current Target - We will want our function to return which unit is currently targeted.
2. Current Targeted Point - We will want our function to return the point the player is targeting, whether it is a unit or terrain. We will use this point to position our impact effects.
3. Current Targeted Height - When our player is targeting a unit, we want to know the height at which the traceline intersected the unit. We will use this point to position our impact effects.

We also need to set them up as arrays because we will need a different set of value for each player. As a lesson in good development practices, I would like to show you how to set up Constant variables, however Galaxy doesn't support setting array lengths by anything except raw numbers so we'll have to do it the hard way.

Make a new folder called "Traceline" so we can separate this function from the rest of your project. Inside the new folder, create another folder named "Variables". Inside the variables folder, make our three variables: traceline_currentTarget (unit), traceline_currentTargetPoint (point) and traceline_currentTargetHeight (real). Turn them into an array by checking the array box. Set the size to the maximum amount of players who can play your game (We'll use 8 as the example):



Now we need a function to write our traceline into. Right click on the "Traceline" folder, hover over "New" and click "New Action Definition". Name it "Traceline":



Our function will need to be passed certain variables as parameters to perform its function, so let's set the parameters up. Our function needs to know:

1. Camera Position - The position of the camera (XY).
2. Camera Height - The height of the camera (Z).
3. Player - The player we're calculating a traceline for.

To create a new parameter, right click on "Parameters", hover over "New" and click "New Parameter". We want to add three: "Camera Position" (point), "Camera Height" (real) and "Player" (integer):



We will also be using Custom Script, so add a new Action of type "Custom Script":



Now we get to the meat of our function. Click on the new custom script action to be greeted with a nice empty box, ready to be filled with juicy target-finding script. While following the tutorial, keep in mind that the editor will change the name of your parameters behind-the-scenes. For instance, the variable "Camera Height" will be renamed "lp_cameraHeight" when referred to in Galaxy Script. The lp_ stands for "Local Parameter", the first letter of our variable is shifted to lowercase and all spaces are removed.


Explanation of Traceline Function
A short intermission to explain what our traceline function will do: Our traceline function is, surprise, going to trace an imaginary line. The beginning of the line is the camera's position and height, the end of the line is 50.0 units in the direction the player is facing. To "trace" the line, we will be checking the position of each point in the line in small increments. For example, if you're at 0,0 at height 0 [0, 0, 0] and you're looking straight east the end of the imaginary line would be at 50, 0 at height 0 [50, 0, 0]. Every 1.0 unit of the line, we will check to see if we've hit anything. This means we run a loop 50 times: First we check [1,0,0], if we find nothing we check at [2,0,0] etc until we hit something.


Inside Our Function
First we need to get the angles of the players camera: The pitch and yaw. First we'll need to create two variables (real) to hold the angles, so create two variables inside the function and call them "Pitch" and "Yaw". For pitch, set the initial value to Camera Pitch of Player (Player) and for yaw set the initial value to Camera Yaw of Player (Player), where the "Player" inside the parenthesis refers to our parameter named "Player":



Last thing before we dive into code: We'll need to set our main unit to a global variable so that we don't end up targeting it in our traceline. Create a variable ON THE LEFT in our trigger list, OUTSIDE of our function named "Main Unit" (unit). In a multiplayer game this variable will need to be an array, as all players will have a different main unit. However, for this example I will only be using 1 main unit.

Next we'll want to start in on our code. As explained earlier, we will be checking each point in small intervals in a loop. So, we need to set up the loop. Create a variable named "Trace Distance" (real) and set its initial value to 0.0. This variable will keep track of how far along the imaginary line we are. Now we'll set up our loop:

while(lv_traceDistance < 50.0) { // While Trace Distance is under our max range, 50, we want to run everything between the squiggly brackets: {}
    // This is where all of our future code is going to go.
    lv_traceDistance += 0.5; // At the end of the loop we increment our distance from the camera by 0.5 units. Since the maximum distance is 50.0, this loop will run up to 100 times.
}


The first things we'll need to do inside our loop is calculate the height and position of our current "step" in the trace. Create a variable called "Trace Height" (real). Inside our loop, we'll calculate the height of the current "step":

if(lv_pitch < 90){
lv_traceHeight = Tan(lv_pitch) * lv_traceDistance * (-1);
} else if(lv_pitch > 270) {
lv_traceHeight = Tan(360 - lv_pitch) * lv_traceDistance;
}


It's not so important that you understand what's going on in the last code segment, but we're finding the Tangent of the angle of lv_Pitch which returns a value between 0 and 1. We multiply that number by the distance we travel every step. For example if we're looking directly toward the sky (90 degree angle) and our trace distance is 0.5 units, the resulting tangent will be 1, and the relative height will be 1 * 0.5 = 0.5, meaning the height at this step in our traceline is 0.5 units higher than the camera's height. As another example, if we're looking forward and up equally (45 degree angle), the resulting tangent will be 0.5 and the relative height will be 0.5 * 0.5 = 0.25, meaning the height at this step in our traceline is 0.25 units higher than the camera's height. If you don't understand all that, it's fine, just continue reading.

Next we need to get the point (XY) where our current step will be located. This one is less complex because Blizzard gave us a function to do the dirty work for us. Create a variable named "Trace Point" (point). We'll get the trace point by calculating it as a polar offset of the camera position. A polar offset means it is a certain distance (offset) away from a point at a given angle (polar). Here's how we call it:

lv_tracePoint = PointWithOffsetPolar(lp_cameraPosition, lv_traceDistance, lv_yaw);

We will need the World Height at the current position of our traceline step, so create a variable named "Trace World Height" (real). This is how we get the height of the world at the current step position:

lv_traceWorldHeight = WorldHeight(c_heightMapGround, lv_tracePoint);

Now that we have the exact information about the current point in our imaginary line, we're going to do a few things to determine if a unit is in our way:

1. Create a small region with a radius of 1.5 at the current traceline point
2. Grab the closest unit to the center of that region, that is within the region
3. Create a region at the unit we found, the size of the unit
4. Get the world height at the position of the unit

So create four variables: "Trace Region" (region), "Closest Unit" (unit), "Unit Region" (region) and "Unit World Height" (real). We are going to use the unit's default radius as the size of the unit, however you can replace it with a custom size if you want. We are also going to assume that the unit is a ground unit and therefore uses the c_heightMapGround heightmap, and that the bottom of their "hitbox" is the worldheight (which means where the terrain they're standing on is):

lv_traceRegion = RegionCircle(lv_tracePoint, 1.5);
lv_closestUnit = libNtve_gf_ClosestUnitToPoint(lv_tracePoint, UnitGroup(null, 15, lv_traceRegion, UnitFilter(0, 0, (1 << c_targetFilterMissile), (1 << (c_targetFilterDead - 32)) | (1 << (c_targetFilterHidden - 32))), 0));
lv_unitRegion = RegionCircle(UnitGetPosition(lv_closestUnit), UnitGetPropertyFixed(lv_closestUnit, c_unitPropRadius, true));
lv_unitWorldHeight = WorldHeight(c_heightMapGround, UnitGetPosition(lv_closestUnit));


Now we have all the information we need to decide whether we're: Looking at a unit, looking at terrain or aren't looking at anything (yet). So, we're going to test which of those 3 is the case. First we're going to check if it's a unit. We will make sure:

1. The unit is not null (A unit was actually selected)
2. The unit is not the player's main hero
3. The traceline point's location (XY) is inside the unit's hitbox
4. The traceline point's height (Z) is above the bottom of the unit (We're using 0 because we assume it's ground)
5. The traceline point's height (Z) is below the top of the unit (We're going to use the radius of the unit as a baseline, however this can be VERY inaccurate on many units and should be replaced with CUSTOM values on a unit-by-unit basis.

And those checks will look like this:

if(lv_closestUnit != null &&
lv_closestUnit != gv_mainUnit &&
RegionContainsPoint(lv_unitRegion, lv_tracePoint) &&
lp_cameraHeight + lv_traceHeight - lv_unitWorldHeight >= 0.0 &&
lp_cameraHeight + lv_traceHeight - lv_unitWorldHeight <= UnitGetPropertyFixed(lv_closestUnit, c_unitPropRadius, true)) {
// This is where our response will go if all of the conditions are met.
// If all of the conditions are met, it means we are looking at the unit stored in lv_closestUnit.
}


If those conditions are met, which means we're looking at a unit, we want to set those three variables we created earlier to the unit's info and then exit our traceline function (This goes inside the brackets {} of the if statement):

gv_traceline_currentTarget[lp_player] = lv_closestUnit;
gv_traceline_currentTargetPoint[lp_player] = lv_tracePoint;
gv_traceline_currentTargetHeight[lp_player] = lp_cameraHeight + lv_traceHeight;

return;


However, if those conditions aren't met, then we aren't looking at a unit (at this step in the traceline) so we need to check whether we're looking at terrain or still haven't collided with anything yet. The way to check if we've hit terrain is very simple, just check if the current height of the traceline step is under the height of the terrain:

if(lp_cameraHeight + lv_traceHeight <= lv_traceWorldHeight) {
    gv_traceline_currentTarget[lp_player] = null;
    gv_traceline_currentTargetPoint[lp_player] = lv_tracePoint;
    gv_traceline_currentTargetHeight[lp_player] = lp_cameraHeight + lv_traceHeight;

    return;
}


So now if we've hit either a unit or terrain, our function has ended and we have the information of the unit and/or terrain that the player is looking at. This is the end of our loop, so if it runs this test and finds neither a unit nor terrain then it will run again, 0.5 units further down the imaginary line. If it runs 100 times and finds nothing, it will exit the loop and continue through the function. Since it's important that we know when it intersects with NOTHING (Say they're looking up at the sky) we're going to add the following AFTER the loop. To reiterate, this goes OUTSIDE of the while(){} statement that we have been putting our code in:

gv_traceline_currentTarget[lp_player] = null;
gv_traceline_currentTargetPoint[lp_player] = null;
gv_traceline_currentTargetHeight[lp_player] = 0.0;


This just clears out all of the values if we're not looking at anything to make sure that if we're not looking at anything we don't use old, outdated values. As for the traceline, that's it! Any time you want to see what the user is looking at, just call the action using the trigger editor like any other action, it's under the "- General" tab:



We're Done!
Two places you can put this function are:

1. In a recurring function (Every 0.0 seconds, etc). This is useful if you want to use the current target information to display info to the player about the unit they're looking at. You can see an example of this in my Reaper Madness videos. You can then use the SAME information (You don't have to run the traceline twice) to determine which unit or piece of terrain to shoot when the player hits the attack key (such as left mouse click).
2. In a trigger responding to left mouse click, or any other attack key. Just run the traceline() function to populate the three variables (Current target, current target position and current target height) and use those variables to process your attack. For instance, you could run traceline, deal 10.0 damage to traceline_currentTarget and spawn a blood splatter effect at position traceline_currentTargetPosition and height traceline_currentTargetHeight.

Tips:
1. After running the traceline function, you can check if the unit is targeting a unit by checking whether or not traceline_currentTarget is null. If it's null, there's no target. If it's not null, then it holds the unit info of the target.
2. After running the traceline function, you can check if the unit is targeting terrain by checking whether or not traceline_currentTarget is null AND whether or not traceline_currentTargetPosition is null. If both are null, then the player isn't looking at a unit nor is he looking at terrain.
3. You can change the values within the traceline such as our max distance, 50.0, to better suit your needs. For example, if the player is holding a sniper rifle you will probably want him to shoot further than 50.0, so just change the 50.0 to however far you want them to see. On the biggest map, 250.0 should be long enough to target anybody on the entire map. Remember that the bigger this number is the more work needs to be processed and it could end up slowing down your game.


Check Your Work!
Lastly, here's a picture of how your trigger editor should look if you followed my tutorial exactly:


And here's how your code should look if you followed my tutorial exactly:

while(lv_traceDistance < 50.0) {
    if(lv_pitch < 90){
        lv_traceHeight = Tan(lv_pitch) * lv_traceDistance * (-1);
    } else if(lv_pitch > 270) {
        lv_traceHeight = Tan(360 - lv_pitch) * lv_traceDistance;
    }
    lv_tracePoint = PointWithOffsetPolar(lp_cameraPosition, lv_traceDistance, lv_yaw);
    lv_traceWorldHeight = WorldHeight(c_heightMapGround, lv_tracePoint);

    lv_traceRegion = RegionCircle(lv_tracePoint, 1.5);
    lv_closestUnit = libNtve_gf_ClosestUnitToPoint(lv_tracePoint, UnitGroup(null, 15, lv_traceRegion, UnitFilter(0, 0, (1 << c_targetFilterMissile), (1 << (c_targetFilterDead - 32)) | (1 << (c_targetFilterHidden - 32))), 0));
    lv_unitRegion = RegionCircle(UnitGetPosition(lv_closestUnit), UnitGetPropertyFixed(lv_closestUnit, c_unitPropRadius, true));
    lv_unitWorldHeight = WorldHeight(c_heightMapGround, UnitGetPosition(lv_closestUnit));

    if(lv_closestUnit != null &&
    lv_closestUnit != gv_mainUnit &&
    RegionContainsPoint(lv_unitRegion, lv_tracePoint) &&
    lp_cameraHeight + lv_traceHeight - lv_unitWorldHeight >= 0.0 &&
    lp_cameraHeight + lv_traceHeight - lv_unitWorldHeight <= UnitGetPropertyFixed(lv_closestUnit, c_unitPropRadius, true)) {
        gv_traceline_currentTarget[lp_player] = lv_closestUnit;
        gv_traceline_currentTargetPoint[lp_player] = lv_tracePoint;
        gv_traceline_currentTargetHeight[lp_player] = lp_cameraHeight + lv_traceHeight;
        
        return;
    }

    if(lp_cameraHeight + lv_traceHeight <= lv_traceWorldHeight) {
        gv_traceline_currentTarget[lp_player] = null;
        gv_traceline_currentTargetPoint[lp_player] = lv_tracePoint;
        gv_traceline_currentTargetHeight[lp_player] = lp_cameraHeight + lv_traceHeight;

        return;
    }

    lv_traceDistance += 0.5;
}

gv_traceline_currentTarget[lp_player] = null;
gv_traceline_currentTargetPoint[lp_player] = null;
gv_traceline_currentTargetHeight[lp_player] = 0.0;


Thanks for Reading!
Thanks for reading everybody! I know it's a bit advanced and a lot of people probably won't get it, but I tried to explain it as best as possible!







Star Depot
Contact      Login