--[[ A basic 3d movement controller component, used by the Templates for freecam and first person movement. Gravity is disabled by default. Will use a Mover to move the unit, if the unit has one. ]]-- require 'core/appkit/lua/class' require 'core/appkit/lua/component_manager' require 'core/appkit/lua/input_mapper' Appkit.UnitController = Appkit.class(Appkit.UnitController) local UnitController = Appkit.UnitController local Unit = stingray.Unit local Mover = stingray.Mover local Vector3 = stingray.Vector3 local Vector3Box = stingray.Vector3Box local Matrix4x4 = stingray.Matrix4x4 local Quaternion = stingray.Quaternion local default_gravity = Vector3Box(Vector3(0,0,-9.82)) Appkit.ComponentManager.give_manager(UnitController) -- The given unit is moved by the controller. function UnitController:init(entity, unit, input_mapper) self.unit = unit self.input_mapper = input_mapper self.translation_speed = Vector3Box(Vector3(1,1,1)) self.yaw_speed = 1 self.pitch_speed = 1 self.is_enabled = true self.mover = nil self.velocity = Vector3Box(Vector3(0,0,0)) self.active_gravity = Vector3Box(Vector3(0,0,0)) self.world_gravity = default_gravity self.pitch_min = -90 self.pitch_max = 90 self.touch_scale = 1 self.gamepad_scale = 5 self.mouse_scale = 0.05 UnitController.manager:add_component(entity, self, stingray.Unit.level(unit)) end function UnitController:set_gravity_enabled(is_enabled) if is_enabled then self.active_gravity = self.world_gravity else self.active_gravity:store(Vector3(0,0,0)) end end function UnitController:set_gravity(custom_value) self.world_gravity:store(custom_value) if self.active_gravity:unbox() ~= Vector3(0,0,0) then self.active_gravity:store(custom_value) end end function UnitController:set_mover(name) self.mover = Unit.set_mover(self.unit, name) end -- Enable or disable control of unit function UnitController:set_enabled(enabled) self.is_enabled = enabled end function UnitController:set_move_speed(speed) self.translation_speed:store(speed) end function UnitController:set_yaw_speed(speed) self.yaw_speed = speed end function UnitController:set_pitch_speed(speed) self.pitch_speed = speed end function UnitController:add_impulse(impulse) self.velocity:store(self.velocity:unbox() + impulse) end function UnitController:set_pitch_min(min) self.pitch_min = min end function UnitController:set_pitch_max(max) self.pitch_max = max end function UnitController:set_touch_scale(scale) self.touch_scale = scale end function UnitController:set_gamepad_scale(scale) self.gamepad_scale = scale end function UnitController:set_mouse_scale(scale) self.mouse_scale = scale end local function compute_rotation(self, input, dt) local q_original = Unit.local_rotation(self.unit, 1) local m_original = Matrix4x4.from_quaternion(q_original) local yaw = 0 local pitch = 0 if stingray.Vector3.equal(stingray.Mouse.axis(stingray.Mouse.axis_id("mouse")), stingray.Vector3.zero()) then if stingray.Pad1 and stingray.Pad1.active() then -- gamepad -- apply an acceleration curve so that we move faster as player presses stick further yaw = (input.pan.x * input.pan.x * input.pan.x / 100) * self.gamepad_scale * dt pitch = (input.pan.y * input.pan.y * input.pan.y / 100) * self.gamepad_scale * dt else -- touch or other device yaw = input.pan.x * self.touch_scale * dt pitch = input.pan.y * self.touch_scale * dt end else -- mouse input is something similar to `pixels` moved this frame instead of a normalized value -- do not attempt to scale mouse input by delta time yaw = input.pan.x * self.mouse_scale pitch = input.pan.y * self.mouse_scale end local q_identity = Quaternion.identity() local q_yaw = Quaternion(Vector3(0,0,1), -yaw * self.yaw_speed) local q_pitch = Quaternion(Matrix4x4.x(m_original), -pitch * self.pitch_speed) local q_frame = Quaternion.multiply(q_yaw, q_pitch) local q_new = Quaternion.multiply(q_frame, q_original) -- clamp yaw and pitch if (self.pitch_min ~= nil and self.pitch_max ~= nil) then local pich_angle = math.acos(Vector3.dot(Quaternion.up(q_new), Vector3(0,0,1))) local above_horizon = Quaternion.forward(q_new).z > 0 local adjustment = 0 if above_horizon then local limit = self.pitch_max * math.pi/180.0 adjustment = math.min(0, limit - pich_angle) else local limit = self.pitch_min * math.pi/180.0 adjustment = math.max(0, limit - -pich_angle) end if adjustment ~= 0 then local adjustment_rotation = Quaternion.axis_angle(Vector3(1,0,0),adjustment) q_new = Quaternion.multiply(q_new, adjustment_rotation) end end return q_new end local function compute_translation(self, input, q, dt) local input_move = input.move local pose = Matrix4x4.from_quaternion(q) local pos = Unit.local_position(self.unit, 1) local mover = self.mover local frame_vel if mover then local active_gravity = self.active_gravity:unbox() local velocity = self.velocity:unbox() local gravity = active_gravity * dt self.velocity:store(velocity + gravity) frame_vel = velocity * dt + gravity else frame_vel = Vector3(0,0,0) end local frame_move = (Vector3.multiply_elements(input_move, self.translation_speed:unbox()) * dt) + frame_vel local move = stingray.Matrix4x4.transform(pose, frame_move) if mover then Mover.move(mover, move, dt) if Mover.standing_frames(mover) > 0 then local velocity = self.velocity:unbox() velocity.z = 0 self.velocity:store(velocity) end return Mover.position(mover) else return Unit.local_position(self.unit, 1) + move end end function UnitController:update(dt) if not self.is_enabled then return end local input = self.input_mapper:get_motion_input() local q = compute_rotation(self, input, dt) local p = compute_translation(self, input, q, dt) local unit = self.unit Unit.set_local_rotation(unit, 1, q) Unit.set_local_position(unit, 1, p) end function UnitController:on_level_shutdown() end function UnitController:shutdown() end return UnitController