• 🏆 Texturing Contest #33 is OPEN! Contestants must re-texture a SD unit model found in-game (Warcraft 3 Classic), recreating the unit into a peaceful NPC version. 🔗Click here to enter!
  • It's time for the first HD Modeling Contest of 2024. Join the theme discussion for Hive's HD Modeling Contest #6! Click here to post your idea!

Advanced Third Person Camera

Level 19
Joined
Oct 29, 2007
Messages
1,184
Advanced Third Person Camera
by Megafyr

This tutorial will teach you how to make an advanced third person camera that focuses on a unit in your warcraft III map. You will have to use the NewGen editor to add this to your map. With basic jass experience this tutorial will become significally easier to understand, and easier to follow.

What this camera system includes:

- camera locks to a given unit and views it from behind
- the rotation of the camera follows the rotation of the unit
- height changes according to the terrain
- the angle of attack changes according to the terrain height
- the distance to target changes according to pathing blockers (walls, obstacles, etc...)

In order to make this camera system work you will have to import the pathability check by Rising_Dusk into your map. ( http://www.wc3c.net/showthread.php?t=103862 )

(Obs! This tutorial does not show you how to import any systems apart from the camera code into your map.)

The pathability check will check if there are any obstacles behind the unit the camera focuses on, and then set the correct camera distance. I have attached a demo map, which you can copy the entire system from. Otherwise you can follow along the tutorial.

During the tutorial I recommend to look for comments between the jass lines. They are part of the tutorial.

Okay, let's get started!

Now that you have imported the pathing code by Rising_Dusk, you can create a trigger that runs every 0.04 seconds of game time, which is every time the camera will be updated. The reason I use 0.04 is because 1 second divided by 25 equals 0.04 seconds. That's a quite acceptable framerate. Convert the trigger to jass. Mine looks like this:

JASS:
function Trig_Multiplayer_Camera_Actions takes nothing returns nothing
endfunction

function InitTrig_Multiplayer_Camera takes nothing returns nothing
    set gg_trg_Multiplayer_Camera = CreateTrigger(  )
    call TriggerRegisterTimerEventPeriodic( gg_trg_Multiplayer_Camera, 0.04 )
    call TriggerAddAction( gg_trg_Multiplayer_Camera, function Trig_Multiplayer_Camera_Actions )
endfunction

Now we have to make a couple of locals:

JASS:
function Trig_Multiplayer_Camera_Actions takes nothing returns nothing

    //For the pathability trick we need a boolean.
    local boolean b
    //We need this integer for a loop.
    local integer i = 1
    //A location we need later on
    local location l
    //Represents the angle of attack. 
    local real aoa
    //Represents the view distance.
    local real v
    //X of the unit the camera will be focusing on.
    local real x
    //Y of the unit the camera will be focusing on.
    local real y
    //Z of the location the unit the camera focuses on is located.
    local real z
    //New X.
    local real x1
    //New Y.
    local real y1
    //New Z.
    local real z1

    //Earlier we created location l, now we null it, so it does not leak.
    set l = null

endfunction


function InitTrig_Multiplayer_Camera takes nothing returns nothing
    set gg_trg_Multiplayer_Camera = CreateTrigger(  )
    call TriggerRegisterTimerEventPeriodic( gg_trg_Multiplayer_Camera, 0.04 )
    call TriggerAddAction( gg_trg_Multiplayer_Camera, function Trig_Multiplayer_Camera_Actions )
endfunction

Now that we have our locals let's continue. In case I want the system to work for more players at once, I will create a loop. (The system can also be done without a loop, but I am assuming it will be used mostly for multiplayer maps, so you have to convert it on your own if you don't.)


JASS:
function Trig_Multiplayer_Camera_Actions takes nothing returns nothing

    local boolean b
    local integer i = 1
    local location l
    local real aoa
    local real v
    local real x
    local real y
    local real z
    local real x1
    local real y1
    local real z1

    loop
    //The loop will end when i equals to 1.
    exitwhen i == 1
    //This will count every time the loop runs.
    set i = i + 1
    endloop

    set l = null
endfunction

If you want the system to work for more players just change the exitwhen i == x. x = players who should have the camera. In this case only Player(1). If you have exitwhen x == 2, the camera will work for Player(1) and Player(2) etc...

In the demo map I have a variable udg_player[index] which is the unit the camera is attached to. As long as it is a unit variable it doesn't matter what it is called. For this code it is important though that the index is the same number as the owning player.

Player(1) owns udg_player[1], Player(2) owns udg_player[2] etc...

JASS:
function Trig_Multiplayer_Camera_Actions takes nothing returns nothing

    local boolean b
    local integer i = 1
    local location l
    local real aoa
    local real v
    local real x
    local real y
    local real z
    local real x1
    local real y1
    local real z1

    loop
        //With this we make sure the camera only works when udg_player[i] is a unit that is alive. We will place the rest of the functions inside this "if".
        if udg_player[i] != null and (GetWidgetLife(udg_player[i]) > 0.405) then
        endif  
        exitwhen i == 1
        set i = i + 1
    endloop

    set l = null
endfunction


function InitTrig_Multiplayer_Camera takes nothing returns nothing
    set gg_trg_Multiplayer_Camera = CreateTrigger(  )
    call TriggerRegisterTimerEvent(gg_trg_Multiplayer_Camera,0.04,true)
    call TriggerAddAction( gg_trg_Multiplayer_Camera, function Trig_Multiplayer_Camera_Actions )
endfunction

I have made sure the camera is only works if udg_player is an alive unit. This way we can spare some performance if for instance Player(1) doesn't have a unit, the functions inside the "if" wont be triggered, but you can have the conditions the way that fits your map.

Now I am going to add a lot of new things, but I will explain what they do in comments between the jass lines.

JASS:
function Trig_Multiplayer_Camera_Actions takes nothing returns nothing

    local boolean b
    local integer i = 1
    local location l
    local real aoa
    local real v
    local real x
    local real y
    local real z
    local real x1
    local real y1
    local real z1

    loop
         if udg_player[i] != null and (GetWidgetLife(udg_player[i]) > 0.405) then
            //v is the view distance. 
            //25 will be the closest possible view distance. 
            //I will explain it more detailed later on.
            set v = 25
            //We get X of udg_player[i] and store it into real x.
            set x = GetUnitX(udg_player[i])
            //We get Y of udg_player[i] and store it into real y.
            set y = GetUnitY(udg_player[i])
            //We set l according to the X and Y we just gained which you stored into the reals x and y.
            set l = Location(x, y)
            //We set Z of location l to real z. 
            //This will be used later to change the angle according to the height.
            set z = GetLocationZ(l)
            call RemoveLocation(l)
            //We set a new X to real x1 from real x.
            set x1 = x + 64 * Cos(GetUnitFacing(udg_player[i]) * bj_DEGTORAD)
            //We set a new Y to real y1 from real y.
            set y1 = y + 64 * Sin(GetUnitFacing(udg_player[i]) * bj_DEGTORAD)
            //The two functions above are used to get the X and Y of a location in front of udg_player[i] according to its facing. 
            //Then we set a new location to those real X and real Y.
            set l = Location(x1, y1)
            //We set a new height. 
            //Now we have stored two different terrain heights in real z and real z1. 
            //These will be used to set the camera angle according to the terrain.
            set z1 = GetLocationZ(l)
            call RemoveLocation(l)
        endif  
    exitwhen i == 1
    set i = i + 1
    endloop

    set l = null
endfunction


function InitTrig_Multiplayer_Camera takes nothing returns nothing
    set gg_trg_Multiplayer_Camera = CreateTrigger(  )
    call TriggerRegisterTimerEvent(gg_trg_Multiplayer_Camera,0.04,true)
    call TriggerAddAction( gg_trg_Multiplayer_Camera, function Trig_Multiplayer_Camera_Actions )
endfunction

We now have two different Z values which we are going to use for setting the angle of attack according to the terrain later on.

Now I am going to check if there are any pathing blockers behind the unit. If that is the case, the distance to target of the camera will be decreased so that it does not collide with obstacles.

JASS:
function Trig_Multiplayer_Camera_Actions takes nothing returns nothing

    local boolean b
    local integer i = 1
    local location l
    local real aoa
    local real v
    local real x
    local real y
    local real z
    local real x1
    local real y1
    local real z1

    loop
         if udg_player[i] != null and (GetWidgetLife(udg_player[i]) > 0.405) then 
            set v = 25 
            set x = GetUnitX(udg_player[i]) 
            set y = GetUnitY(udg_player[i]) 
            set l = Location(x, y) 
            set z = GetLocationZ(l) 
            call RemoveLocation(l)
            set x1 = x + 64 * Cos(GetUnitFacing(udg_player[i]) * bj_DEGTORAD) 
            set y1 = y + 64 * Sin(GetUnitFacing(udg_player[i]) * bj_DEGTORAD) 
            set l = Location(x1, y1) 
            set z1 = GetLocationZ(l) 
            call RemoveLocation(l)
            //This loop will check every location behind udg_player[i] for pathing blockers.
            loop
                //As mentioned the minimum view distance was real v, which equals to 25.
                //Now we get the X and Y of a location real v behind udg_player[i].
                set x1 = x - v * Cos(GetUnitFacing(udg_player[i]) * bj_DEGTORAD)
                set y1 = y - v * Sin(GetUnitFacing(udg_player[i]) * bj_DEGTORAD)
                //Then we check if this location is pathable.
                //If it is not, we will set real v = v + 25, and check if that location is walkable etc...
                //If any location lower than the distance of 450 is not walkable we store that information into real v.
                //Then we set real = 450, and that will be the maximum view distance.
                set b = IsTerrainPathingType(x1, y1, TERRAIN_PATHING_WALKABLE)
                    if ((b == true) and (v < 450)) then
                    set v = v + 25
                    elseif ((b == false) or (v == 450)) then
                    exitwhen true
                    endif
            endloop
        endif  
    exitwhen i == 1
    set i = i + 1
    endloop

    set l = null
endfunction


function InitTrig_Multiplayer_Camera takes nothing returns nothing
    set gg_trg_Multiplayer_Camera = CreateTrigger(  )
    call TriggerRegisterTimerEvent(gg_trg_Multiplayer_Camera,0.04,true)
    call TriggerAddAction( gg_trg_Multiplayer_Camera, function Trig_Multiplayer_Camera_Actions )
endfunction

The loop that was added above will check every location with a 25 space in a straight line behind the unit the camera focuses on. If it encounters any obstacles it will store that distance into v. v will later be used to set the camera distance. The reason I set v = 25 above, was to make sure camera doesn't get that close it goes inside the unit it focuses on If v was to be 0, the distance to target of the camera would become 0, and we would view udg_player from inside.

The number 450 is the maximum view distance if nothing else blocks the camera. You can change the value to whatever you want, but you should change it at both places, and they have to be equally large, otherwise the system will bug.

Anyway, let's continue, we are almost done!

JASS:
function Trig_Multiplayer_Camera_Actions takes nothing returns nothing

    local boolean b
    local integer i = 1
    local location l = Location(0,0) //declare this in the beginning
    local real aoa
    local real v
    local real x
    local real y
    local real z
    local real x1
    local real y1
    local real z1

    loop
         if udg_player[i] != null and (GetWidgetLife(udg_player[i]) > 0.405) then 
            set v = 25 
            set x = GetUnitX(udg_player[i]) 
            set y = GetUnitY(udg_player[i]) 
            call MoveLocation(l,x,y) //instead, just move the location to prevent several location creations/removals
            set z = GetLocationZ(l) 
            set x1 = x + 64 * Cos(GetUnitFacing(udg_player[i]) * bj_DEGTORAD) 
            set y1 = y + 64 * Sin(GetUnitFacing(udg_player[i]) * bj_DEGTORAD) 
            call MoveLocation(l,x1,y1) 
            set z1 = GetLocationZ(l) 
            loop
                set x1 = x - v * Cos(GetUnitFacing(udg_player[i]) * bj_DEGTORAD)
                set y1 = y - v * Sin(GetUnitFacing(udg_player[i]) * bj_DEGTORAD)
                set b = IsTerrainPathingType(x1, y1, TERRAIN_PATHING_WALKABLE)
                    if ((b == true) and (v < 450)) then
                    set v = v + 25
                    elseif ((b == false) or (v == 450)) then
                    exitwhen true
                    endif
            endloop
            //The following "if" works for Player(1), the owner of udg_player[i].
            if GetLocalPlayer() == Player(i - 1) then
                //We change the rotation according to udg_player[i].
                call SetCameraField(CAMERA_FIELD_ROTATION, GetUnitFacing(udg_player[i]), 0.08)
                //We set the view distance to real v.
                call SetCameraField(CAMERA_FIELD_TARGET_DISTANCE, v, 0.08)
                //We change the angle of attack according to the terrain. 
                //By changing the number 335, the angle of attack will change ingame.
                call SetCameraField(CAMERA_FIELD_ANGLE_OF_ATTACK, 335 + ((z1 - z) * 0.5), 0.32)
                //We change the field of view.
                //I prefer to have it at 120.
                call SetCameraField(CAMERA_FIELD_FIELD_OF_VIEW, 120, 0)
                //Change the view distance.
                //You can change it to whatever you like.
                call SetCameraField(CAMERA_FIELD_FARZ, 5000, 5)
                //We lock the camera to udg_player[i], to make sure the player doesn't reset it.
                call SetCameraTargetController(udg_player[i], 0, 0, false)
                //Set the height of the camera according to the height of the terrain.
                call SetCameraField(CAMERA_FIELD_ZOFFSET, GetCameraField(CAMERA_FIELD_ZOFFSET) + z - GetCameraTargetPositionZ() + 128, -0.01)
                call SetCameraField(CAMERA_FIELD_ZOFFSET, GetCameraField(CAMERA_FIELD_ZOFFSET) + z - GetCameraTargetPositionZ() + 128, 0.01)
            endif
        endif  
    exitwhen i == 1
    set i = i + 1
    endloop
    
    call RemoveLocation(l)
    set l = null
endfunction
function InitTrig_Multiplayer_Camera takes nothing returns nothing
    set gg_trg_Multiplayer_Camera = CreateTrigger(  )
    call TriggerRegisterTimerEvent(gg_trg_Multiplayer_Camera,0.04,true)
    call TriggerAddAction( gg_trg_Multiplayer_Camera, function Trig_Multiplayer_Camera_Actions )
endfunction

What I did above was to set the field camera of the owner of udg_player – which is Player(i) – according to the variables we had stored. If you have followed all the above steps correctly your camera system should work fine now. You can view the testing map I attached to view what mine looks like.

Final words:

Once again, remember to set the array of variable for the unit to the same number as the owning player. Otherwise you will have to edit some things in the system to make it work.

I suggest you play around with the variables until you find fitting settings for your map. I can imagine there might be one or two things about this configuration you do not like, or want to change. I am happy if this tutorial was what you were after. If you think I should change something, you can tell me in a comment below, and I will consider your suggestion.

~Megafyr
 

Attachments

  • Third Person Camera.w3x
    60.8 KB · Views: 2,946
Last edited by a moderator:
@Darkness-4ever: Have no fear! =D

@Megafyr: Great tutorial. I have to say, it is definitely an awesome effect. I would have gone with a 0.03125 interval (32 fps) but that is just me. =P I updated the code with the native equivalent of the event, and I also just reused the location by moving it rather than creating/destroying one each part of the loop. That way, it is a bit more efficient.

Overall though, great job.

~Approved.
 
Level 19
Joined
Oct 29, 2007
Messages
1,184
@Darkness-4ever: Have no fear! =D

@Megafyr: Great tutorial. I have to say, it is definitely an awesome effect. I would have gone with a 0.03125 interval (32 fps) but that is just me. =P I updated the code with the native equivalent of the event, and I also just reused the location by moving it rather than creating/destroying one each part of the loop. That way, it is a bit more efficient.

Overall though, great job.

~Approved.

Thank you very much :D
 
Top