Character template: /player.lua — code sample - Stingray Lua API Reference

Character template: /player.lua — code sample

Code

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