RTS Camera in Leadwerks Engine

SETUP


The videos and code below cover the setup, motion, and actions for an RTS "like" camera in Leadwerks. We will focus on the basic elements of the Rise of Nations camera.

What we want:
  • Camera that moves forwards, backwards, left, right.
  • Zooms in and out.
  • Can turn left and right.
  • Has a speed multiplier.
  • Can "pick" entities/actors/characters and can assign them to move to a specific location.
  • [update] Group selection via "Selection Box" has been added.

We will place a pivot into our map, this pivot will have a script attached. In the script we will create a camera, which will have the pivot as its parent. Most of the movement manipulations will be done on the pivot, but there are some that will be specific to the camera (such as zoom in/out).  Picking characters and assigning actions will be done through raycasting from the camera and mouse input.



MOTION



ACTIONS

The Actions portion has been broken down into three subportions: Actions Setup, Pick Actors, and Assign Actions.

Actions - Actions Setup


The Actions Setup video covers creating "global" scope arrays/tables.  The values of these tables are then used in other scripts to identify floors, walls, actors, etc.  Some of the tables are also used for animations, actions, subactions, and other things that are shared in multiple script files.  Using LUA Table Maker does facilitate the creation, and updating, of LUA arrays.


Actions - Pick Actors


Actions - Assign Actions



RTSCam.lua

-- RTS Camera by Jesse B Andersen
-- August 20, 2014
-- Full tutorial at:
-- http://www.jessebandersen.com/2014/08/rts-camera-in-leadwerks-engine.html


function Script:Start()
 -- Pivot Settings
 self.initPos = self.entity:GetPosition(true)
 self.initAngle = self.entity:GetRotation(true)
 self.moveSpeed = 0.2
 self.moveMultiplier = 2
 self.turnSpeed = 0.4
 self.yOffset = 1.0
 self.entity:SetPosition(self.initPos.x, self.initPos.y + self.yOffset, self.initPos.z)

 -- Camera Settings
 self.cam = Camera:Create(self.entity)
 self.camInitPos = Vec3(0, 7, -8)
 self.cam:SetPosition(self.camInitPos)
 self.zoomIn = 2
 self.zoomOut = 25
 self.cam:Point(self.entity)

 -- Pick Settings
 self.pickRadius = 0.01
 self.pickedActor = nil
end


function Script:UpdateWorld()

 local Win = Window:GetCurrent()
 local Ts = Time:GetSpeed()
 local smp = 1

 --System:Print("Animations run: " .. animations.run)

 -- Increase speed
 if Win:KeyDown(Key.Shift) then
  smp = self.moveMultiplier
 end
 
 -- Forwards
 if Win:KeyDown(Key.W) then
  self.entity:Move(0, 0, self.moveSpeed * smp * Ts)
 end
 
 -- Backwards
 if Win:KeyDown(Key.S) then
  self.entity:Move(0, 0, -self.moveSpeed * smp * Ts)
 end

 -- Left
 if Win:KeyDown(Key.A) then
  self.entity:Move(-self.moveSpeed * smp * Ts, 0, 0)
 end
 
 -- Right
 if Win:KeyDown(Key.D) then
  self.entity:Move(self.moveSpeed * smp * Ts, 0, 0)
 end

 -- Zoom in
 if Win:KeyDown(Key.R) then
  if self.cam:GetDistance(self.entity) > self.zoomIn then
   self.cam:Move(0, 0, self.moveSpeed * smp * Ts)
  end
 end

 -- Zoom Out
 if Win:KeyDown(Key.F) then
  if self.cam:GetDistance(self.entity) < self.zoomOut then
   self.cam:Move(0, 0, -self.moveSpeed * smp * Ts)
  end
 end

 -- Turn Left
 if Win:KeyDown(Key.Q) then
  self.entity:Turn(0, -self.turnSpeed * smp * Ts, 0)
 end
 
 -- Turn Right
 if Win:KeyDown(Key.E) then
  self.entity:Turn(0, self.turnSpeed * smp * Ts, 0)
 end

 -- Reset Camera
 if Win:KeyHit(Key.F12) then
  self.entity:SetPosition(self.initPos.x, self.initPos.y + self.yOffset, self.initPos.z)
  self.entity:SetRotation(self.initAngle)
  self.cam:SetPosition(self.camInitPos)
  self.cam:Point(self.entity)
 end

 -- Left Click
 if Win:MouseHit(1) then
  self:DoPick(Win)
 end

 -- Right Click
 if Win:MouseHit(2) then
  self:DoAssign(Win)
 end

 -- Walk Roam
 if Win:KeyHit(Key.F1) then
  if self.pickedActor ~= nil then
   self.pickedActor.script:Roam(subActions.walk)
  end
 end

 -- Run Roam
 if Win:KeyHit(Key.F2) then
  if self.pickedActor ~= nil then
   self.pickedActor.script:Roam(subActions.run)
  end
 end
end


function Script:DoPick(Win)
 local pickInfo = PickInfo()
 local mp = Win:GetMousePosition()

 -- Release existing actor
 if self.pickedActor ~= nil then
  self.pickedActor.script:GotReleased()
  self.pickedActor = nil
 end

 -- Raycast
 if self.cam:Pick(mp.x, mp.y, pickInfo, self.pickRadius, true) then
  
  -- Ensure we have script and entiType
  if pickInfo.entity.script ~= nil and pickInfo.entity.script.entiType ~= nil then
   
   -- Ensure we have an actor
   if pickInfo.entity.script.entiType == entiTypes.actor then
    
    -- Store new actor
    self.pickedActor = pickInfo.entity
    self.pickedActor.script:GotPicked()
   end
  end
 end
end


function Script:DoAssign(Win)

 if self.pickedActor == nil then return end

 local pickInfo = PickInfo()
 local mp = Win:GetMousePosition()

 -- Raycast
 if self.cam:Pick(mp.x, mp.y, pickInfo, self.pickRadius, true) then
 
  -- Ensure we have script and entiType
  if pickInfo.entity.script ~= nil and pickInfo.entity.script.entiType ~= nil then

   -- Ensure we have a floor
   if pickInfo.entity.script.entiType == entiTypes.floor then
    
    -- If shift then make actor run, else walk
    if Win:KeyDown(Key.Shift) then
     self.pickedActor.script:MoveTo(pickInfo.position, subActions.run)
    else
     self.pickedActor.script:MoveTo(pickInfo.position, subActions.walk)
    end
    
   end
  end
 end
end


Selection Box - Setup


Selection Box - Drawing the Selection Box


Selection Box - Selecting Actors


Selection Box - Assigning Actions


RTSCam.lua -- with Group Selection via Selection Box

-- RTS Camera (with group selection) by Jesse B Andersen
-- September 02, 2014
-- Full tutorial at:
-- http://www.jessebandersen.com/2014/08/rts-camera-in-leadwerks-engine.html

function Script:Start()
 -- Pivot Settings
 self.initPos = self.entity:GetPosition(true)
 self.initAngle = self.entity:GetRotation(true)
 self.moveSpeed = 0.2
 self.moveMultiplier = 2
 self.turnSpeed = 0.4
 self.yOffset = 1.0
 self.entity:SetPosition(self.initPos.x, self.initPos.y + self.yOffset, self.initPos.z)

 -- Camera Settings
 self.cam = Camera:Create(self.entity)
 self.camInitPos = Vec3(0, 7, -8)
 self.cam:SetPosition(self.camInitPos)
 self.zoomIn = 2
 self.zoomOut = 25
 self.cam:Point(self.entity)

 -- Pick Settings
 self.pickRadius = 0.01
 self.pickedActor = nil
 self.groupPicked = false
 self.pickedActors = {}

 -- Pick Actors Rectangle
 self.context = nil
 self.pickInit = false
 self.mpInit = Vec2(0)
 self.mpEnd = Vec2(0)

end


function Script:UpdateWorld()
 local Win = Window:GetCurrent()
 local Ts = Time:GetSpeed()
 local smp = 1

 --System:Print("Animations run: " .. animations.run)

 -- Increase speed
 if Win:KeyDown(Key.Shift) then
  smp = self.moveMultiplier
 end
 
 -- Forwards
 if Win:KeyDown(Key.W) then
  self.entity:Move(0, 0, self.moveSpeed * smp * Ts)
 end
 
 -- Backwards
 if Win:KeyDown(Key.S) then
  self.entity:Move(0, 0, -self.moveSpeed * smp * Ts)
 end

 -- Left
 if Win:KeyDown(Key.A) then
  self.entity:Move(-self.moveSpeed * smp * Ts, 0, 0)
 end
 
 -- Right
 if Win:KeyDown(Key.D) then
  self.entity:Move(self.moveSpeed * smp * Ts, 0, 0)
 end

 -- Zoom in
 if Win:KeyDown(Key.R) then
  if self.cam:GetDistance(self.entity) > self.zoomIn then
   self.cam:Move(0, 0, self.moveSpeed * smp * Ts)
  end
 end

 -- Zoom Out
 if Win:KeyDown(Key.F) then
  if self.cam:GetDistance(self.entity) < self.zoomOut then
   self.cam:Move(0, 0, -self.moveSpeed * smp * Ts)
  end
 end

 -- Turn Left
 if Win:KeyDown(Key.Q) then
  self.entity:Turn(0, -self.turnSpeed * smp * Ts, 0)
 end
 
 -- Turn Right
 if Win:KeyDown(Key.E) then
  self.entity:Turn(0, self.turnSpeed * smp * Ts, 0)
 end

 -- Reset Camera
 if Win:KeyHit(Key.F12) then
  self.entity:SetPosition(self.initPos.x, self.initPos.y + self.yOffset, self.initPos.z)
  self.entity:SetRotation(self.initAngle)
  self.cam:SetPosition(self.camInitPos)
  self.cam:Point(self.entity)
 end


 -- Left Click
 if Win:MouseHit(1) then
  self:DoPick(Win)
 end

 -- Right Click
 if Win:MouseHit(2) then
  self:DoAssign(Win)
 end
 
 
 -- Left mouse down
 if Win:MouseDown(1) then
  -- Begin tracking mouse position
  if self.pickInit == false then
   self.pickInit = true
   self.mpInit = Win:GetMousePosition()
   self.mpEnd = Win:GetMousePosition()
   self.context = Context:GetCurrent()
  else
   -- Tracking of mouse position is on going
   self.mpEnd = Win:GetMousePosition()
  end
 else
  -- Determine if a "release" has occurred and perform a group selection
  if self.pickInit == true then
   self.pickInit = false
   self:DoSelection(Win)
   self.context = nil
  end
 end


 -- Walk Roam
 if Win:KeyHit(Key.F1) then
  -- For single actor
  if self.pickedActor ~= nil then
   self.pickedActor.script:Roam(subActions.walk)
  end

  -- For group
  if self.groupPicked == true then
   for j, jActor in pairs(self.pickedActors) do
    jActor.entity.script:Roam(subActions.walk)
   end
  end

 end

 -- Run Roam
 if Win:KeyHit(Key.F2) then
  -- For single actor
  if self.pickedActor ~= nil then
   self.pickedActor.script:Roam(subActions.run)
  end

  -- For group
  if self.groupPicked == true then
   for j, jActor in pairs(self.pickedActors) do
    jActor.entity.script:Roam(subActions.run)
   end
  end

 end
end


function Script:DoSelection(Win)

 -- Release existing group
 if self.groupPicked == true then
  self.groupPicked = false
  
  -- Iterate through the pickedActors table and release
  for j, jActor in pairs(self.pickedActors) do
   jActor.entity.script:GotReleased()
  end
  self.pickedActors = {}
 end

 -- Get the new group
 
 -- Determine the smallest x and the smallest y
 local initPos = Vec2(self.mpInit.x, self.mpInit.y)
 local endPos = Vec2(self.mpEnd.x, self.mpEnd.y)

 if endPos.x < initPos.x then
  local temp = initPos.x
  initPos.x = endPos.x
  endPos.x = temp
 end

 if endPos.y < initPos.y then
  local temp = initPos.y
  initPos.y = endPos.y
  endPos.y = temp
 end

 -- Populate the pickedActors table
 for j, jActor in pairs(actors) do
  local p = self.cam:Project(jActor.entity:GetPosition(true))
  if p.x > initPos.x and p.x < endPos.x then
   if p.y > initPos.y and p.y < endPos.y then
    jActor.entity.script:GotPicked()
    table.insert(self.pickedActors, jActor)
    self.groupPicked = true
   end
  end
  
 end
end


function Script:PostRender()
 if self.pickInit == true then
  -- Red 
  self.context:SetColor(1, 0, 0)
 
  -- Horizontal lines
  self.context:DrawLine(self.mpInit.x, self.mpInit.y, self.mpEnd.x, self.mpInit.y)
  self.context:DrawLine(self.mpInit.x, self.mpEnd.y, self.mpEnd.x, self.mpEnd.y)

  -- Vertical lines
  self.context:DrawLine(self.mpInit.x, self.mpInit.y, self.mpInit.x, self.mpEnd.y)
  self.context:DrawLine(self.mpEnd.x, self.mpInit.y, self.mpEnd.x, self.mpEnd.y)

 end
end


function Script:DoPick(Win)

 local pickInfo = PickInfo()
 local mp = Win:GetMousePosition()

 -- Release existing actor
 if self.pickedActor ~= nil then
  self.pickedActor.script:GotReleased()
  self.pickedActor = nil
 end

 -- Raycast
 if self.cam:Pick(mp.x, mp.y, pickInfo, self.pickRadius, true) then
  
  -- Ensure we have script and entiType
  if pickInfo.entity.script ~= nil and pickInfo.entity.script.entiType ~= nil then
   
   -- Ensure we have an actor
   if pickInfo.entity.script.entiType == entiTypes.actor then
    
    -- Store new actor
    self.pickedActor = pickInfo.entity
    self.pickedActor.script:GotPicked()
   end
  end
 end
end


function Script:DoAssign(Win)

 local pickInfo = PickInfo()
 local mp = Win:GetMousePosition()

 -- Raycast
 if self.cam:Pick(mp.x, mp.y, pickInfo, self.pickRadius, true) then
 
  -- Ensure we have script and entiType
  if pickInfo.entity.script ~= nil and pickInfo.entity.script.entiType ~= nil then

   -- Ensure we have a floor
   if pickInfo.entity.script.entiType == entiTypes.floor then
    
    -- If shift then make actor run, else walk
    if Win:KeyDown(Key.Shift) then
     
     -- Assign a single actor to run
     if self.pickedActor ~= nil then
      self.pickedActor.script:MoveTo(pickInfo.position, subActions.run)
     end
     
     -- Assign a group of actors to run
     if self.groupPicked == true then
      for j, jActor in pairs(self.pickedActors) do
       jActor.entity.script:MoveTo(pickInfo.position, subActions.run)
      end
     end


    else
     -- Assign a single actor to walk
     if self.pickedActor ~= nil then
      self.pickedActor.script:MoveTo(pickInfo.position, subActions.walk)
     end

     -- Assign a group of actors to walk
     if self.groupPicked == true then
      for j, jActor in pairs(self.pickedActors) do
       jActor.entity.script:MoveTo(pickInfo.position, subActions.walk)
      end
     end

    end
   end
  end
 end
end


I presume that not everything I said made "sense" or was technically correct. If there are issues that need to be clarified then please let me know via email or in the comments below. The tutorial should cover most of the principles requires to make RTS games. Please support the superb Leadwerks game engine by getting a copy or demo at http://www.leadwerks.com/ or Steam. Lastly, thank you for visiting this blog.

Published: Aug 17, 2014

No comments:

Post a Comment

Sign up for the JBA Newsletter. A few times per year I may sell or give away gadgets and other electronics By signing up you ensure getting notified in a timely manner. I do NOT send you emails that will waste your time. Thank you.

Home

Hi. My name is Jesse, and I'm a technology enthusiast. I play with technology and share what I find on this blog. If you have any questions then please use the contact form below. I'll get back to you as soon as I can.


Contact

Name

Email *

Message *