Files
WickedEngine/Content/scripts/character_controller/character_controller.lua
T
2025-06-29 12:26:32 +02:00

1034 lines
40 KiB
Lua

-- Lua Third person camera and character controller script
-- This script will load a simple scene with a character that can be controlled
--
-- Tips:
-- - Character models that you use should have a humanoid rig (this is created automatically from VRM, Makehuman and Mixamo model imports)
-- - The level objects should be tagged as "Navmesh" because the characters will collide with only those, and these are optimized for intersections
-- - To set the player start position, you can put a metadata component to the level scene and set it to "Player" preset
-- - To add NPCs you can put a metadata component to the level scene and set it to "NPC" preset
-- - To specify which model a character in the level (Player or NPC) uses, add a string property to its metadata named "name" and its value is the name of the character, for example name = johnny will use assets/johnny.wiscene
-- - To specify which animation set a character should use, add "animset" string property to metadata, the value should be the name of the animset which will be concatenated to anim names with "_". For example: animset = male will load animations like "walk_male", etc. if available, else fall back to "walk"
-- - To set patrol route for an NPC character, add a "waypoint" named string property to its metadata and the value is a name of the target waypoint entity. Waypoints can be chained by having a metadata for them and having "waypoint" string properties on each.
-- - To set conversation dialog for an NPC character, add a "dialog" named string property to its metadata and the value is a name of the lua file in assets/dialogs/. For example dialog = hello will make the character use the assets/dialogs/hello.lua dialog script
--
-- CONTROLS:
-- WASD/left thumbstick: walk/jog
-- Left Control: slow walk
-- Left Shift/left shoulder button: jog -> run
-- SPACE/gamepad X/gamepad button 3: 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
-- R: reload script
-- ENTER: interaction
local debug = false -- press H to toggle
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 dynamic_voxelization = false -- Set to true to revoxelize navigation every frame
dofile(script_dir() .. "/assets/conversation.lua") -- load conversation system logic from separate file
conversation = Conversation() -- conversation will be accessible from dialog scripts
local scene = GetScene()
local Layers = {
Player = 1 << 0,
NPC = 1 << 1,
}
States = {
IDLE = "idle",
WALK = "walk",
JOG = "jog",
RUN = "run",
JUMP = "jump",
SWIM_IDLE = "swim_idle",
SWIM = "swim",
DANCE = "dance",
WAVE = "wave"
}
Mood = {
Neutral = ExpressionPreset.Neutral,
Happy = ExpressionPreset.Happy,
Angry = ExpressionPreset.Angry,
Sad = ExpressionPreset.Sad,
Relaxed = ExpressionPreset.Relaxed,
Surprised = ExpressionPreset.Surprised
}
player = nil
npcs = {}
local enable_footprints = true
local footprint_texture = Texture(script_dir() .. "assets/footprint.dds")
local footprints = {}
local character_capsules = {}
local voxelgrid = VoxelGrid(128,32,128)
voxelgrid.SetVoxelSize(0.25)
voxelgrid.SetCenter(Vector(0,0.1,0))
local function Character(model_scene, start_transform, controllable, anim_scene, animset)
local self = {
model = INVALID_ENTITY,
target_rot_horizontal = 0,
target_rot_vertical = 0,
target_height = 0,
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,
savedPointerPos = Vector(),
walk_speed = 0.1,
jog_speed = 0.2,
run_speed = 0.4,
jump_speed = 8,
swim_speed = 0.2,
layerMask = ~0, -- layerMask will be used to filter collisions
scale = Vector(1, 1, 1),
rotation = Vector(0,math.pi,0),
position = Vector(),
ground_intersect = false,
controllable = true,
foot_placed_left = false,
foot_placed_right = false,
mood = Mood.Neutral,
mood_amount = 1,
expression = INVALID_ENTITY,
object_entities = {},
patrol_waypoints = {},
patrol_waypoints_original = {},
patrol_next = 0,
patrol_wait = 0,
patrol_reached = false,
hired = nil,
dialogs = {},
next_dialog = 1,
GetFacing = function(self)
local charactercomponent = scene.Component_GetCharacter(self.model)
return charactercomponent.GetFacingSmoothed()
end,
Jump = function(self,f)
local charactercomponent = scene.Component_GetCharacter(self.model)
charactercomponent.Jump(f)
self.state = States.JUMP
charactercomponent.StopAnimation() -- fix for jumping when jump anim didn't finish yet, instead I force prev anim to finish
end,
Turn = function(self,dir)
local charactercomponent = scene.Component_GetCharacter(self.model)
charactercomponent.Turn(dir)
end,
MoveDirection = function(self,dir)
local charactercomponent = scene.Component_GetCharacter(self.model)
local rotation_matrix = matrix.Multiply(matrix.RotationY(self.target_rot_horizontal), matrix.RotationX(self.target_rot_vertical))
dir = vector.TransformNormal(dir.Normalize(), rotation_matrix)
dir.SetY(0)
charactercomponent.Turn(dir.Normalize())
local speed = self.walk_speed
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
charactercomponent.Move(self:GetFacing():Multiply(Vector(speed,speed,speed)))
end,
SetAnimationAmount = function(self,amount)
local charactercomponent = scene.Component_GetCharacter(self.model)
charactercomponent.SetAnimationAmount(amount)
end,
Update = function(self)
local dt = getDeltaTime()
local charactercomponent = scene.Component_GetCharacter(self.model)
self.ground_intersect = charactercomponent.IsGrounded()
self.position = charactercomponent.GetPositionInterpolated()
local capsule = charactercomponent.GetCapsule()
character_capsules[self.model] = capsule
--DrawCapsule(capsule)
local humanoid = scene.Component_GetHumanoid(self.humanoid)
humanoid.SetLookAtEnabled(false)
if self.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(0.016 * 0.1) -- not using delta time for delta mouse because it's not a diff from prev frame but hw delta movement reading!
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
end
-- Blend to current mood, blend out other moods:
local expression = scene.Component_GetExpression(self.expression)
if expression ~= nil then
for i,preset in pairs(Mood) do
local weight = expression.GetPresetWeight(preset)
if preset == self.mood then
weight = math.lerp(weight, self.mood_amount, 0.1)
else
weight = math.lerp(weight, 0, 0.1)
end
expression.SetPresetWeight(preset, weight)
end
end
-- state and animation update
charactercomponent.PlayAnimation(self.anims[self.state])
if self.state == States.JUMP then
if charactercomponent.IsAnimationEnded() then
self.state = States.IDLE
end
else
if self.ground_intersect and self.state ~= States.DANCE and self.state ~= States.WAVE then
self.state = States.IDLE
end
end
if charactercomponent.IsSwimming() then
self.state = States.SWIM_IDLE
end
if self.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()))
-- Apply movement:
if(lookDir.Length() > 0) then
if self.state == States.SWIM_IDLE then
self.state = States.SWIM
self:MoveDirection(lookDir)
elseif self.ground_intersect or charactercomponent.IsWallIntersect() then
if self.ground_intersect then
local analog_len = vector.Length(analog)
if input.Down(KEYBOARD_BUTTON_LCONTROL) or (analog_len > 0 and analog_len < 0.75) then
self.state = States.WALK
else
if input.Down(KEYBOARD_BUTTON_LSHIFT) or input.Down(GAMEPAD_BUTTON_5) then
self.state = States.RUN
else
self.state = States.JOG
end
end
end
self:MoveDirection(lookDir)
end
end
if (input.Press(string.byte('J')) or input.Press(KEYBOARD_BUTTON_SPACE) or input.Press(GAMEPAD_BUTTON_3)) and self.ground_intersect then
self:Jump(self.jump_speed)
end
end
elseif not(conversation.override_input and conversation.character == self) then
-- NPC patrol behavior:
local patrol_count = len(self.patrol_waypoints)
if patrol_count > 0 then
local pos = self.position
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()
local patrol_wait = patrol.wait or 0 -- default: 0
local patrol_vec = vector.Subtract(patrol_pos, pos)
patrol_vec.SetY(0)
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:
charactercomponent.SetPathGoal(patrol_pos, voxelgrid) -- this is deferred into scene update
local pathquery = charactercomponent.GetPathQuery()
pathquery.SetAgentHeight(3)
if pathquery.IsSuccessful() then
-- If there is a valid pathfinding result for waypoint, replace heading direction by that:
patrol_vec = vector.Subtract(pathquery.GetNextWaypoint(), pos)
patrol_vec.SetY(0)
end
if debug then
DrawPathQuery(pathquery)
end
self.patrol_wait = 0
-- check if it's blocked by player collision:
local capsule = charactercomponent.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
-- If camera is inside character capsule, fade out the character, otherwise fade in:
if capsule.Intersects(GetCamera().GetPosition()) then
for i,entity in ipairs(self.object_entities) do
local object = scene.Component_GetObject(entity)
local color = object.GetColor()
local opacity = color.GetW()
opacity = math.lerp(opacity, 0, 0.1)
color.SetW(opacity)
object.SetColor(color)
end
else
for i,entity in ipairs(self.object_entities) do
local object = scene.Component_GetObject(entity)
local color = object.GetColor()
local opacity = color.GetW()
opacity = math.lerp(opacity, 1, 0.1)
color.SetW(opacity)
object.SetColor(color)
end
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:GetFacing()):Inverse())
transform.Rotate(Vector(math.pi * 0.5))
transform.Scale(0.1)
transform.Translate(collPos)
material.SetTexture(TextureSlot.BASECOLORMAP, footprint_texture)
material.SetBaseColor(Vector(0.1,0.1,0.1,1))
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:GetFacing()):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
material.SetBaseColor(Vector(0.1,0.1,0.1,1))
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,
SetPatrol = function(self, patrol)
self.patrol_waypoints_original = patrol
self.patrol_waypoints = patrol
end,
ReturnToPatrol = function(self) -- return to original patrol route
self.patrol_waypoints = self.patrol_waypoints_original
end,
Follow = function(self, leader)
self.hired = leader
end,
Unfollow = function(self)
self.hired = nil
self.patrol_waypoints = {}
end,
}
-- Character initialization:
self.position = start_transform.GetPosition()
local facing = vector.Rotate(start_transform.GetForward(), vector.QuaternionFromRollPitchYaw(self.rotation))
self.controllable = controllable
if controllable then
self.layerMask = Layers.Player
self.target_rot_horizontal = vector.GetAngle(Vector(0,0,1), facing, Vector(0,1,0)) -- only modify camera rot for player
else
self.layerMask = Layers.NPC
end
-- Note: we instantiate the model_scene into the main scene, start_transform stores a component pointer which gets invalidated after this!!
self.model = scene.Instantiate(model_scene, true)
local charactercomponent = scene.Component_CreateCharacter(self.model) -- some character features will be handled by the built-in CharacterComponent in Wicked Engine
charactercomponent.SetPosition(self.position)
charactercomponent.SetFacing(facing)
charactercomponent.SetWidth(0.3)
charactercomponent.SetHeight(1.8)
if controllable then
charactercomponent.SetTurningSpeed(9)
else
charactercomponent.SetTurningSpeed(7) -- Set NPC to turn slower because when we approach it for conversation, fast turning looks bad
end
--charactercomponent.SetFootPlacementEnabled(false)
local layer = scene.Component_GetLayer(self.model)
layer.SetLayerMask(self.layerMask)
self.state = States.IDLE
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)
break
end
end
for i,entity in ipairs(scene.Entity_GetExpressionArray()) do
if scene.Entity_IsDescendant(entity, self.model) then
self.expression = entity
-- Set up some overrides to avoid bad looking expression combinations:
local expression = scene.Component_GetExpression(self.expression)
expression.SetPresetOverrideBlink(ExpressionPreset.Happy, ExpressionOverride.Block)
expression.SetPresetOverrideMouth(ExpressionPreset.Happy, ExpressionOverride.Blend)
expression.SetPresetOverrideBlink(ExpressionPreset.Surprised, ExpressionOverride.Block)
end
end
-- Enable wetmap for all objects of character, so it can get wet in the ocean (if the weather has it):
for i,entity in ipairs(scene.Entity_GetObjectArray()) do
if scene.Entity_IsDescendant(entity, self.model) then
local object = scene.Component_GetObject(entity)
object.SetWetmapEnabled(true)
table.insert(self.object_entities, entity) -- save object array for later use
end
end
self.root = self.humanoid
scene.ResetPose(self.model)
-- The base animation entities are queried from the anim_scene, they are not yet tergeting our character:
self.anims[States.IDLE] = anim_scene.Entity_FindByName("idle")
self.anims[States.WALK] = anim_scene.Entity_FindByName("walk")
self.anims[States.JOG] = anim_scene.Entity_FindByName("jog")
self.anims[States.RUN] = anim_scene.Entity_FindByName("run")
self.anims[States.JUMP] = anim_scene.Entity_FindByName("jump")
self.anims[States.SWIM_IDLE] = anim_scene.Entity_FindByName("swim_idle")
self.anims[States.SWIM] = anim_scene.Entity_FindByName("swim")
self.anims[States.DANCE] = anim_scene.Entity_FindByName("dance")
self.anims[States.WAVE] = anim_scene.Entity_FindByName("wave")
-- Retarget animation entities onto the character, and add them to the CharacterComponent:
-- Also see if a requested animation set (animset) is available for each animation, and use that instead if yes
animset = "_" .. animset
for state,anim in pairs(self.anims) do
local anim_name_component = anim_scene.Component_GetName(anim)
if anim_name_component ~= nil then
local anim_set_name = anim_name_component.GetName() .. animset
local anim_set_entity = anim_scene.Entity_FindByName(anim_set_name)
if anim_set_entity ~= INVALID_ENTITY then
anim = anim_set_entity -- if there is animation with requested animset, replace default anim to that
end
self.anims[state] = scene.RetargetAnimation(self.humanoid, anim, false, anim_scene)
charactercomponent.AddAnimation(self.anims[state]) --
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.position)
model_transform.UpdateTransform()
self.target_height = scene.Component_GetTransform(self.neck).GetPosition().GetY()
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)
local facing_amount = vector.Dot(characterB:GetFacing(), vector.Subtract(characterA.position, characterB.position).Normalize())
if #characterA.dialogs > 0 and conversation.state == ConversationState.Disabled and facing_amount > 0.8 then
if input.Press(KEYBOARD_BUTTON_ENTER) or input.Press(GAMEPAD_BUTTON_2) then
characterA:Turn(vector.Subtract(headB, headA):Normalize())
conversation:Enter(characterA)
application.CrossFade(0.5)
end
DrawDebugText("", vector.Add(headA, Vector(0,0.4)), Vector(1,1,1,1), 0.1, DEBUG_TEXT_DEPTH_TEST | DEBUG_TEXT_CAMERA_FACING)
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 charactercomponent = scene.Component_GetCharacter(self.character.model)
local character_position = charactercomponent.GetPositionInterpolated()
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 + charactercomponent.GetFootOffset(), 0.1)
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()
end,
}
self:Create(character)
return self
end
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()
local loadingscreen = LoadingScreen()
--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)
SetCapsuleShadowEnabled(true)
SetCapsuleShadowFade(0.4)
SetCapsuleShadowAngle(math.pi * 0.15)
--application.SetInfoDisplay(false)
--application.SetFPSDisplay(true)
--path.SetResolutionScale(0.75)
--path.SetFSR2Enabled(true)
--path.SetFSR2Preset(FSR2_Preset.Performance)
--SetProfilerEnabled(true)
SetDebugEnvProbesEnabled(false)
SetGridHelperEnabled(false)
SetDebugCamerasEnabled(false)
-- Configure a simple loading progress bar:
local loadingbar = Sprite()
loadingbar.SetMaskTexture(texturehelper.CreateGradientTexture(
GradientType.Linear,
2048, 1,
Vector(0, 0), Vector(1, 0),
GradientFlags.Inverse,
"111r"
))
local loadingbarparams = loadingbar.GetParams()
loadingbarparams.SetColor(Vector(1,0.2,0.2,1))
loadingbarparams.SetBlendMode(BLENDMODE_ALPHA)
loadingscreen.AddSprite(loadingbar)
loadingscreen.SetBackgroundTexture(Texture(script_dir() .. "assets/loadingscreen.png"))
-- All LoadModel tasks are started by the LoadingScreen asynchronously, but we get back root Entity handles immediately:
local anim_scene = Scene()
loadingscreen.AddLoadModelTask(anim_scene, script_dir() .. "assets/animations.wiscene")
local loading_scene = Scene()
loadingscreen.AddLoadModelTask(loading_scene, script_dir() .. "assets/level.wiscene")
loadingscreen.AddRenderPathActivationTask(path, application, 0.5, 0, 0, 0, FadeType.FadeToColor)
application.SetActivePath(loadingscreen, 0.5, 0, 0, 0, FadeType.CrossFade) -- activate and switch to loading screen
-- Because we are in a runProcess, we can block loading screen like this while application is still running normally:
-- Meanwhile, we can update the progress bar sprite
while not loadingscreen.IsFinished() do
update() -- gives back control for application for one frame, script waits until next update() phase
local canvas = application.GetCanvas()
loadingbarparams.SetPos(Vector(50, canvas.GetLogicalHeight() * 0.8))
loadingbarparams.SetSize(Vector(canvas.GetLogicalWidth() - 100, 20))
local progress = 1 - loadingscreen.GetProgress() / 100.0
loadingbarparams.SetMaskAlphaRange(math.saturate(progress - 0.05), math.saturate(progress))
loadingbar.SetParams(loadingbarparams)
end
-- After loading finished, we clear the main scene, and merge loaded scene into it:
scene.Clear()
scene.Merge(loading_scene)
scene.Update(0)
if len(scene.Component_GetVoxelGridArray()) > 0 then
voxelgrid = scene.Component_GetVoxelGridArray()[1] -- take existing voxel grid from scene if available
else
scene.VoxelizeScene(voxelgrid, false, FILTER_NAVIGATION_MESH | FILTER_COLLIDER, ~(Layers.Player | Layers.NPC)) -- generate a voxel grid in code, player and NPCs not included
end
-- Create characters from scene metadata components:
local character_scenes = {}
for i,entity in ipairs(scene.Entity_GetMetadataArray()) do
local metadata = scene.Component_GetMetadata(entity)
local transform = scene.Component_GetTransform(entity)
if metadata ~= nil and transform ~= nil then
-- Determine name of the placed character:
local name = "character" -- default name
if metadata.HasString("name") then
name = metadata.GetString("name")
end
-- Load character model if doesn't exist yet:
if character_scenes[name] == nil then
character_scenes[name] = Scene()
LoadModel(character_scenes[name], script_dir() .. "assets/" .. name .. ".wiscene")
end
if player == nil and metadata.GetPreset() == MetadataPreset.Player then
player = Character(character_scenes[name], transform, true, anim_scene, metadata.GetString("animset"))
end
if metadata.GetPreset() == MetadataPreset.NPC then
local npc = Character(character_scenes[name], transform, false, anim_scene, metadata.GetString("animset"))
-- add dialog tree if found:
if metadata.HasString("dialog") then
npc.dialogs = dofile(script_dir() .. "assets/dialogs/" .. metadata.GetString("dialog") .. ".lua")
end
-- Add patrol waypoints if found:
-- It will be looking for "waypoint" named string values in metadata components, and they can be chained by their value
local patrol_waypoints = {}
local visited = {} -- avoid infinite loop
while metadata ~= nil and metadata.HasString("waypoint") do
local waypoint_name = metadata.GetString("waypoint")
local waypoint_entity = scene.Entity_FindByName(waypoint_name)
if waypoint_entity ~= INVALID_ENTITY then
if visited[waypoint_entity] then
break
else
metadata = scene.Component_GetMetadata(waypoint_entity) -- chain waypoints
visited[waypoint_entity] = true
end
if metadata == nil then
break
end
local waypoint = {
entity = waypoint_entity,
wait = metadata.GetFloat("wait")
}
if metadata.HasString("state") then
waypoint.state = metadata.GetString("state")
end
table.insert(patrol_waypoints, waypoint) -- add waypoint to NPC
else
break
end
end
npc:SetPatrol(patrol_waypoints)
table.insert(npcs, npc) -- add NPC
end
end
end
-- if player was not created from a metadata component, create a default player:
if player == nil then
if character_scenes["character"] == nil then
character_scenes["character"] = Scene()
LoadModel(character_scenes["character"], script_dir() .. "assets/character.wiscene")
end
player = Character(character_scenes["character"], TransformComponent(), true, anim_scene, "")
end
local camera = ThirdPersonCamera(player)
path.AddFont(conversation.font)
path.AddFont(conversation.advance_font)
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: jog\n"
help_text = help_text .. "SHIFT/right shoulder button: jog -> run\n"
help_text = help_text .. "LEFT CONTROL: walk\n"
help_text = help_text .. "SPACE/gamepad X/gamepad button 3: 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 .. "ENTER key: interact\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"
-- Main loop:
while true do
update()
player:Update()
for k,npc in pairs(npcs) do
npc:Update()
ResolveCharacters(npc, player)
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
conversation:Update(path, scene, player)
player.controllable = not conversation.override_input
if dynamic_voxelization then
voxelgrid.ClearData()
scene.VoxelizeScene(voxelgrid, false, FILTER_NAVIGATION_MESH | FILTER_COLLIDER, ~(Layers.Player | Layers.NPC)) -- player and npc layers not included in voxelization
end
render() -- Camera update below will be happening after render phase is signaled, after update phase ended (this helps camera jitter as it will use the latest scene.Update() transforms doen by engine system)
if not conversation.override_input then
camera:Update()
end
-- 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)
-- 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_GetCharacter(player.model).GetCapsule()
DrawCapsule(capsule)
local str = "State: " .. player.state .. "\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)
DrawVoxelGrid(voxelgrid)
end
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")
for i,anim in ipairs(scene.Component_GetAnimationArray()) do
anim.Stop() -- stop animations because some of them are retargeted and animation source scene will be lost after we exit this script!
end
for i,character in ipairs(scene.Component_GetCharacterArray()) do
character.StopAnimation()
end
application.SetActivePath(prevPath, 0.5, 0, 0, 0, FadeType.CrossFade)
killProcesses()
return
end
if(input.Press(string.byte('R'))) then
-- reload script
backlog_post("RELOAD")
application.SetActivePath(prevPath, 0.5, 0, 0, 0, FadeType.CrossFade)
while not application.IsFaded() do
update()
end
killProcesses()
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)