-- Lua Third person camera and character controller script -- This script will load a simple scene with a character that can be controlled -- -- CONTROLS: -- WASD/left thumbstick: walk -- SHIFT/right shoulder button: walk -> jog -- E/left shoulder button: jog -> run -- SPACE/gamepad X/gamepad button 2: jump -- Right Mouse Button/Right thumbstick: rotate camera -- Scoll middle mouse/Left-Right triggers: adjust camera distance -- H: toggle debug draw -- L: toggle framerate lock -- ESCAPE key: quit -- ENTER: reload script -- Debug Draw Helper local DrawAxis = function(point,f) DrawLine(point,point:Add(Vector(f,0,0)),Vector(1,0,0,1)) DrawLine(point,point:Add(Vector(0,f,0)),Vector(0,1,0,1)) DrawLine(point,point:Add(Vector(0,0,f)),Vector(0,0,1,1)) end local debug = true -- press H to toggle local allow_pushaway_NPC = true -- You can decide whether NPCs can be pushed away by player local framerate_lock = false local framerate_lock_target = 20 local slope_threshold = 0.2 -- How much slopeiness will cause character to slide down instead of standing on it local gravity = -30 local scene = GetScene() local Layers = { Player = 1 << 0, NPC = 1 << 1, } local States = { IDLE = "idle", WALK = "walk", JOG = "jog", RUN = "run", JUMP = "jump", SWIM_IDLE = "swim_idle", SWIM = "swim", DANCE = "dance" } local enable_footprints = true local footprint_texture = Texture(script_dir() .. "assets/footprint.dds") local footprints = {} local animations = {} local function LoadAnimations(model_name) local anim_scene = Scene() LoadModel(anim_scene, model_name) animations = { IDLE = anim_scene.Entity_FindByName("idle"), WALK = anim_scene.Entity_FindByName("walk"), JOG = anim_scene.Entity_FindByName("jog"), RUN = anim_scene.Entity_FindByName("run"), JUMP = anim_scene.Entity_FindByName("jump"), SWIM_IDLE = anim_scene.Entity_FindByName("swim_idle"), SWIM = anim_scene.Entity_FindByName("swim"), DANCE = anim_scene.Entity_FindByName("dance") } scene.Merge(anim_scene) end local character_capsules = {} local function Character(model_name, start_position, face, controllable) local self = { model = INVALID_ENTITY, target_rot_horizontal = 0, target_rot_vertical = 0, target_height = 0, current_anim = INVALID_ENTITY, idle_anim = INVALID_ENTITY, walk_anim = INVALID_ENTITY, jog_anim = INVALID_ENTITY, run_anim = INVALID_ENTITY, jump_anim = INVALID_ENTITY, swim_idle_anim = INVALID_ENTITY, dance_anim = INVALID_ENTITY, swim_anim = INVALID_ENTITY, all_anims = {}, neck = INVALID_ENTITY, head = INVALID_ENTITY, left_hand = INVALID_ENTITY, right_hand = INVALID_ENTITY, left_foot = INVALID_ENTITY, right_foot = INVALID_ENTITY, left_toes = INVALID_ENTITY, right_toes = INVALID_ENTITY, face = Vector(0,0,1), -- forward direction (smoothed) face_next = Vector(0,0,1), -- forward direction in current frame force = Vector(), velocity = Vector(), savedPointerPos = Vector(), walk_speed = 0.2, jog_speed = 0.4, run_speed = 0.8, jump_speed = 8, swim_speed = 0.5, layerMask = ~0, -- layerMask will be used to filter collisions scale = Vector(1, 1, 1), rotation = Vector(0,math.pi,0), start_position = Vector(0, 1, 0), position = Vector(), controllable = true, fixed_update_remain = 0, root_bone_offset = 0, foot_placed_left = false, foot_placed_right = false, patrol_waypoints = {}, patrol_next = 0, patrol_wait = 0, patrol_reached = false, hired = nil, Create = function(self, model_name, start_position, face, controllable) self.start_position = start_position self.face = face self.face_next = face self.controllable = controllable if controllable then self.layerMask = Layers.Player else self.layerMask = Layers.NPC end self.model = LoadModel(model_name) local layer = scene.Component_GetLayer(self.model) layer.SetLayerMask(self.layerMask) self.state = States.IDLE self.state_prev = self.state for i,entity in ipairs(scene.Entity_GetHumanoidArray()) do if scene.Entity_IsDescendant(entity, self.model) then self.humanoid = entity local humanoid = scene.Component_GetHumanoid(self.humanoid) humanoid.SetLookAtEnabled(false) self.neck = humanoid.GetBoneEntity(HumanoidBone.Neck) self.head = humanoid.GetBoneEntity(HumanoidBone.Head) self.left_hand = humanoid.GetBoneEntity(HumanoidBone.LeftHand) self.right_hand = humanoid.GetBoneEntity(HumanoidBone.RightHand) self.left_foot = humanoid.GetBoneEntity(HumanoidBone.LeftFoot) self.right_foot = humanoid.GetBoneEntity(HumanoidBone.RightFoot) self.left_toes = humanoid.GetBoneEntity(HumanoidBone.LeftToes) self.right_toes = humanoid.GetBoneEntity(HumanoidBone.RightToes) -- Create a base capsule collider if it's not yet configured for character: -- It will be used for movement logic and GPU collision effects if scene.Component_GetCollider(entity) == nil then local collider = scene.Component_CreateCollider(entity) collider.SetCPUEnabled(false) collider.SetGPUEnabled(true) collider.Shape = ColliderShape.Capsule collider.Radius = 0.3 collider.Offset = Vector(0, collider.Radius, 0) collider.Tail = Vector(0, 1.4, 0) local head_transform = scene.Component_GetTransform(self.head) if head_transform ~= nil then collider.Tail = head_transform.GetPosition() end end self.collider = entity break end end for i,entity in ipairs(scene.Entity_GetExpressionArray()) do if scene.Entity_IsDescendant(entity, self.model) then self.expression = entity self.happy = 0 break end end self.root = scene.Entity_FindByName("Root", self.model) self.idle_anim = scene.RetargetAnimation(self.humanoid, animations.IDLE, false) self.walk_anim = scene.RetargetAnimation(self.humanoid, animations.WALK, false) self.jog_anim = scene.RetargetAnimation(self.humanoid, animations.JOG, false) self.run_anim = scene.RetargetAnimation(self.humanoid, animations.RUN, false) self.jump_anim = scene.RetargetAnimation(self.humanoid, animations.JUMP, false) self.swim_idle_anim = scene.RetargetAnimation(self.humanoid, animations.SWIM_IDLE, false) self.swim_anim = scene.RetargetAnimation(self.humanoid, animations.SWIM, false) self.dance_anim = scene.RetargetAnimation(self.humanoid, animations.DANCE, false) for i,entity in ipairs(scene.Entity_GetAnimationArray()) do if scene.Entity_IsDescendant(entity, self.model) then table.insert(self.all_anims, entity) end end local model_transform = scene.Component_GetTransform(self.model) model_transform.ClearTransform() model_transform.Scale(self.scale) model_transform.Rotate(self.rotation) model_transform.Translate(self.start_position) model_transform.UpdateTransform() self.target_height = scene.Component_GetTransform(self.neck).GetPosition().GetY() end, Jump = function(self,f) self.force.SetY(f) self.state = States.JUMP end, MoveDirection = function(self,dir) local rotation_matrix = matrix.Multiply(matrix.RotationY(self.target_rot_horizontal), matrix.RotationX(self.target_rot_vertical)) dir = vector.Transform(dir.Normalize(), rotation_matrix) dir.SetY(0) local dot = vector.Dot(self.face, dir) if(dot < 0) then self.face = vector.TransformNormal(self.face, matrix.RotationY(math.pi * 0.01)) -- Turn around 180 degrees easily when wanting to go backwards end self.face_next = dir self.face_next = self.face_next.Normalize() if(dot > 0) then local speed = 0 if self.state == States.WALK then speed = self.walk_speed elseif self.state == States.JOG then speed = self.jog_speed elseif self.state == States.RUN then speed = self.run_speed elseif self.state == States.SWIM then speed = self.swim_speed end self.force = vector.Add(self.force, self.face:Multiply(Vector(speed,speed,speed))) end end, Update = function(self) local dt = getDeltaTime() local humanoid = scene.Component_GetHumanoid(self.humanoid) humanoid.SetLookAtEnabled(false) local model_transform = scene.Component_GetTransform(self.model) local savedPos = model_transform.GetPosition() model_transform.ClearTransform() model_transform.MatrixTransform(matrix.LookTo(Vector(),self.face):Inverse()) model_transform.Scale(self.scale) model_transform.Rotate(self.rotation) model_transform.Translate(savedPos) model_transform.UpdateTransform() if controllable then -- Camera target control: -- read from gamepad analog stick: local diff = input.GetAnalog(GAMEPAD_ANALOG_THUMBSTICK_R) diff = vector.Multiply(diff, dt * 4) -- read from mouse: if(input.Down(MOUSE_BUTTON_RIGHT)) then local mouseDif = input.GetPointerDelta() mouseDif = mouseDif:Multiply(dt * 0.3) diff = vector.Add(diff, mouseDif) input.SetPointer(self.savedPointerPos) input.HidePointer(true) else self.savedPointerPos = input.GetPointer() input.HidePointer(false) end self.target_rot_horizontal = self.target_rot_horizontal + diff.GetX() self.target_rot_vertical = math.clamp(self.target_rot_vertical + diff.GetY(), -math.pi * 0.3, math.pi * 0.4) -- vertical camers limits if self.dance_anim ~= INVALID_ENTITY and self.state == States.IDLE and input.Press(string.byte('C')) then self.state = States.DANCE end end if self.state == States.DANCE then self.happy = math.lerp(self.happy, 1, 0.1) else self.happy = math.lerp(self.happy, 0, 0.1) end local expression = scene.Component_GetExpression(self.expression) expression.SetPresetWeight(ExpressionPreset.Happy, self.happy) -- state and animation update if(self.state == States.IDLE) then self.current_anim = self.idle_anim elseif(self.state == States.WALK) then self.current_anim = self.walk_anim elseif(self.state == States.JOG) then self.current_anim = self.jog_anim elseif(self.state == States.RUN) then self.current_anim = self.run_anim elseif(self.state == States.JUMP) then self.current_anim = self.jump_anim elseif(self.state == States.SWIM_IDLE) then self.current_anim = self.swim_idle_anim elseif(self.state == States.SWIM) then self.current_anim = self.swim_anim elseif(self.state == States.DANCE) then self.current_anim = self.dance_anim end local current_anim = scene.Component_GetAnimation(self.current_anim) if current_anim ~= nil then -- Play current anim: current_anim.Play() if self.state_prev ~= self.state then -- If anim just started in this frame, reset timer to beginning: current_anim.SetTimer(current_anim.GetStart()) self.state_prev = self.state end -- Simple state transition to idle: if self.state == States.JUMP then if current_anim.GetTimer() > current_anim.GetEnd() then self.state = States.IDLE end else if self.velocity.Length() < 0.1 and self.state ~= States.SWIM_IDLE and self.state ~= States.SWIM and self.state ~= States.DANCE then self.state = States.IDLE end end end if dt > 0.2 then return -- avoid processing too large delta times to avoid instability end -- swim test: if self.neck ~= INVALID_ENTITY then local neck_pos = scene.Component_GetTransform(self.neck).GetPosition() local water_threshold = 0.1 neck_pos = vector.Add(neck_pos, Vector(0, -water_threshold, 0)) local swim_ray = Ray(neck_pos, Vector(0,1,0), 0, 100) local water_entity, water_pos, water_normal, water_distance = scene.Intersects(swim_ray, FILTER_WATER) if water_entity ~= INVALID_ENTITY then model_transform.Translate(Vector(0,water_distance - water_threshold,0)) model_transform.UpdateTransform() self.force.SetY(0) self.force = vector.Multiply(self.force, 0.8) -- water friction self.state = States.SWIM_IDLE end end if controllable then if not backlog_isactive() then -- Movement input: local lookDir = Vector() if(input.Down(KEYBOARD_BUTTON_LEFT) or input.Down(string.byte('A'))) then lookDir = lookDir:Add( Vector(-1) ) end if(input.Down(KEYBOARD_BUTTON_RIGHT) or input.Down(string.byte('D'))) then lookDir = lookDir:Add( Vector(1) ) end if(input.Down(KEYBOARD_BUTTON_UP) or input.Down(string.byte('W'))) then lookDir = lookDir:Add( Vector(0,0,1) ) end if(input.Down(KEYBOARD_BUTTON_DOWN) or input.Down(string.byte('S'))) then lookDir = lookDir:Add( Vector(0,0,-1) ) end local analog = input.GetAnalog(GAMEPAD_ANALOG_THUMBSTICK_L) lookDir = vector.Add(lookDir, Vector(analog.GetX(), 0, analog.GetY())) if self.state ~= States.JUMP and self.state_prev ~= States.JUMP and self.velocity.GetY() == 0 then if(lookDir.Length() > 0) then if self.state == States.SWIM_IDLE then self.state = States.SWIM self:MoveDirection(lookDir) else if(input.Down(KEYBOARD_BUTTON_LSHIFT) or input.Down(GAMEPAD_BUTTON_6)) then if input.Down(string.byte('E')) or input.Down(GAMEPAD_BUTTON_5) then self.state = States.RUN self:MoveDirection(lookDir) else self.state = States.JOG self:MoveDirection(lookDir) end else self.state = States.WALK self:MoveDirection(lookDir) end end end if(input.Press(string.byte('J')) or input.Press(KEYBOARD_BUTTON_SPACE) or input.Press(GAMEPAD_BUTTON_2)) then self:Jump(self.jump_speed) end elseif self.velocity.GetY() > 0 then self:MoveDirection(lookDir) end end else -- NPC patrol behavior: local patrol_count = len(self.patrol_waypoints) if patrol_count > 0 then local pos = savedPos pos.SetY(0) local patrol = self.patrol_waypoints[self.patrol_next % patrol_count + 1] local patrol_transform = scene.Component_GetTransform(patrol.entity) if patrol_transform ~= nil then local patrol_pos = patrol_transform.GetPosition() patrol_pos.SetY(0) local patrol_wait = patrol.wait or 0 -- default: 0 local patrol_vec = vector.Subtract(patrol_pos, pos) local distance = patrol_vec.Length() local patrol_dist = patrol.distance or 0.5 -- default : 0.5 local patrol_dist_threshold = patrol.distance_threshold or 0 -- default : 0 if distance < patrol_dist then -- reached patrol waypoint: self.patrol_reached = true if self.state ~= States.SWIM_IDLE then self.state = States.IDLE end if self.patrol_wait >= patrol_wait then self.patrol_next = self.patrol_next + 1 else self.patrol_wait = self.patrol_wait + dt end elseif self.patrol_reached then -- threshold to avoid jerking between moving and reached destination: -- Between distance and distance + threshold, the character will not advance towards waypoint -- Useful for dynamic waypoint, such as following behaviour if distance > patrol_dist + patrol_dist_threshold then self.patrol_reached = false end else -- move towards patrol waypoint: self.patrol_wait = 0 -- check if it's blocked by player collision: local capsule = scene.Component_GetCollider(self.collider).GetCapsule() local forward_offset = vector.Multiply(patrol_vec.Normalize(), 0.5) capsule.SetBase(vector.Add(capsule.GetBase(), forward_offset)) capsule.SetTip(vector.Add(capsule.GetTip(), forward_offset)) capsule.SetRadius(capsule.GetRadius() * 2) -- Manual check for blocked patrol: -- This is manually done because it is difficult to filter out current NPC among other NPCs -- Because NPCs can also be blocked by other NPCs but not themselves -- And introducing custom layer for every NPC is not very good local blocked = false for other_entity,other_capsule in pairs(character_capsules) do if other_entity ~= self.model then if capsule.Intersects(other_capsule) then blocked = true end end end if blocked then if debug then DrawDebugText("patrol blocked", vector.Add(capsule.GetTip(), Vector(0,0.1)), Vector(1,0,0,1), 0.08, DEBUG_TEXT_DEPTH_TEST | DEBUG_TEXT_CAMERA_FACING) DrawCapsule(capsule, Vector(1,0,0,1)) end else if self.state == States.SWIM_IDLE then self.state = States.SWIM else self.state = patrol.state or States.WALK -- default : WALK end self:MoveDirection(patrol_vec) if debug then DrawDebugText("patrol blocking check", vector.Add(capsule.GetTip(), Vector(0,0.1)), Vector(0,1,0,1), 0.08, DEBUG_TEXT_DEPTH_TEST | DEBUG_TEXT_CAMERA_FACING) DrawCapsule(capsule, Vector(0,1,0,1)) end end end end end end self.velocity = self.force -- Capsule collision for character: local capsule = scene.Component_GetCollider(self.collider).GetCapsule() local original_capsulepos = model_transform.GetPosition() local capsulepos = original_capsulepos local capsuleheight = vector.Subtract(capsule.GetTip(), capsule.GetBase()).Length() local radius = capsule.GetRadius() local collision_layer = ~self.layerMask if not controllable and not allow_pushaway_NPC then -- For NPC, this makes it not pushable away by player: -- This works by disabling NPC's collision to player -- But the player can still collide with NPC and can be blocked collision_layer = collision_layer & ~Layers.Player end local current_anim = scene.Component_GetAnimation(self.current_anim) local ground_intersect = false local platform_velocity_accumulation = Vector() local platform_velocity_count = 0 -- Perform fixed timestep logic: self.fixed_update_remain = self.fixed_update_remain + dt local fixed_update_fps = 120 local fixed_dt = 1.0 / fixed_update_fps while self.fixed_update_remain >= fixed_dt do self.fixed_update_remain = self.fixed_update_remain - fixed_dt capsulepos = vector.Add(capsulepos, vector.Multiply(self.velocity, fixed_dt)) capsule = Capsule(capsulepos, vector.Add(capsulepos, Vector(0, capsuleheight)), radius) local o2, p2, n2, depth, platform_velocity = scene.Intersects(capsule, FILTER_NAVIGATION_MESH | FILTER_COLLIDER, collision_layer) -- scene/capsule collision if(o2 ~= INVALID_ENTITY) then if debug then DrawPoint(p2,0.1,Vector(1,1,0,1)) DrawLine(p2, vector.Add(p2, n2), Vector(1,1,0,1)) end local ground_slope = vector.Dot(n2, Vector(0,1,0)) if ground_slope > slope_threshold then -- Ground intersection: ground_intersect = true capsulepos = vector.Add(capsulepos, Vector(0, depth, 0)) -- avoid sliding, instead stand upright platform_velocity_accumulation = vector.Add(platform_velocity_accumulation, platform_velocity) platform_velocity_count = platform_velocity_count + 1 self.velocity.SetY(0) -- remove falling motion else -- Slide on contact surface: capsulepos = vector.Add(capsulepos, vector.Multiply(n2, depth)) end end -- Some other things also updated at fixed rate: self.face = vector.Lerp(self.face, self.face_next, 0.1) -- smooth the turning in fixed update if self.force.Length() < 30 then self.force = vector.Add(self.force, Vector(0, gravity * fixed_dt, 0)) -- gravity end -- Animation blending if current_anim ~= nil then -- Blend in current animation: current_anim.SetAmount(math.lerp(current_anim.GetAmount(), 1, 0.1)) -- Ease out other animations: for i,anim in ipairs(self.all_anims) do if (anim ~= INVALID_ENTITY) and (anim ~= self.current_anim) then local prev_anim = scene.Component_GetAnimation(anim) prev_anim.SetAmount(math.lerp(prev_anim.GetAmount(), 0, 0.1)) if prev_anim.GetAmount() <= 0 then prev_anim.Stop() end end end end end if platform_velocity_count > 0 then capsulepos = vector.Add(capsulepos, vector.Multiply(platform_velocity_accumulation, 1.0 / platform_velocity_count)) -- apply moving platform velocity end model_transform.Translate(vector.Subtract(capsulepos, original_capsulepos)) -- transform by the capsule offset model_transform.UpdateTransform() self.face.SetY(0) self.face = self.face.Normalize() if ground_intersect then self.force = vector.Multiply(self.force, 0.85) -- ground friction end -- try to put water ripple: if self.velocity.Length() > 0.01 and self.state ~= States.SWIM_IDLE then local w,wp = scene.Intersects(capsule, FILTER_WATER) if w ~= INVALID_ENTITY then PutWaterRipple(script_dir() .. "assets/ripple.png", wp) end end character_capsules[self.model] = capsule self.position = model_transform.GetPosition() end, Update_IK = function(self) -- IK foot placement: local base_y = self.position.GetY() local ik_foot = INVALID_ENTITY local ik_pos = Vector() -- Compute root bone offset: -- I determine which foot wants to step on lower ground, that will offset root bone of skeleton downwards -- The other foot will be the upper foot which will be later attached an Inverse Kinematics (IK) effector if (self.state == States.IDLE or self.state == States.DANCE) and self.velocity.GetY() == 0 then local pos_left = scene.Component_GetTransform(self.left_foot).GetPosition() local pos_right = scene.Component_GetTransform(self.right_foot).GetPosition() local ray_left = Ray(vector.Add(pos_left, Vector(0, 1)), Vector(0, -1), 0, 1.8) local ray_right = Ray(vector.Add(pos_right, Vector(0, 1)), Vector(0, -1), 0, 1.8) -- Ray trace for both feet: local collision_layer = ~(Layers.Player | Layers.NPC) -- specifies that neither NPC, nor Player can collide with feet local collEntity_left,collPos_left = scene.Intersects(ray_left, FILTER_NAVIGATION_MESH | FILTER_COLLIDER, collision_layer) local collEntity_right,collPos_right = scene.Intersects(ray_right, FILTER_NAVIGATION_MESH | FILTER_COLLIDER, collision_layer) local diff_left = 0 local diff_right = 0 if collEntity_left ~= INVALID_ENTITY then --DrawAxis(collPos_left, 0.2) diff_left = collPos_left.GetY() - base_y end if collEntity_right ~= INVALID_ENTITY then --DrawAxis(collPos_right, 0.2) diff_right = collPos_right.GetY() - base_y end local diff = diff_left if collPos_left.GetY() > collPos_right.GetY() + 0.01 then diff = diff_right if collEntity_left ~= INVALID_ENTITY then ik_foot = self.left_foot ik_pos = collPos_left end else if collEntity_right ~= INVALID_ENTITY then ik_foot = self.right_foot ik_pos = collPos_right end end self.root_bone_offset = math.lerp(self.root_bone_offset, diff, 0.1) else self.root_bone_offset = math.lerp(self.root_bone_offset, 0, 0.1) end -- Offset root transform to lower foot pos: local root_bone_transform = scene.Component_GetTransform(self.root) root_bone_transform.ClearTransform() local root_pos = root_bone_transform.GetPosition() root_bone_transform.Translate(Vector(0, self.root_bone_offset)) --DrawDebugText(self.root_bone_offset, self.position, Vector(1,1,1,1), 0.1, DEBUG_TEXT_CAMERA_FACING) --DrawPoint(vector.Add(root_pos, Vector(0, self.root_bone_offset)), 0.1, Vector(1,0,0,1)) -- Remove IK effectors by default: if scene.Component_GetInverseKinematics(self.left_foot) ~= nil then scene.Component_RemoveInverseKinematics(self.left_foot) end if scene.Component_GetInverseKinematics(self.right_foot) ~= nil then scene.Component_RemoveInverseKinematics(self.right_foot) end -- The upper foot will use IK effector in IDLE state: if ik_foot ~= INVALID_ENTITY then if self.foot_placement == nil then self.foot_placement = CreateEntity() scene.Component_CreateTransform(self.foot_placement) end --DrawAxis(ik_pos, 0.2) local transform = scene.Component_GetTransform(self.foot_placement) transform.ClearTransform() transform.Translate(vector.Add(ik_pos, Vector(0, 0.15))) local ik = scene.Component_CreateInverseKinematics(ik_foot) ik.SetTarget(self.foot_placement) ik.SetChainLength(2) ik.SetIterationCount(10) end end, Update_Footprints = function(self) local radius = 0.1 -- Left footprint: local capsule = Capsule(scene.Component_GetTransform(self.left_foot).GetPosition(), scene.Component_GetTransform(self.left_toes).GetPosition(), 0.1) --DrawCapsule(capsule, Vector(1,0,0,1)) local collEntity,collPos = scene.Intersects(capsule, FILTER_NAVIGATION_MESH) if collEntity ~= INVALID_ENTITY then if not self.foot_placed_left then self.foot_placed_left = true local entity = CreateEntity() local transform = scene.Component_CreateTransform(entity) local material = scene.Component_CreateMaterial(entity) local decal = scene.Component_CreateDecal(entity) local layer = scene.Component_CreateLayer(entity) transform.MatrixTransform(matrix.LookTo(Vector(),self.face):Inverse()) transform.Rotate(Vector(math.pi * 0.5)) transform.Scale(0.1) transform.Translate(collPos) material.SetTexture(TextureSlot.BASECOLORMAP, footprint_texture) decal.SetSlopeBlendPower(2) layer.SetLayerMask(~(Layers.Player | Layers.NPC)) scene.Component_Attach(entity, collEntity) table.insert(footprints, entity) end else self.foot_placed_left = false end -- Right footprint: local capsule = Capsule(scene.Component_GetTransform(self.right_foot).GetPosition(), scene.Component_GetTransform(self.right_toes).GetPosition(), 0.1) --DrawCapsule(capsule, Vector(1,0,0,1)) local collEntity,collPos = scene.Intersects(capsule, FILTER_NAVIGATION_MESH) if collEntity ~= INVALID_ENTITY then if not self.foot_placed_right then self.foot_placed_right = true local entity = CreateEntity() local transform = scene.Component_CreateTransform(entity) local material = scene.Component_CreateMaterial(entity) local decal = scene.Component_CreateDecal(entity) local layer = scene.Component_CreateLayer(entity) transform.MatrixTransform(matrix.LookTo(Vector(),self.face):Inverse()) transform.Rotate(Vector(math.pi * 0.5)) transform.Scale(0.1) transform.Translate(collPos) material.SetTexture(TextureSlot.BASECOLORMAP, footprint_texture) material.SetTexMulAdd(Vector(-1, 1, 0, 0)) -- flip left footprint texture to be right decal.SetSlopeBlendPower(2) layer.SetLayerMask(~(Layers.Player | Layers.NPC)) scene.Component_Attach(entity, collEntity) table.insert(footprints, entity) end else self.foot_placed_right = false end end, } self:Create(model_name, start_position, face, controllable) return self end -- Interaction between two characters: local ResolveCharacters = function(characterA, characterB) local humanoidA = scene.Component_GetHumanoid(characterA.humanoid) local humanoidB = scene.Component_GetHumanoid(characterB.humanoid) if humanoidA ~= INVALID_ENTITY and humanoidB ~= INVALID_ENTITY then local headA = scene.Component_GetTransform(characterA.head).GetPosition() local headB = scene.Component_GetTransform(characterB.head).GetPosition() local distance = vector.Subtract(headA, headB).Length() if distance < 1.5 then humanoidA.SetLookAtEnabled(true) humanoidB.SetLookAtEnabled(true) humanoidA.SetLookAt(headB) humanoidB.SetLookAt(headA) if characterB.controllable then if input.Press(KEYBOARD_BUTTON_ENTER) then characterA.hired = characterB end if characterA.hired == nil then DrawDebugText("Hello there!\nPress ENTER to hire me!", vector.Add(headA, Vector(0,0.4)), Vector(1,0.2,1,1), 0.1, DEBUG_TEXT_DEPTH_TEST | DEBUG_TEXT_CAMERA_FACING) else DrawDebugText("Where are we going?", vector.Add(headA, Vector(0,0.4)), Vector(0.2,1,0.2,1), 0.1, DEBUG_TEXT_DEPTH_TEST | DEBUG_TEXT_CAMERA_FACING) end end end if characterA.hired ~= nil then characterA.patrol_waypoints = { { entity = characterA.hired.model, distance = 2, distance_threshold = 1 } } if distance < 5 then if characterA.hired.state == States.JOG or characterA.hired.state == States.RUN then characterA.patrol_waypoints[1].state = States.JOG else characterA.patrol_waypoints[1].state = States.WALK end elseif distance < 10 then characterA.patrol_waypoints[1].state = States.JOG else characterA.patrol_waypoints[1].state = States.RUN end end end end -- Third person camera controller class: local function ThirdPersonCamera(character) local self = { camera = INVALID_ENTITY, character = nil, side_offset = 0.2, rest_distance = 1, rest_distance_new = 1, min_distance = 0.5, zoom_speed = 0.3, target_rot_horizontal = 0, target_rot_vertical = 0, target_height = 1, Create = function(self, character) self.character = character self.camera = CreateEntity() local camera_transform = scene.Component_CreateTransform(self.camera) end, Update = function(self) if self.character == nil then return end -- Mouse scroll or gamepad triggers will move the camera distance: local scroll = input.GetPointer().GetZ() -- pointer.z is the mouse wheel delta this frame scroll = scroll + input.GetAnalog(GAMEPAD_ANALOG_TRIGGER_R).GetX() scroll = scroll - input.GetAnalog(GAMEPAD_ANALOG_TRIGGER_L).GetX() scroll = scroll * self.zoom_speed self.rest_distance_new = math.max(self.rest_distance_new - scroll, self.min_distance) -- do not allow too close using max self.rest_distance = math.lerp(self.rest_distance, self.rest_distance_new, 0.1) -- lerp will smooth out the zooming -- This will allow some smoothing for certain movements of camera target: local character_transform = scene.Component_GetTransform(self.character.model) local character_position = character_transform.GetPosition() self.target_rot_horizontal = math.lerp(self.target_rot_horizontal, self.character.target_rot_horizontal, 0.1) self.target_rot_vertical = math.lerp(self.target_rot_vertical, self.character.target_rot_vertical, 0.1) self.target_height = math.lerp(self.target_height, character_position.GetY() + self.character.target_height, 0.05) local camera_transform = scene.Component_GetTransform(self.camera) local target_transform = TransformComponent() target_transform.Translate(Vector(character_position.GetX(), 0, character_position.GetZ())) target_transform.Translate(Vector(0, self.target_height)) target_transform.Rotate(Vector(self.target_rot_vertical, self.target_rot_horizontal)) target_transform.UpdateTransform() -- First calculate the rest orientation (transform) of the camera: local mat = matrix.Translation(Vector(self.side_offset, 0, -self.rest_distance)) mat = matrix.Multiply(mat, target_transform.GetMatrix()) camera_transform.ClearTransform() camera_transform.MatrixTransform(mat) camera_transform.UpdateTransform() -- Camera collision: -- Compute the relation vectors between camera and target: local camPos = camera_transform.GetPosition() local targetPos = target_transform.GetPosition() local camDistance = vector.Subtract(camPos, targetPos).Length() -- These will store the closest collision distance and required camera position: local bestDistance = camDistance local bestPos = camPos local camera = GetCamera() -- Update global camera matrices for rest position: camera.TransformCamera(camera_transform) camera.UpdateCamera() -- Cast rays from target to clip space points on the camera near plane to avoid clipping through objects: local unproj = camera.GetInvViewProjection() -- camera matrix used to unproject from clip space to world space local clip_coords = { Vector(0,0,1,1), -- center Vector(-1,-1,1,1), -- bottom left Vector(-1,1,1,1), -- top left Vector(1,-1,1,1), -- bottom right Vector(1,1,1,1), -- top right } for i,coord in ipairs(clip_coords) do local corner = vector.TransformCoord(coord, unproj) local target_to_corner = vector.Subtract(corner, targetPos) local corner_to_campos = vector.Subtract(camPos, corner) local TMin = 0 local TMax = target_to_corner.Length() -- optimization: limit the ray tracing distance local ray = Ray(targetPos, target_to_corner.Normalize(), TMin, TMax) local collision_layer = ~(Layers.Player | Layers.NPC) -- specifies that neither NPC, nor Player can collide with camera local collObj,collPos,collNor = scene.Intersects(ray, FILTER_NAVIGATION_MESH | FILTER_COLLIDER, collision_layer) if(collObj ~= INVALID_ENTITY) then -- It hit something, see if it is between the player and camera: local collDiff = vector.Subtract(collPos, targetPos) local collDist = collDiff.Length() if(collDist > 0 and collDist < bestDistance) then bestDistance = collDist bestPos = vector.Add(collPos, corner_to_campos) --DrawPoint(collPos, 0.1, Vector(1,0,0,1)) end end end -- We have the best candidate for new camera position now, so offset the camera with the delta between the old and new camera position: local collision_offset = vector.Subtract(bestPos, camPos) mat = matrix.Multiply(mat, matrix.Translation(collision_offset)) camera_transform.ClearTransform() camera_transform.MatrixTransform(mat) camera_transform.UpdateTransform() --DrawPoint(bestPos, 0.1, Vector(1,1,0,1)) -- Feed back camera after collision: camera.TransformCamera(camera_transform) camera.UpdateCamera() camera.ApertureSize = self.character.happy camera.FocalLength = vector.Subtract(scene.Component_GetTransform(self.character.head).GetPosition(), camera.GetPosition()).Length() end, } self:Create(character) return self end ClearWorld() LoadModel(script_dir() .. "assets/level.wiscene") --LoadModel(script_dir() .. "assets/terrain.wiscene") --LoadModel(script_dir() .. "assets/waypoints.wiscene", matrix.Translation(Vector(1,0,2))) --dofile(script_dir() .. "../dungeon_generator/dungeon_generator.lua") LoadAnimations(script_dir() .. "assets/animations.wiscene") local player = Character(script_dir() .. "assets/character.wiscene", Vector(0,0.5,0), Vector(0,0,1), true) local npcs = { -- Patrolling NPC IDs: 1,2,3 Character(script_dir() .. "assets/character.wiscene", Vector(4,0.1,4), Vector(0,0,-1), false), Character(script_dir() .. "assets/character.wiscene", Vector(-8,1,4), Vector(-1,0,0), false), Character(script_dir() .. "assets/character.wiscene", Vector(-2,0.1,8), Vector(-1,0,0), false), -- stationary NPC IDs: 3,4.... Character(script_dir() .. "assets/character.wiscene", Vector(-1,0.1,-6), Vector(0,0,1), false), --Character(script_dir() .. "assets/character.wiscene", Vector(10.8,0.1,4.1), Vector(0,0,-1), false), --Character(script_dir() .. "assets/character.wiscene", Vector(11.1,4,7.2), Vector(-1,0,0), false), } local camera = ThirdPersonCamera(player) -- Main loop: runProcess(function() -- We will override the render path so we can invoke the script from Editor and controls won't collide with editor scripts -- Also save the active component that we can restore when ESCAPE is pressed local prevPath = application.GetActivePath() local path = RenderPath3D() --path.SetLightShaftsEnabled(true) path.SetLightShaftsStrength(0.01) path.SetAO(AO_MSAO) path.SetAOPower(0.25) --path.SetOutlineEnabled(true) path.SetOutlineThreshold(0.11) path.SetOutlineThickness(1.7) path.SetOutlineColor(0,0,0,0.6) path.SetBloomThreshold(5) application.SetActivePath(path) --application.SetInfoDisplay(false) application.SetFPSDisplay(true) --path.SetResolutionScale(0.5) --path.SetFSR2Enabled(true) --path.SetFSR2Preset(FSR2_Preset.Performance) --SetProfilerEnabled(true) while true do player:Update() for k,npc in pairs(npcs) do npc:Update() ResolveCharacters(npc, player) end -- Hierarchy after character positioning is updated, this is needed to place camera and IK afterwards to most up to date locations -- But we do it once, not every character! scene.UpdateHierarchy() -- Note: if I don't do this, you get foot positions after IK, but we want foot positions after animation, to start from "fresh" player:Update_IK() for k,npc in pairs(npcs) do npc:Update_IK() end if enable_footprints then player:Update_Footprints() for k,npc in pairs(npcs) do npc:Update_Footprints() end -- Cap the max number of decals: while #footprints > 100 do local entity = footprints[1] scene.Entity_Remove(entity) table.remove(footprints, 1) end end camera:Update() update() if not backlog_isactive() then if(input.Press(KEYBOARD_BUTTON_ESCAPE)) then -- restore previous component -- so if you loaded this script from the editor, you can go back to the editor with ESC backlog_post("EXIT") killProcesses() application.SetActivePath(prevPath) return end if(input.Press(string.byte('R'))) then -- reload script backlog_post("RELOAD") killProcesses() application.SetActivePath(prevPath) dofile(script_file()) return end if input.Press(string.byte('L')) then framerate_lock = not framerate_lock application.SetFrameRateLock(framerate_lock) if framerate_lock then application.SetTargetFrameRate(framerate_lock_target) end end end end end) -- Draw runProcess(function() local help_text = "Wicked Engine Character demo (LUA)\n\n" help_text = help_text .. "Controls:\n" help_text = help_text .. "#############\n" help_text = help_text .. "WASD/arrows/left analog stick: walk\n" help_text = help_text .. "SHIFT/right shoulder button: walk -> jog\n" help_text = help_text .. "E/left shoulder button: jog -> run\n" help_text = help_text .. "SPACE/gamepad X/gamepad button 2: Jump\n" help_text = help_text .. "Right Mouse Button/Right thumbstick: rotate camera\n" help_text = help_text .. "Scoll middle mouse/Left-Right triggers: adjust camera distance\n" help_text = help_text .. "ESCAPE key: quit\n" help_text = help_text .. "R: reload script\n" help_text = help_text .. "H: toggle debug draw\n" help_text = help_text .. "L: toggle framerate lock\n" while true do -- Do some debug draw geometry: DrawDebugText(help_text, Vector(0,2,2), Vector(1,1,1,1), 0.1, DEBUG_TEXT_DEPTH_TEST) if input.Press(string.byte('H')) then debug = not debug end if debug and backlog_isactive() == false then local model_transform = scene.Component_GetTransform(player.model) local target_transform = scene.Component_GetTransform(player.neck) --velocity DrawLine(target_transform.GetPosition(),target_transform.GetPosition():Add(player.velocity)) --face DrawLine(target_transform.GetPosition(),target_transform.GetPosition():Add(player.face:Normalize()),Vector(0,1,0,1)) --intersection --DrawAxis(player.p,0.5) -- camera target box and axis --DrawBox(target_transform.GetMatrix()) -- Neck bone DrawPoint(scene.Component_GetTransform(player.neck).GetPosition(),0.05, Vector(0,1,1,1)) -- Head bone DrawPoint(scene.Component_GetTransform(player.head).GetPosition(),0.05, Vector(0,1,1,1)) -- Left hand bone DrawPoint(scene.Component_GetTransform(player.left_hand).GetPosition(),0.05, Vector(0,1,1,1)) -- Right hand bone DrawPoint(scene.Component_GetTransform(player.right_hand).GetPosition(),0.05, Vector(0,1,1,1)) -- Left foot bone DrawPoint(scene.Component_GetTransform(player.left_foot).GetPosition(),0.05, Vector(0,1,1,1)) -- Right foot bone DrawPoint(scene.Component_GetTransform(player.right_foot).GetPosition(),0.05, Vector(0,1,1,1)) local capsule = scene.Component_GetCollider(player.collider).GetCapsule() DrawCapsule(capsule) local str = "State: " .. player.state .. "\n" --str = str .. "Velocity = " .. player.velocity.GetX() .. ", " .. player.velocity.GetY() .. "," .. player.velocity.GetZ() .. "\n" --str = str .. "Force = " .. player.force.GetX() .. ", " .. player.force.GetY() .. "," .. player.force.GetZ() .. "\n" DrawDebugText(str, vector.Add(capsule.GetBase(), Vector(0,0.4)), Vector(1,1,1,1), 1, DEBUG_TEXT_CAMERA_FACING | DEBUG_TEXT_CAMERA_SCALING) end -- Wait for the engine to render the scene render() end end) -- Patrol waypoints: local waypoints = { scene.Entity_FindByName("waypoint1"), scene.Entity_FindByName("waypoint2"), scene.Entity_FindByName("waypoint3"), scene.Entity_FindByName("waypoint4"), scene.Entity_FindByName("waypoint5"), scene.Entity_FindByName("waypoint6"), scene.Entity_FindByName("waypoint7"), scene.Entity_FindByName("waypoint8"), scene.Entity_FindByName("waypoint9"), scene.Entity_FindByName("waypoint10"), scene.Entity_FindByName("waypoint11"), scene.Entity_FindByName("waypoint12"), scene.Entity_FindByName("waypoint13"), scene.Entity_FindByName("waypoint14"), scene.Entity_FindByName("waypoint15"), scene.Entity_FindByName("waypoint16"), scene.Entity_FindByName("waypoint17"), scene.Entity_FindByName("waypoint18"), scene.Entity_FindByName("waypoint19"), scene.Entity_FindByName("waypoint20"), scene.Entity_FindByName("waypoint21"), scene.Entity_FindByName("waypoint22"), scene.Entity_FindByName("waypoint23"), scene.Entity_FindByName("waypoint24"), scene.Entity_FindByName("waypoint25"), } -- Simplest 1-2 patrol: if( waypoints[1] ~= INVALID_ENTITY and waypoints[2] ~= INVALID_ENTITY ) then npcs[1].patrol_waypoints = { { entity = waypoints[1], wait = 0, }, { entity = waypoints[2], wait = 2, }, } end -- Some more advanced, toggle between walk and jog, also swimming (because waypoints are across water mesh in test level): if( waypoints[3] ~= INVALID_ENTITY and waypoints[4] ~= INVALID_ENTITY and waypoints[5] ~= INVALID_ENTITY and waypoints[6] ~= INVALID_ENTITY ) then npcs[2].patrol_waypoints = { { entity = waypoints[3], wait = 0, state = States.JOG, }, { entity = waypoints[4], wait = 0, }, { entity = waypoints[5], wait = 2, }, { entity = waypoints[6], wait = 0, state = States.JOG, }, } end -- Run long circle: if( waypoints[7] ~= INVALID_ENTITY and waypoints[8] ~= INVALID_ENTITY and waypoints[9] ~= INVALID_ENTITY and waypoints[10] ~= INVALID_ENTITY and waypoints[11] ~= INVALID_ENTITY and waypoints[12] ~= INVALID_ENTITY and waypoints[13] ~= INVALID_ENTITY and waypoints[14] ~= INVALID_ENTITY and waypoints[15] ~= INVALID_ENTITY and waypoints[16] ~= INVALID_ENTITY and waypoints[17] ~= INVALID_ENTITY and waypoints[18] ~= INVALID_ENTITY and waypoints[19] ~= INVALID_ENTITY and waypoints[20] ~= INVALID_ENTITY and waypoints[21] ~= INVALID_ENTITY and waypoints[22] ~= INVALID_ENTITY and waypoints[23] ~= INVALID_ENTITY and waypoints[24] ~= INVALID_ENTITY and waypoints[25] ~= INVALID_ENTITY ) then npcs[3].patrol_waypoints = { { entity = waypoints[7], state = States.JOG, }, { entity = waypoints[8], state = States.JOG, }, { entity = waypoints[9], state = States.JOG, }, { entity = waypoints[10], state = States.JOG, }, { entity = waypoints[11], state = States.JOG, }, { entity = waypoints[12], state = States.JOG, }, { entity = waypoints[13], state = States.JOG, }, { entity = waypoints[14], state = States.JOG, }, { entity = waypoints[15], state = States.JOG, }, { entity = waypoints[16], state = States.JOG, }, { entity = waypoints[17], state = States.JOG, }, { entity = waypoints[18], state = States.JOG, wait = 2, -- little wait at top of slope }, { entity = waypoints[19], state = States.JOG, }, { entity = waypoints[20], state = States.JOG, }, { entity = waypoints[21], state = States.JOG, }, { entity = waypoints[22], state = States.JOG, }, { entity = waypoints[23], state = States.JOG, }, { entity = waypoints[24], state = States.JOG, }, { entity = waypoints[25], state = States.JOG, }, } end