require 'core/appkit/lua/class' require 'core/appkit/lua/app' local SimpleProject = require 'core/appkit/lua/simple_project' local ComponentManager = require 'core/appkit/lua/component_manager' local UnitController = require 'core/appkit/lua/unit_controller' local UnitLink = require 'core/appkit/lua/unit_link' local Util = require 'core/appkit/lua/util' local CameraWrapper = require 'core/appkit/lua/camera_wrapper' require 'script/lua/input_mapper' local PlayerUtil = require 'script/lua/util' local PlayerHud = require 'script/lua/player_hud' -- on touch devices we will override the input_mapper in Appkit to use the Hud Controls if scaleform and Util.use_touch() then function Appkit.input_mapper:get_motion_input() return PlayerHud:get_motion_input() end end Project.Player = Appkit.class(Project.Player) local Player = Project.Player -- cache off for readability and speed -- cache off for readability and speed local Vector3 = stingray.Vector3 local Vector3Box = stingray.Vector3Box local Quaternion = stingray.Quaternion local QuaternionBox = stingray.QuaternionBox local Matrix4x4 = stingray.Matrix4x4 local Matrix4x4Box = stingray.Matrix4x4Box local Math = stingray.Math local Unit = stingray.Unit local Camera = stingray.Camera local World = stingray.World local Actor = stingray.Actor local Mover = stingray.Mover local PhysicsWorld = stingray.PhysicsWorld local Wwise = stingray.Wwise local WwiseWorld = stingray.WwiseWorld -- cache off move directions for readability local NORTH = 2 local SOUTH = 4 local EAST = 3 local WEST = 1 local NORTH_EAST = 2.5 local NORTH_WEST = 1.5 local SOUTH_EAST = 3.5 local SOUTH_WEST = 0.5 local free_cam_move_speed = Vector3Box(3.8, 3.8, 3.8) local free_cam_sprint_speed = Vector3Box(9, 9, 8) local free_cam_yaw_speed = 0.085 local free_cam_pitch_speed = 0.075 local character_walk_speed = Vector3Box(1.67, 1.67, 1.67) local character_walk_speed_in_air = Vector3Box(2.5, 2.5, 2.5) local character_run_speed = Vector3Box(6.13, 6.13, 6.13) local character_run_speed_in_air = Vector3Box(5.5, 5.5, 5.5) local character_sprint_speed = Vector3Box(10, 10, 10) local character_sprint_speed_in_air = Vector3Box(8, 8, 8) local character_jump_strength = 15 local character_max_slope_angle = math.rad(90) -- 1st person camera parameters local camera_positioning_1p = Vector3Box(-0.08, 0.0, 1.50) -- best for method 2 local character_cam_yaw_speed_1p = 0.3 local character_cam_pitch_speed_1p = 0.1 local camera_pitch_min_1p = -85 local camera_pitch_max_1p = 75 local fov_1p = math.rad(60) -- FOV of 90 for 1st person (standard) local fov_1p_aim = math.rad(35) local player_1p_aim_rate = 8.0 local head_bob_amp_1p_walk = 0.1 -- magnitude of overall bob while walking local head_bob_freq_walk = 3.8 -- frequency of bobbing per walking distance local head_bob_trav_mult_walk = 0.2 -- how much side to side bob relative to up-down bob, higher values bob side to side more local head_bob_amp_1p_sprint = 0.1 -- magnitude of overall bob while sprinting local head_bob_trav_mult_sprint = 0.2 -- how much side to side bob relative to up-down bob, higher values bob side to side more local head_bob_freq_sprint = 2.0 -- frequency of bobbing per walking distance -- 3rd person camera parameters local camera_stand_target = Vector3Box(0.3, 0, 1.3) local camera_crouch_target = Vector3Box(0.3, 0, 1.3) local camera_default_yaw = math.rad(-90) local camera_default_pitch = math.rad(85) local camera_pitch_min = math.rad(15) local camera_pitch_max = math.rad(105) local camera_yaw_speed = math.rad(10) local camera_pitch_speed = math.rad(6) local camera_pitch_aim_speed = math.rad(3) local character_walk_speed_3p = 1.67 local character_walk_speed_in_air_3p = 2.5 local character_run_speed_3p = 6.13 local character_run_speed_in_air_3p = 5.5 local character_sprint_speed_3p = 10 local character_sprint_speed_in_air_3p = 8 local character_cam_yaw_speed_3p = 0.1 local fov_3p = math.rad(45) local fov_3p_aim = 0.5 local character_rotation_blending_speed = 12 local camera_offset_blending_speed = 10 local camera_center_blending_speed = 10 local camera_fov_blending_speed = 10 local character_cam_offset_length_3p = 5 local character_cam_offset_length_3p_aim = 3.5 local wwise_world = nil ------------------------ Unit Controller ------------------------ 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 aim_modifier = self.fov_modifier or 1 -- always need dt since unit_controller computes rotation by (new_rotation = previous_rotation + rotation_speed * dt) local q_identity = Quaternion.identity() local q_yaw = Quaternion(Vector3(0, 0, 1), -Vector3.x(input.pan) * self.yaw_speed * aim_modifier * dt) local q_pitch = Quaternion(Matrix4x4.x(m_original), -Vector3.y(input.pan) * self.pitch_speed * aim_modifier * dt) local q_frame = Quaternion.multiply(q_yaw, q_pitch) local q_new = Quaternion.multiply(q_frame, q_original) if self.pitch_min or self.pitch_max then local xyz = {Quaternion.to_euler_angles_xyz(q_new)} if self.pitch_min ~= nil and xyz[1] < self.pitch_min then xyz[1] = self.pitch_min elseif self.pitch_max ~= nil and xyz[1] > self.pitch_max then xyz[1] = self.pitch_max end q_new = Quaternion.from_euler_angles_xyz(xyz[1], xyz[2], xyz[3]) 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 = 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 ------------------------ Unit Controller ------------------------ ------------------------ SFX ------------------------ local function play_spawn_sound() if Wwise then wwise_world = wwise_world or Wwise.wwise_world(SimpleProject.world) WwiseWorld.trigger_event(wwise_world, "sfx_spawn_sound") end end local function play_ambient_sound() if Wwise then wwise_world = wwise_world or Wwise.wwise_world(SimpleProject.world) WwiseWorld.trigger_event(wwise_world, "sfx_amb_warehouse") end end local function play_fire_sound() if Wwise then wwise_world = wwise_world or Wwise.wwise_world(SimpleProject.world) WwiseWorld.trigger_event(wwise_world, "sfx_ballgun_fire") end end ------------------------ SFX ------------------------ ------------------------ Helper Function ------------------------ -- return a random unit vector3 -- local function random_unit_vector3() -- return Vector3.normalize(Vector3(Math.random() - 0.5, Math.random() - 0.5, Math.random() - 0.5)) -- end -- return a new quaternion containing only euler z-axis rotation local function get_rotation_z(rotation) local _, _, euler_z = Quaternion.to_euler_angles_xyz(rotation) return Quaternion(Vector3.up(), math.rad(euler_z)) end -- return if character is in air local function is_character_in_air(land_character) local unit_controller = UnitController.manager:get(land_character) return unit_controller and unit_controller.velocity.z ~= 0 end -- check to see if there are player starts in the level, if not return world 0 plus 5 up to spawn character local function get_player_start_pose() local player_starts = World.units_by_resource(SimpleProject.world, "tools/player_start") if #player_starts > 0 then local index = Math.random(1, #player_starts) local translation = Unit.world_position(player_starts[index], 1) local rotation = Unit.world_rotation(player_starts[index], 1) local rotation_z = get_rotation_z(rotation) return Matrix4x4.from_quaternion_position(rotation_z, translation) else return Matrix4x4.from_translation(Vector3(0, 0, 5)) end end -- set parameters from unit data local function set_unit_script_data_values(character_unit) local walk_speed = Unit.get_data(character_unit, "walk speed") local run_speed = Unit.get_data(character_unit, "run speed") local sprint_speed = Unit.get_data(character_unit, "sprint speed") local air_speed = Unit.get_data(character_unit, "air control") local jump_strength = Unit.get_data(character_unit, "jump strength") if walk_speed then character_walk_speed:store(walk_speed, walk_speed, walk_speed) character_walk_speed_3p = walk_speed else print('Unit walk speed not set. Using default value.') end if run_speed then character_run_speed:store(run_speed, run_speed, run_speed) character_run_speed_3p = run_speed else print('Unit run speed not set. Using default value.') end if sprint_speed then character_sprint_speed:store(sprint_speed, sprint_speed, sprint_speed) character_sprint_speed_3p = sprint_speed else print('Unit sprint speed not set. Using default value.') end if air_speed then character_walk_speed_in_air:store(air_speed, air_speed, air_speed) character_walk_speed_in_air_3p = air_speed else print('Unit air control not set. Using default value.') end if jump_strength then character_jump_strength = jump_strength else print('Unit jump strength not set. Using default value.') end end local function compute_move_direction(motion_direction) if(motion_direction == nil) then print("Error compute_move_direction received nil motion_direction") return nil end local norm_motion_dir = Vector3.normalize(motion_direction) local x_angle = math.acos(Vector3.dot(norm_motion_dir, Vector3.right())) local y_angle = math.acos(Vector3.dot(norm_motion_dir, Vector3.forward())) local thresh = math.rad(15) local move_direction if y_angle <= thresh then move_direction = NORTH -- forward elseif y_angle >= math.pi - thresh then move_direction = SOUTH -- back elseif x_angle <= thresh then move_direction = EAST -- right elseif x_angle>= math.pi - thresh then move_direction = WEST -- left elseif x_angle > thresh and x_angle < math.pi/2 and y_angle > thresh and y_angle < math.pi/2 then move_direction = NORTH_EAST -- forward, right elseif x_angle > math.pi/2 and x_angle < math.pi - thresh and y_angle > thresh and y_angle < math.pi/2 then move_direction = NORTH_WEST -- forward, left elseif x_angle > thresh and x_angle < math.pi/2 and y_angle > math.pi/2 and y_angle < math.pi-thresh then move_direction = SOUTH_EAST -- back, right elseif x_angle > math.pi/2 and x_angle < math.pi - thresh and y_angle > math.pi/2 and y_angle < math.pi - thresh then move_direction = SOUTH_WEST -- back, left else print("Error in compute_move_direction:\nmotion_direction", motion_direction, "\nx_angle", x_angle, "\ny_angle", y_angle) end return move_direction end local function blend_to_move_direction(cur, targ, dt) local full_range = 4 local halfrange = full_range * 0.5 local delta_direction = targ - cur local abs_dir = math.abs(delta_direction) local rotation_direction if delta_direction >= 0 and abs_dir <= halfrange then rotation_direction = 1 elseif delta_direction >= 0 and abs_dir > halfrange then rotation_direction = -1 elseif delta_direction < 0 and abs_dir <= halfrange then rotation_direction = -1 elseif delta_direction < 0 and abs_dir > halfrange then rotation_direction = 1 end local rotation_rate = 3 -- blend speed local abs_frame_rotation_amt = rotation_rate * dt local frame_rotation_amt = abs_frame_rotation_amt * rotation_direction local new_val = cur + frame_rotation_amt if new_val > full_range then new_val = new_val - full_range elseif new_val < 0 then new_val = new_val + full_range end if (math.abs(new_val - targ) <= abs_frame_rotation_amt) or (full_range - math.abs(new_val - targ) <= abs_frame_rotation_amt) then new_val = targ end return new_val end ------------------------ Helper Function ------------------------ ------------------------ Spawn Function ------------------------ local function spawn_freecam_player(self, level, view_position, view_rotation) view_position = view_position or Vector3(0, 0, 2) view_rotation = view_rotation or Quaternion.identity() local player_camera = self.player_camera player_camera.unit = SimpleProject.camera_unit -- camera local camera_wrapper = CameraWrapper(player_camera, player_camera.unit, 1) camera_wrapper:set_local_position(view_position) camera_wrapper:set_local_rotation(view_rotation) camera_wrapper:enable() -- add camera input movement, starts enabled local unit_controller = UnitController(player_camera, player_camera.unit, Appkit.input_mapper) unit_controller:set_move_speed(free_cam_move_speed:unbox()) unit_controller:set_yaw_speed(free_cam_yaw_speed) unit_controller:set_pitch_speed(free_cam_pitch_speed) -- give free cam ability to attached to character for walking mode, starts disabled local unit_link = UnitLink(player_camera, level, player_camera.unit, 1, nil, 1, false) self.is_freecam_mode = true end local function spawn_player_character(self, pose) local land_character = self.land_character land_character.unit = World.spawn_unit(SimpleProject.world, "content/models/character/skm_chr_genrig_3p", pose) -- set up parent of link without actually linking for 3p controller local unit_link = UnitLink.manager:get(self.player_camera) unit_link:set_parent(land_character.unit, 1) -- initialize unit controller for character local controller = UnitController(land_character, land_character.unit, Appkit.input_mapper) controller:set_gravity_enabled(true) controller:set_mover("body") Mover.set_max_slope_angle(controller.mover, character_max_slope_angle) -- set the movement parameters according to the units script data values set_unit_script_data_values(land_character.unit) if Unit.has_visibility_group(land_character.unit, "first person only meshes") then Unit.set_visibility(land_character.unit, "first person only meshes", false) end end local function spawn_player_weapon(self, character_unit) local player_weapon = self.player_weapon player_weapon.unit = World.spawn_unit(SimpleProject.world, "content/models/weapon/foam_gun") -- link weapon to character local unit_link = UnitLink(player_weapon, SimpleProject.level, player_weapon.unit, 1, nil, 1, false) local node_index = Unit.node(character_unit, "s_genrig_RightHandAttach") unit_link:set_parent(character_unit, node_index) unit_link:link() -- set orientation local rotation = Quaternion.from_euler_angles_xyz(180, 0, 180) Unit.set_local_rotation(player_weapon.unit, 1, rotation) end ------------------------ Spawn Function ------------------------ ------------------------ Despawn Function ------------------------ local function cleanup_projectiles(self) for _, unit in pairs(self.spawn_projs) do if unit then ComponentManager.remove_components(unit) World.destroy_unit(SimpleProject.world, unit) end end self.spawn_projs = {} end local function despawn_weapon(self) local player_weapon = self.player_weapon if player_weapon.unit then ComponentManager.remove_components(player_weapon) World.destroy_unit(SimpleProject.world, player_weapon.unit) player_weapon.unit = nil player_weapon = {} end end local function despawn_character_1p(self) if self.unit_1p then World.destroy_unit(SimpleProject.world, self.unit_1p) self.unit_1p = nil end end local function despawn_character(self) local land_character = self.land_character if land_character.unit then ComponentManager.remove_components(land_character) World.destroy_unit(SimpleProject.world, land_character.unit) land_character.unit = nil land_character = {} end end local function despawn_freecam(self) local player_camera = self.player_camera ComponentManager.remove_components(player_camera) player_camera.unit = nil player_camera = {} end ------------------------ Despawn Function ------------------------ ------------------------ 3P Controller Function ------------------------ local function set_character_walking_3p(self) local character_controller = UnitController.manager:get(self.land_character) character_controller:set_move_speed(Vector3(0, 0, 0)) character_controller:set_yaw_speed(0) -- hide crosshair PlayerHud:set_crosshair_visibility(false) end local function set_character_aiming_3p(self) local character_controller = UnitController.manager:get(self.land_character) character_controller:set_move_speed(character_run_speed:unbox()) character_controller:set_yaw_speed(character_cam_yaw_speed_3p) -- show crosshair PlayerHud:set_crosshair_visibility(true) end local function update_camera_offset(self, yaw, pitch, is_blending) local player_camera = self.player_camera if yaw == nil or pitch == nil then local camera_offset = player_camera.offset:unbox() local current_offset_length = Vector3.length(camera_offset) yaw = yaw or math.atan2(camera_offset.y, camera_offset.x) pitch = pitch or math.acos(camera_offset.z / current_offset_length) end local sin_yaw = math.sin(yaw) local cos_yaw = math.cos(yaw) local sin_pitch = math.sin(pitch) local cos_pitch = math.cos(pitch) local x = player_camera.offset_length * cos_yaw * sin_pitch local y = player_camera.offset_length * sin_yaw * sin_pitch local z = player_camera.offset_length * cos_pitch if is_blending then self.camera_blend_offset_target = Vector3Box(x, y, z) else self.player_camera.offset = Vector3Box(x, y, z) end end local function center_camera(self, is_blending) local player_camera = self.player_camera local land_unit = self.land_character.unit local land_rotation = Unit.world_rotation(land_unit, 1) local euler_x, euler_y, euler_z = Quaternion.to_euler_angles_xyz(land_rotation) local yaw = camera_default_yaw + math.rad(euler_z) local pitch = camera_default_pitch update_camera_offset(self, yaw, pitch, is_blending) end ------------------------ 3P Controller Function ------------------------ ------------------------ 1P Controller Function ------------------------ local function set_1p_stand_position(self) -- assuming camera linked to character and unit_1p mesh linked to camera local camera_unit = self.player_camera.unit -- set the camera to the character unit head position local camera_offset = camera_positioning_1p:unbox() local camera_position = Unit.local_position(camera_unit, 1) local camera_blend_position = Vector3.lerp(camera_offset, camera_position, 0.5) Unit.set_local_position(camera_unit, 1, camera_blend_position) -- set the unit_1p mesh to character unit position local mesh_1p_position = -camera_positioning_1p:unbox() Unit.set_local_position(self.unit_1p, 1, mesh_1p_position) end local function get_aim_modifier(self) local camera_wrapper = CameraWrapper.manager:get(self.player_camera) local fov = Camera.vertical_fov(camera_wrapper.camera) local modifier = 1 if self.is_firstperson then if self.is_aim then modifier = fov / fov_1p end else if self.is_aim then modifier = fov / fov_3p end end return modifier end local function update_head_bob(self, dt, is_running) local cam_unit = self.player_camera.unit local land_unit = self.land_character.unit local cam_offset = camera_positioning_1p:unbox() local input = Appkit.input_mapper:get_motion_input() local speed = Vector3.length(input.move) if speed < 0.01 then Unit.set_local_position(cam_unit, 1, Vector3.lerp(cam_offset, Unit.local_position(cam_unit, 1), 0.5)) -- smooth it with lerp return end local move_direction = compute_move_direction(input.move) if started_moving then self.current_direction = move_direction else self.current_direction = blend_to_move_direction(self.current_direction, move_direction, dt) end local move_speed = 0 local bob_frequency = 0 local bob_trans_mult = 0 local bob_amp = 0 if is_running then move_speed = character_sprint_speed.x bob_frequency = head_bob_freq_sprint bob_trans_mult = head_bob_trav_mult_sprint bob_amp = head_bob_amp_1p_sprint else move_speed = character_walk_speed.x bob_frequency = head_bob_freq_walk bob_trans_mult = head_bob_trav_mult_walk bob_amp = head_bob_amp_1p_walk end if self.current_direction == 1 or self.current_direction == 3 then -- purely strafing to left or right, or not moving return else -- walking somewhat forward or somewhat backward self.walk_distance = self.walk_distance + speed * move_speed * dt -- assumes isotropic speed end local x_head = bob_trans_mult * bob_amp * math.sin(3 / 2 * bob_frequency * self.walk_distance) + cam_offset.x local y_head = cam_offset.y local z_head = bob_amp * math.abs(math.sin(bob_frequency * self.walk_distance)) + cam_offset.z local cameraLocalPos = Vector3(x_head, y_head, z_head) Unit.set_local_position(cam_unit, 1, Vector3.lerp(cameraLocalPos, Unit.local_position(cam_unit, 1), 0.5)) -- smooth it with lerp end ------------------------ 1P Controller Function ------------------------ ------------------------ Enable Function ------------------------ local function enable_third_person_mode(self) local land_character = self.land_character local player_camera = self.player_camera -- turn on the visiblity of the land unit Unit.set_unit_visibility(land_character.unit, true) despawn_character_1p(self) despawn_weapon(self) spawn_player_weapon(self, land_character.unit) -- set 3p FOV local camera = Unit.camera(player_camera.unit, "camera") Camera.set_vertical_fov(camera, fov_3p) Camera.set_near_range(camera, 0.1) -- default is 0.1 -- attach camera to character player_camera.offset_length = character_cam_offset_length_3p set_character_walking_3p(self) local unit_link = UnitLink.manager:get(player_camera) if unit_link.is_linked then unit_link:unlink() end player_camera.target_offset = Vector3Box(camera_stand_target:unbox()) center_camera(self, false) local freecam_controller = UnitController.manager:get(player_camera) freecam_controller:set_pitch_speed(0) local character_controller = UnitController.manager:get(land_character) character_controller:set_move_speed(Vector3(0, 0, 0)) character_controller:set_yaw_speed(0) character_controller:set_pitch_speed(0) self.is_firstperson = false end local function enable_first_person_mode(self) local land_unit = self.land_character.unit local player_camera = self.player_camera -- attach camera to character local unit_link = UnitLink.manager:get(player_camera) unit_link:link() -- reset character pose Unit.set_local_pose(land_unit, 1, Matrix4x4.identity()) Unit.set_local_pose(player_camera.unit, 1, Matrix4x4.identity()) -- offset camera to 1p view local camera_wrapper = CameraWrapper.manager:get(player_camera) local pose_offset = Matrix4x4.identity() Matrix4x4.set_translation(pose_offset, camera_positioning_1p:unbox()) camera_wrapper:set_local_pose(pose_offset) -- set 1p FOV and near/far distances local camera = Unit.camera(player_camera.unit, "camera") Camera.set_vertical_fov(camera, fov_1p) Camera.set_near_range(camera, 0.01) -- default is 0.1 -- turn off existing char mesh and make new one linked as child to camera, does not use the aim animation to rotate with view -- remove the current weapon despawn_weapon(self) -- turn off the visiblity of the nominal char unit Unit.set_unit_visibility(land_unit, false) -- completely constrain the character controller to only control translation local character_controller = UnitController.manager:get(self.land_character) character_controller:set_yaw_speed(character_cam_yaw_speed_1p) character_controller:set_pitch_speed(0) -- freecam controller will control all viewport rotations local freecam_controller = UnitController.manager:get(player_camera) freecam_controller.pitch_min = camera_pitch_min_1p freecam_controller.pitch_max = camera_pitch_max_1p freecam_controller:set_pitch_speed(character_cam_pitch_speed_1p) freecam_controller:set_yaw_speed(0) -- spawn a new unit that only shows arms and have it rotate passively with camera rotation via UnitLink local unit_1p = World.spawn_unit(SimpleProject.world, "content/models/character/skm_chr_genrig_3p", Unit.local_pose(player_camera.unit, 1)) Unit.set_animation_state_machine(unit_1p, "content/animations/1p_character/skm_chr_genrig_1p") self.unit_1p = unit_1p if Unit.has_visibility_group(unit_1p, "third person only meshes") then Unit.set_visibility(unit_1p, "third person only meshes", false) end -- link the first person mesh as a child to the camera unit UnitLink(unit_1p, SimpleProject.level, unit_1p, 1, player_camera.unit, 1) -- will automatically link true -- offset 1p mesh so that the head lines up with the camera (opposite of camera offset above) Unit.set_local_position(unit_1p, 1, -camera_positioning_1p:unbox()) set_1p_stand_position(self) -- spawn character weapon for new 1p mesh spawn_player_weapon(self, unit_1p) -- show crosshair PlayerHud:set_crosshair_visibility(true) self.is_firstperson = true end local function enable_walk_mode(self) local player_camera = self.player_camera -- save off freecam camera position local camera_wrapper = CameraWrapper.manager:get(player_camera) self.saved_freecam_pose:store(camera_wrapper:local_pose()) -- disable movement and yaw on camera local freecam_controller = UnitController.manager:get(player_camera) freecam_controller:set_move_speed(Vector3(0, 0, 0)) freecam_controller:set_yaw_speed(0) -- spawn character spawn_player_character(self, get_player_start_pose()) self.is_freecam_mode = false enable_third_person_mode(self) end local function enable_free_mode(self) local player_camera = self.player_camera -- detach camera from character local unit_link = UnitLink.manager:get(player_camera) if unit_link.is_linked then unit_link:unlink() end despawn_weapon(self) despawn_character(self) despawn_character_1p(self) -- enable camera movement and yaw local camera_controller = UnitController.manager:get(player_camera) camera_controller:set_move_speed(free_cam_move_speed:unbox()) camera_controller:set_yaw_speed(free_cam_yaw_speed) camera_controller:set_pitch_speed(free_cam_pitch_speed) camera_controller.pitch_min = nil camera_controller.pitch_max = nil -- set freecam to saved off camera position local camera_wrapper = CameraWrapper.manager:get(player_camera) camera_wrapper:set_local_pose(self.saved_freecam_pose:unbox()) -- hide crosshair PlayerHud:set_crosshair_visibility(false) self.is_freecam_mode = true self.is_firstperson = false end ------------------------ Enable Function ------------------------ ------------------------ Check Function ------------------------ local function check_camera_mode(self) if Appkit.input_mapper:if_toggle_camera() or PlayerHud:check_toggle_camera() then if self.is_freecam_mode then enable_walk_mode(self) else enable_free_mode(self) end end end local function check_first_person_mode(self) if Appkit.input_mapper:if_switch_perspective() then if self.is_firstperson then enable_third_person_mode(self) else enable_first_person_mode(self) end end end local function check_sprint(self) self.is_sprinting = Appkit.input_mapper:if_run() or PlayerHud:check_sprint() if not self.is_freecam_mode then local input = Appkit.input_mapper:get_motion_input() if self.is_firstperson then self.is_sprinting = self.is_sprinting and not self.is_aim and (input.move.y > 0.9 and math.abs(input.move.x) < 0.5) else self.is_sprinting = self.is_sprinting and not self.is_aim and Vector3.length(input.move) > 0.9 end end end local function check_moving(self) local input = Appkit.input_mapper:get_motion_input() local speed = Vector3.length(input.move) if self.is_moving == false and speed > 0 then self.started_moving = true else self.started_moving = false end self.is_moving = speed > 0 end local function check_move_speed(self) --TODO: refactor so this only does sprint check. Get speed in another function if self.is_freecam_mode then local unit_controller = UnitController.manager:get(self.player_camera) if self.is_sprinting then unit_controller:set_move_speed(free_cam_sprint_speed:unbox()) else unit_controller:set_move_speed(free_cam_move_speed:unbox()) end else local land_character = self.land_character local unit_controller = UnitController.manager:get(land_character) if self.is_firstperson then if self.is_sprinting then if not is_character_in_air(land_character) then unit_controller:set_move_speed(character_sprint_speed:unbox()) self.jumped_while_sprinting = false elseif self.jumped_while_sprinting then unit_controller:set_move_speed(character_sprint_speed_in_air:unbox()) else unit_controller:set_move_speed(character_walk_speed_in_air:unbox()) end else if not is_character_in_air(land_character) then unit_controller:set_move_speed(character_run_speed:unbox()) self.jumped_while_sprinting = false elseif self.jumped_while_sprinting then unit_controller:set_move_speed(character_sprint_speed_in_air:unbox()) else unit_controller:set_move_speed(character_run_speed_in_air:unbox()) end end else if self.is_sprinting then if not is_character_in_air(land_character) then self.character_speed_3p = character_sprint_speed_3p self.jumped_while_sprinting = false elseif self.jumped_while_sprinting then self.character_speed_3p = character_sprint_speed_in_air_3p else self.character_speed_3p = character_run_speed_in_air_3p end else if not is_character_in_air(land_character) then if not self.is_crouching then local input = Appkit.input_mapper:get_motion_input() self.character_speed_3p = character_walk_speed_3p + Vector3.length(input.move) * (character_run_speed_3p - character_walk_speed_3p) else self.character_speed_3p = character_walk_speed_3p end self.jumped_while_sprinting = false elseif self.jumped_while_sprinting then self.character_speed_3p = character_sprint_speed_in_air_3p else self.character_speed_3p = character_run_speed_in_air_3p end end end end end local function check_aim(self, dt) local aim = Appkit.input_mapper:if_aim() or PlayerHud:check_aim() if self.is_aim ~= aim then if self.is_firstperson then if aim then self.camera_blend_fov_target = fov_1p_aim else self.camera_blend_fov_target = fov_1p end else local player_camera = self.player_camera if aim then self.camera_blend_fov_target = fov_3p_aim -- blend camera offset local camera_offset = player_camera.offset:unbox() local current_offset_length = Vector3.length(camera_offset) local current_yaw = math.atan2(camera_offset.y, camera_offset.x) local current_pitch = math.acos(camera_offset.z / current_offset_length) player_camera.offset_length = character_cam_offset_length_3p_aim update_camera_offset(self, current_yaw, current_pitch, true) -- blend character rotation local camera_wrapper = CameraWrapper.manager:get(player_camera) local camera_rotation = camera_wrapper:world_rotation() local camera_rotation_z = get_rotation_z(camera_rotation) self.character_blend_target = QuaternionBox(camera_rotation_z) set_character_aiming_3p(self) else self.camera_blend_fov_target = fov_3p player_camera.offset_length = character_cam_offset_length_3p set_character_walking_3p(self) end end end self.is_aim = aim local controller = UnitController.manager:get(self.land_character) controller.fov_modifier = get_aim_modifier(self) end local function check_projectile_fire(self) -- retrun if 3p non-aim mode if not self.is_firstperson and not self.is_aim then return end -- check input local is_firing = Appkit.input_mapper:if_fire() or PlayerHud:check_fire() if not is_firing then return end -- return if firing too fast local world = SimpleProject.world local current_time = world:time() local time_difference = current_time - self.last_fire_time if time_difference < 0.075 then return end -- the projectile spawn position is based off a fire node in the weapon unit, which inextricably links the projectile spawning to the weapon animation local player_weapon = self.player_weapon local weapon_unit = player_weapon.unit local fire_node_id = Unit.node(weapon_unit, "weapon_fire_node") local fire_node_position = Unit.world_position(weapon_unit, fire_node_id) local player_camera = self.player_camera local camera_wrapper = CameraWrapper.manager:get(player_camera) local source_pose = camera_wrapper:world_pose() local source_forward = Matrix4x4.forward(source_pose) -- here we'd then use our offset position. right now it's still using the old fire node position, though local proj_pose = Matrix4x4.from_translation(fire_node_position + source_forward * 0.1) local proj_unit = World.spawn_unit(world, "content/models/props/foam_projectile", proj_pose) if proj_unit then local physics_world = World.physics_world(world) local source_position = camera_wrapper:world_position() local hit_found, hit_position, hit_distance, hit_normal, hit_actor = PhysicsWorld.raycast(physics_world, source_position, source_forward, "collision_filter", "aim") -- get target position local minimum_distance = Vector3.distance(fire_node_position, source_position) local target_position = source_position + source_forward * 20 if hit_found and hit_distance > minimum_distance then target_position = hit_position end -- compute impulse local proj_forward = Vector3.normalize(target_position - fire_node_position) * 750 proj_forward = proj_forward + Vector3(0, 0, 10) -- add tiny bit of up to combat gravity vs cursor -- add noise to impulse local noise_vector = PlayerUtil.random_unit_vector3() local noise_scalar = 1 / time_difference if self.is_aim == false then noise_scalar = noise_scalar * 2 end proj_forward = proj_forward + noise_vector * noise_scalar -- apply impulse local proj_act = Unit.actor(proj_unit, "foam_projectile") Actor.add_impulse(proj_act, proj_forward) play_fire_sound() local spawn_projs = self.spawn_projs spawn_projs[current_time] = proj_unit self.last_fire_time = current_time else print("did not spawn projectile") end end local function check_crouch(self) -- return if character is in air, could be taken out if there is an additional animation for crouch in air if is_character_in_air(self.land_character) then return end local is_crouching = Appkit.input_mapper:if_crouch() or PlayerHud:check_crouch() if self.is_firstperson == false and self.is_crouching ~= is_crouching then if is_crouching then self.camera_blend_center_target = camera_crouch_target else self.camera_blend_center_target = camera_stand_target end end self.is_crouching = is_crouching end local function check_jump(self) local is_jumping = Appkit.input_mapper:if_jump() or PlayerHud:check_jump() if is_jumping then local land_character = self.land_character if not is_character_in_air(land_character) then local controller = UnitController.manager:get(land_character) controller:add_impulse(Vector3(0, 0, character_jump_strength)) if self.is_sprinting then self.jumped_while_sprinting = true end end end end -- for 3rd person controller local function check_move_character(self, dt) -- return if aim mode if self.is_aim then return end local input = Appkit.input_mapper:get_motion_input() -- return if no move input if Vector3.equal(input.move, Vector3.zero()) then return end local player_camera = self.player_camera local camera_wrapper = CameraWrapper.manager:get(player_camera) local camera_rotation = camera_wrapper:world_rotation() local camera_rotation_z = get_rotation_z(camera_rotation) local move_direction = Quaternion.rotate(camera_rotation_z, Vector3.normalize(input.move)) local target_rotation = Quaternion.look(move_direction) -- set target rotation to blend self.character_blend_target = QuaternionBox(target_rotation) -- translate character local land_mover = Unit.mover(self.land_character.unit) -- walk_speed + input local move_offset = move_direction * self.character_speed_3p * dt Mover.move(land_mover, move_offset, dt) -- update camera offset local camera_offset = player_camera.offset:unbox() - move_offset Vector3Box.store(player_camera.offset, camera_offset) end -- for 3rd person controller local function check_center_camera(self) -- return if aim mode if self.is_aim then return end -- TODO: support touch if Appkit.input_mapper:if_center_camera() then center_camera(self, true) end end -- for 3rd person controller local function check_rotate_camera(self, dt) local input = Appkit.input_mapper:get_motion_input() -- return if no pan input if Vector3.equal(input.pan, Vector3.zero()) then return end local camera_offset = self.player_camera.offset:unbox() local current_offset_length = Vector3.length(camera_offset) local current_yaw = math.atan2(camera_offset.y, camera_offset.x) local current_pitch = math.acos(camera_offset.z / current_offset_length) local yaw_speed = camera_yaw_speed local pitch_speed if self.is_aim then pitch_speed = camera_pitch_aim_speed else pitch_speed = camera_pitch_speed end local desired_yaw = current_yaw - input.pan.x * yaw_speed * dt local desired_pitch = current_pitch - input.pan.y * pitch_speed * dt -- boundry check if desired_pitch < camera_pitch_min then desired_pitch = camera_pitch_min elseif desired_pitch > camera_pitch_max then desired_pitch = camera_pitch_max end if self.is_aim then local land_unit = self.land_character.unit local land_rotation = Unit.world_rotation(land_unit, 1) local euler_x, euler_y, euler_z = Quaternion.to_euler_angles_xyz(land_rotation) desired_yaw = math.rad(euler_z - 90) end update_camera_offset(self, desired_yaw, desired_pitch, false) end local function check_exit_level(self) if Appkit.input_mapper:if_exit_level() or PlayerHud:exit_level() then SimpleProject.change_level(Project.level_names.menu) end end ------------------------ Check Function ------------------------ ------------------------ Update Function ------------------------ local function update_character_blend(self, dt) if self.character_blend_target == nil then return end local land_unit = self.land_character.unit local source = Unit.local_rotation(land_unit, 1) local target = self.character_blend_target:unbox() local ratio = dt * character_rotation_blending_speed if ratio > 1 then ratio = 1 end local blend = Quaternion.lerp(source, target, ratio) -- if blend is close to target enough if Quaternion.dot(blend, target) > 0.99 then blend = target end Unit.set_local_rotation(land_unit, 1, blend) -- blending is done if Quaternion.equal(blend, target) then self.character_blend_target = nil end end local function update_camera_blend(self, dt) local player_camera = self.player_camera local camera_wrapper = CameraWrapper.manager:get(player_camera) -- blend camera offset if self.camera_blend_offset_target then local source = player_camera.offset:unbox() local target = self.camera_blend_offset_target:unbox() local ratio = dt * camera_offset_blending_speed if ratio > 1 then ratio = 1 end local blend = Vector3.lerp(source, target, ratio) -- if blend is close to target enough if Vector3.distance_squared(blend, target) < 0.01 then blend = target end Vector3Box.store(player_camera.offset, blend) -- blending is done if Vector3.equal(blend, target) then self.camera_blend_offset_target = nil end end -- blend camera target offset (standing or crouching) if self.camera_blend_center_target then local source = player_camera.target_offset:unbox() local target = self.camera_blend_center_target:unbox() local ratio = dt * camera_center_blending_speed if ratio > 1 then ratio = 1 end local blend = Vector3.lerp(source, target, ratio) -- if blend is close to target enough if Vector3.distance_squared(blend, target) < 0.01 then blend = target end Vector3Box.store(player_camera.target_offset, blend) -- blending is done if Vector3.equal(blend, target) then self.camera_blend_center_target = nil end end -- blend camera fov if self.camera_blend_fov_target then local source = Camera.vertical_fov(camera_wrapper.camera) local target = self.camera_blend_fov_target local ratio = dt * camera_fov_blending_speed if ratio > 1 then ratio = 1 end local blend = (1 - ratio) * source + ratio * target -- if blend is close to target enough if math.abs(blend - target) < 0.01 then blend = target end Camera.set_vertical_fov(camera_wrapper.camera, blend) -- blending is done if blend == target then self.camera_blend_fov_target = nil end end end local function set_aim_target_position(self) local character_unit = self.land_character.unit local aim_pose if self.is_aim or self.is_firstperson then local player_camera = self.player_camera local camera_wrapper = CameraWrapper.manager:get(player_camera) aim_pose = camera_wrapper:world_pose() else aim_pose = Unit.world_pose(character_unit, 1) end local target = Matrix4x4.translation(aim_pose) + Matrix4x4.forward(aim_pose) * 1000 -- this number controls how many meters in the front of the camera the focal point is Unit.animation_set_constraint_target(character_unit, 1, target) end local function animate_character_3p(self, dt) -- movement animation local input = Appkit.input_mapper:get_motion_input() local speed = Vector3.length(input.move) local land_character = self.land_character local land_unit = land_character.unit if self.is_aim then Unit.animation_event(land_unit, "aim_up") else Unit.animation_event(land_unit, "aim_down") end if self.is_moving then -- play jump animation while moving if is_character_in_air(land_character) then Unit.animation_event(land_unit, "jump") -- setting sprint animation and move direction elseif self.is_sprinting then Unit.animation_event(land_unit, "sprint") -- setting crouch animation and move direction elseif self.is_crouching then Unit.animation_event(land_unit, "crouch_walk") -- setting walk animation and move direction else Unit.animation_event(land_unit, "stand_walk") end local move_direction = compute_move_direction(input.move) if self.started_moving then self.current_direction = move_direction else self.current_direction = blend_to_move_direction(self.current_direction, move_direction, dt) end local move_dir_var = Unit.animation_find_variable(land_unit, "move_direction") local move_speed_var = Unit.animation_find_variable(land_unit, "move_speed") if self.is_aim then Unit.animation_set_variable(land_unit, move_dir_var, self.current_direction) Unit.animation_set_variable(land_unit, move_speed_var, speed) -- only walking when aiming else -- normally we just rotate the character to move -- so animation dir is just forward (NORTH) Unit.animation_set_variable(land_unit, move_dir_var, 2) Unit.animation_set_variable(land_unit, move_speed_var, speed) end --play jump animation not moving elseif is_character_in_air(land_character) then Unit.animation_event(land_unit, "jump") move_direction = self.current_direction --setting crouch idle animation elseif self.is_crouching then Unit.animation_event(land_unit, "crouch_idle") --setting stand idle animation else Unit.animation_event(land_unit, "stand_idle") end self.previous_player_altitude = Unit.world_position(land_unit, 1).z -- update character aiming set_aim_target_position(self) if Unit.has_node(land_unit, "s_genrig_RightHand") and Unit.has_node(land_unit, "s_genrig_AimNode") then local hand_index = Unit.node(land_unit, "s_genrig_RightHand") local aim_index = Unit.node(land_unit, "s_genrig_AimNode") local hand_aim_offset = Unit.world_position(land_unit, hand_index) - Unit.world_position(land_unit, 1) hand_aim_offset = hand_aim_offset + Vector3(0, 100, 0) Unit.set_local_position(land_unit, aim_index, hand_aim_offset) end end local function animate_character_1p(self, dt) local land_character = self.land_character local land_unit = self.unit_1p -- movement animation if self.is_aim then Unit.animation_event(land_unit, "aim_up") else Unit.animation_event(land_unit, "aim_down") end if land_character and self.is_moving and not is_character_in_air(land_character) then update_head_bob(self, dt, self.is_sprinting) elseif land_character and not self.is_moving then self.walk_distance = 0.0 set_1p_stand_position(self) end -- animate standard (3p) character that is hidden for sounds and physics interaction animate_character_3p(self, dt) end local function animate_camera(self, dt) local player_camera = self.player_camera local camera_wrapper = CameraWrapper.manager:get(player_camera) local camera_offset = player_camera.offset:unbox() local land_unit = self.land_character.unit local land_position = Unit.world_position(land_unit, 1) local land_rotation = Unit.world_rotation(land_unit, 1) local target_offset = player_camera.target_offset:unbox() local desired_target_offset = Quaternion.rotate(land_rotation, target_offset) local desired_camera_offset = Vector3.normalize(camera_offset) * player_camera.offset_length local desired_translation = land_position + desired_target_offset + desired_camera_offset local desired_rotation = Quaternion.look(-desired_camera_offset) local camera_pose = Matrix4x4.from_quaternion_position(desired_rotation, desired_translation) camera_wrapper:set_local_pose(camera_pose) Vector3Box.store(player_camera.offset, desired_camera_offset) end local function update_projectiles(self) local world = SimpleProject.world for time, unit in pairs(self.spawn_projs) do local proj_act = Unit.actor(unit, "foam_projectile") local dt = world:time() - time if dt > 5 and unit then table.remove(self.spawn_projs, time) ComponentManager.remove_components(unit) World.destroy_unit(world, unit) self.spawn_projs[time] = nil end end end ------------------------ Update and Animate Function ------------------------ ------------------------ Player Function ------------------------ function Player:init() -- The Basic Project gameplay design is for the player character to spawn, but the -- camera is initially set to freecam and the player character will be stationary. self.player_camera = {} self.land_character = {} self.player_weapon = {} self.spawn_projs = {} self.is_freecam_mode = false self.is_firstperson = false self.saved_freecam_pose = Matrix4x4Box() self.is_aim = false self.is_moving = false self.started_moving = false self.is_crouching = false self.is_sprinting = false self.current_direction = 0 self.last_fire_time = 0 self.previous_player_altitude = 0 self.character_speed_3p = 0 self.walk_distance = 0 self.jumped_while_sprinting = false self.character_blend_target = nil self.camera_blend_offset_target = nil self.camera_blend_center_target = nil self.camera_blend_fov_target = nil end -- Main Player Spawn function -- player starts in Free Cam mode, and can toggle to spawning a character and back to Free Cam function Player.spawn_player(level, view_position, view_rotation) if not level then print("ERROR: No current level - cannot spawn") return end local player = Player() PlayerHud:init_hud() spawn_freecam_player(player, level, view_position, view_rotation) enable_walk_mode(player) play_spawn_sound() play_ambient_sound() Appkit.manage_level_object(level, Player, player) end function Player:update(dt) PlayerHud:update(dt) check_camera_mode(self) check_moving(self) check_sprint(self) check_move_speed(self) -- non free cam mode if self.is_freecam_mode == false then check_first_person_mode(self) check_aim(self, dt) check_projectile_fire(self) check_crouch(self) check_jump(self) -- 3p character and camera controller if self.is_firstperson == false then check_move_character(self, dt) check_rotate_camera(self, dt) check_center_camera(self) update_character_blend(self, dt) end update_camera_blend(self, dt) if self.is_firstperson then animate_character_1p(self, dt) else animate_character_3p(self, dt) animate_camera(self, dt) end end update_projectiles(self) check_exit_level(self) end function Player:shutdown() PlayerHud:shutdown() cleanup_projectiles(self) despawn_weapon(self) despawn_character_1p(self) despawn_character(self) despawn_freecam(self) end ------------------------ Player Function ------------------------ return Player