diff --git a/Content/Documentation/ScriptingAPI-Documentation.md b/Content/Documentation/ScriptingAPI-Documentation.md index eab0305cd..10623e9e4 100644 --- a/Content/Documentation/ScriptingAPI-Documentation.md +++ b/Content/Documentation/ScriptingAPI-Documentation.md @@ -48,6 +48,7 @@ This is a reference and explanation of Lua scripting features in Wicked Engine. 19. [HumanoidComponent](#humanoidcomponent) 19. [DecalComponent](#decalcomponent) 19. [MetadataComponent](#metadatacomponent) + 19. [CharacterComponent](#charactercomponent) 10. [Canvas](#canvas) 11. [High Level Interface](#high-level-interface) 1. [Application](#application) @@ -1501,6 +1502,60 @@ The metadata component can store and retrieve an arbitrary amount of named user Pickup = 5, } +#### CharacterComponent +Implementation of basic character controller features such as movement in the scene, inverse kinematics for legs, swimming, water ripples, etc. + +- SetActive(bool value) -- Enable/disable character processing (enabled by default) +- IsActive() : bool -- Returns whether the character processing is active or not + +- Move(Vector value) -- Move the character in a direction continuously. The given vector doesn't need to be normalized, the length of it corresponds to the movement amount. The character will be moved the next time the scene is updated. The movement will be blocked by objects tagged as navigation mesh and CPU colliders. If this entity has a layer component, the layer will be used to ensure that the character doesn't collide with that layer. +- Strafe(Vector value) -- Similar to Move, but relative to the facing direction. +- Jump(float amount) -- Jump upwards by an amount. The jump will be executed in the next scene update, with collisions. +- Turn(Vector value) -- Turn towards a direction continuously. + +- AddAnimation(Entity entity) -- Adds animation for tracking blending state +- PlayAnimation(Entity entity) -- Play the animation. This will be blended in as primary animation, others will be belnded out. +- SetAnimationAmount(float value) -- Set target blend amount of current animation +- GetAnimatioNAmount() : float -- returns target blend amount of current animation +- IsAnimationEnded() : bool --returns true if the current animation is ended, false otherwise + +- SetGroundFriction(float value) -- velocity multiplier when moving on ground, default: 0.92 +- SetWaterFriction(float value) -- velocity multiplier when swimming in water, default: 0.9 +- SetSlopeThreshold(float value) -- Slope detection threshold, default: 0.2 +- SetLeaningLimit(float value) -- Leaning min/max clamping, default: 0.12 +- SetFixedUpdateFPS(float value) -- Frame rate of simulation, default: 120 +- SetGravity(float value) -- Gravity value, default: -30 +- SetWaterVerticalOffset(float value) -- vertical offset to keep from water. Useful if character is too submerged in the swimming state + +- SetHealth(int value) -- Set health of the character +- SetWidth(float value) -- Set the horizontal size of the character capsule (same as capsule radius) +- SetHeight(float value) -- Set the vertical size of the character capsule (same as capsule height) +- SetScale(float value) -- Apply an overall scale on the character +- SetPosition(Vector value) -- Set current position immediately (teleport) +- SetVelocity(Vector value) -- Set current velocity immediately +- SetFacing(Vector value) -- Set the facing direction of the character +- SetRelativeOffset(Vector value) -- Apply a relative offset (relative to facing direction) +- SetFootPlacementEnabled(bool value) --Enable/disable foot placement with inverse kinematics + +- GetHealth() : int -- Get the current health +- GetWidth() : float -- Get the horizontal size of the character capsule (same as capsule radius) +- GetHeight() : float -- Get the vertical size of the character capsule (same as capsule height) +- GetScale() : float -- Get the overall scale of the character +- GetPosition() : Vector -- Retrieve the current position without interpolation (this is the raw value from fixed timestep update) +- GetPositionInterpolated() : Vector -- Retrieve the current position with interpolation (this is the position that is rendered) +- GetVelocity() : Vector -- Get current velocity +- GetMovement() : Vector -- Get current movement direction +- IsGrounded() : bool -- returns whether the character is currently standing on ground or not +- IsSwimming() : bool -- returns whether the character is currently swimming or not +- IsFootPlacementEnabled() : bool -- Returns whether foot placement with inverse kinematics is currently enabled or not +- GetCapsule() : Capsule -- returns the capsule representing the character +- GetFacing() : Vector -- returns the immediate facing of the character +- GetFacingSmoothed() : Vector -- returns the smoothed facing of the character +- GetRelativeOffset() : Vector -- returns the relative offset (relative to facing direction) + +- SetPathGoal(Vector goal, VoxelGrid voxelgrid) -- Set the goal for path finding, it will be processed the next time the scene is updated. You can get the results by accessing the pathquery object of the character with GetPathQuery(). +- GetPathQuery() : PathQuery -- returns the PathQuery object of this character + ## Canvas This is used to describe a drawable area diff --git a/Content/scripts/character_controller/character_controller.lua b/Content/scripts/character_controller/character_controller.lua index 8ba90c920..80270f9be 100644 --- a/Content/scripts/character_controller/character_controller.lua +++ b/Content/scripts/character_controller/character_controller.lua @@ -313,10 +313,6 @@ local function Character(model_scene, start_transform, controllable, anim_scene) 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 - movement_velocity = Vector(), - velocity = Vector(), leaning_next = 0, -- leaning sideways when turning leaning = 0, -- leaning sideways when turning (smoothed) savedPointerPos = Vector(), @@ -324,7 +320,7 @@ local function Character(model_scene, start_transform, controllable, anim_scene) jog_speed = 0.2, run_speed = 0.4, jump_speed = 8, - swim_speed = 0.5, + swim_speed = 0.2, layerMask = ~0, -- layerMask will be used to filter collisions scale = Vector(1, 1, 1), rotation = Vector(0,math.pi,0), @@ -340,7 +336,6 @@ local function Character(model_scene, start_transform, controllable, anim_scene) mood_amount = 1, expression = INVALID_ENTITY, - pathquery = PathQuery(), patrol_waypoints = {}, patrol_next = 0, patrol_wait = 0, @@ -351,12 +346,11 @@ local function Character(model_scene, start_transform, controllable, anim_scene) Create = function(self, model_scene, start_transform, controllable, anim_scene) self.position = start_transform.GetPosition() - self.face = vector.Rotate(start_transform.GetForward(), vector.QuaternionFromRollPitchYaw(self.rotation)) - self.face_next = self.face + 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), self.face, Vector(0,1,0)) -- only modify camera rot for 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 @@ -364,6 +358,10 @@ local function Character(model_scene, start_transform, controllable, anim_scene) -- 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) + local layer = scene.Component_GetLayer(self.model) layer.SetLayerMask(self.layerMask) @@ -432,6 +430,16 @@ local function Character(model_scene, start_transform, controllable, anim_scene) self.anims[States.SWIM] = scene.RetargetAnimation(self.humanoid, animations.SWIM, false, anim_scene) self.anims[States.DANCE] = scene.RetargetAnimation(self.humanoid, animations.DANCE, false, anim_scene) self.anims[States.WAVE] = scene.RetargetAnimation(self.humanoid, animations.WAVE, false, anim_scene) + + charactercomponent.AddAnimation(self.anims[States.IDLE]) + charactercomponent.AddAnimation(self.anims[States.WALK]) + charactercomponent.AddAnimation(self.anims[States.JOG]) + charactercomponent.AddAnimation(self.anims[States.RUN]) + charactercomponent.AddAnimation(self.anims[States.JUMP]) + charactercomponent.AddAnimation(self.anims[States.SWIM_IDLE]) + charactercomponent.AddAnimation(self.anims[States.SWIM]) + charactercomponent.AddAnimation(self.anims[States.DANCE]) + charactercomponent.AddAnimation(self.anims[States.WAVE]) local model_transform = scene.Component_GetTransform(self.model) model_transform.ClearTransform() @@ -444,52 +452,55 @@ local function Character(model_scene, start_transform, controllable, anim_scene) end, + GetFacing = function(self) + local charactercomponent = scene.Component_GetCharacter(self.model) + return charactercomponent.GetFacingSmoothed() + end, Jump = function(self,f) - self.velocity.SetY(f) + local charactercomponent = scene.Component_GetCharacter(self.model) + charactercomponent.Jump(f) self.state = States.JUMP 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) - 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.movement_velocity = self.face:Multiply(Vector(speed,speed,speed)) + charactercomponent.Turn(dir.Normalize()) + 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 + 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 velocity = charactercomponent.GetVelocity() + local capsule = charactercomponent.GetCapsule() + character_capsules[self.model] = capsule + local humanoid = scene.Component_GetHumanoid(self.humanoid) humanoid.SetLookAtEnabled(false) - - local face_rotation = matrix.LookTo(Vector(),self.face) - - local model_transform = scene.Component_GetTransform(self.model) - local savedPos = model_transform.GetPosition() - model_transform.ClearTransform() - model_transform.MatrixTransform(matrix.Inverse(face_rotation)) - model_transform.Scale(self.scale) - model_transform.Rotate(self.rotation) - model_transform.Translate(savedPos) - model_transform.UpdateTransform() if self.controllable then -- Camera target control: @@ -529,59 +540,19 @@ local function Character(model_scene, start_transform, controllable, anim_scene) end -- state and animation update - local current_anim = scene.Component_GetAnimation(self.anims[self.state]) - 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 + charactercomponent.PlayAnimation(self.anims[self.state]) + if self.state == States.JUMP then + if charactercomponent.IsAnimationEnded() then + self.state = States.IDLE 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.ground_intersect and self.state ~= States.DANCE and self.state ~= States.WAVE 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 dt > 0.2 then - return -- avoid processing too large delta times to avoid instability - end - - -- swim test: - local swimming = false - 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 oceanpos = scene.GetOceanPosAt(neck_pos) - local water_distance = oceanpos.GetY() - neck_pos.GetY() - if water_distance > 0 then - -- Ocean determined to be above neck: - model_transform.Translate(Vector(0,water_distance - water_threshold,0)) - model_transform.UpdateTransform() - self.velocity.SetY(0) - swimming = true - self.state = States.SWIM_IDLE - else - -- Ray trace water mesh: - 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.velocity.SetY(0) - swimming = true - self.state = States.SWIM_IDLE - end - end + if charactercomponent.IsSwimming() then + self.state = States.SWIM_IDLE end if self.controllable then @@ -624,7 +595,7 @@ local function Character(model_scene, start_transform, controllable, anim_scene) end end - if(input.Press(string.byte('J')) or input.Press(KEYBOARD_BUTTON_SPACE) or input.Press(GAMEPAD_BUTTON_3)) then + 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 @@ -634,7 +605,7 @@ local function Character(model_scene, start_transform, controllable, anim_scene) -- NPC patrol behavior: local patrol_count = len(self.patrol_waypoints) if patrol_count > 0 then - local pos = savedPos + 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 @@ -665,15 +636,17 @@ local function Character(model_scene, start_transform, controllable, anim_scene) end else -- move towards patrol waypoint: - self.pathquery.SetAgentHeight(3) - self.pathquery.Process(pos, patrol_pos, voxelgrid) - if self.pathquery.IsSuccessful() then + local charactercomponent = scene.Component_GetCharacter(self.model) + 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(self.pathquery.GetNextWaypoint(), pos) + patrol_vec = vector.Subtract(pathquery.GetNextWaypoint(), pos) patrol_vec.SetY(0) end if debug then - DrawPathQuery(self.pathquery) + DrawPathQuery(pathquery) end self.patrol_wait = 0 -- check if it's blocked by player collision: @@ -719,142 +692,6 @@ local function Character(model_scene, start_transform, controllable, anim_scene) end end - - -- 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.anims[self.state]) - 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 - self.timestep_occured = false - - if self.fixed_update_remain >= fixed_dt then - self.ground_intersect = false - end - - while self.fixed_update_remain >= fixed_dt do - self.timestep_occured = true; - self.fixed_update_remain = self.fixed_update_remain - fixed_dt - - if swimming then - self.velocity = vector.Multiply(self.velocity, 0.8) -- water friction - end - if self.velocity.GetY() > -30 then - self.velocity = vector.Add(self.velocity, Vector(0, gravity * fixed_dt, 0)) -- gravity - end - self.velocity = vector.Add(self.velocity, self.movement_velocity) - - capsulepos = vector.Add(capsulepos, vector.Multiply(self.velocity, fixed_dt)) - - -- Check ground: - 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 slope = vector.Dot(n2, Vector(0,1,0)) - if slope > slope_threshold then - -- Ground intersection: - self.ground_intersect = true - self.velocity = vector.Multiply(self.velocity, 0.92) -- ground friction - 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 - end - end - - -- Check wall: - capsule = Capsule(capsulepos, vector.Add(capsulepos, Vector(0, capsuleheight)), radius) - 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 slope = vector.Dot(n2, Vector(0,1,0)) - if slope <= slope_threshold then - -- Wall intersection: - local velocityLen = self.velocity.Length() - local velocityNormalized = self.velocity.Normalize() - local undesiredMotion = n2:Multiply(vector.Dot(velocityNormalized, n2)) - local desiredMotion = vector.Subtract(velocityNormalized, undesiredMotion) - self.velocity = vector.Multiply(desiredMotion, velocityLen) - 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 - self.face.SetY(0) - self.face = self.face.Normalize() - self.leaning_next = math.lerp(self.leaning_next, vector.TransformNormal(vector.Subtract(self.face_next, self.face), face_rotation).GetX() * Vector(self.velocity.GetX(), self.velocity.GetZ()).Length() * 0.08, 0.05) - self.leaning = math.lerp(self.leaning, self.leaning_next, 0.05) - - -- Animation blending - if current_anim ~= nil then - -- Blend in current animation: - current_anim.SetAmount(math.lerp(current_anim.GetAmount(), self.anim_amount, 0.1)) - - -- Ease out other animations: - for i,anim in pairs(self.anims) do - if (anim ~= INVALID_ENTITY) and (anim ~= self.anims[self.state]) 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.Rotate(Vector(0, 0, self.leaning * math.pi)) - model_transform.UpdateTransform() - - self.movement_velocity = Vector() - - -- try to put water ripple: - if Vector(self.velocity.GetX(), self.velocity.GetZ()).Length() > 0.01 and self.state ~= States.SWIM_IDLE then - local oceanpos = scene.GetOceanPosAt(self.position) - if self.position.GetY() < oceanpos.GetY() then - PutWaterRipple(Vector(self.position.GetX(), oceanpos.GetY(), self.position.GetZ())) - else - local w,wp = scene.Intersects(capsule, FILTER_WATER) - if w ~= INVALID_ENTITY then - PutWaterRipple(wp) - end - end - end - - character_capsules[self.model] = capsule - self.position = model_transform.GetPosition() -- If camera is inside character capsule, fade out the character, otherwise fade in: if capsule.Intersects(GetCamera().GetPosition()) then @@ -883,87 +720,6 @@ local function Character(model_scene, start_transform, controllable, anim_scene) end, - Update_IK = function(self) - -- Make sure we only update IK as often as we check for ground collisions etc. - -- in Update. Otherwise, when the framerate is higher than the step rate, - -- self.velocity is unreliable - if not self.timestep_occured then return end; - - -- IK foot placement: - local base_y = self.position.GetY() - local ik_foot = INVALID_ENTITY - local ik_pos = Vector() - -- Compute root offset: - -- I determine which foot wants to step on lower ground, that will offset whole root 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.ground_intersect 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_offset = math.lerp(self.root_offset, diff, 0.1) - else - self.root_offset = math.lerp(self.root_offset, 0, 0.1) - end - - -- Offset root transform to lower foot pos: - local root_transform = scene.Component_GetTransform(self.root) - if root_transform ~= nil then - root_transform.Translation_local = Vector(0, self.root_offset) - root_transform.SetDirty(true) - end - - -- 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 @@ -979,7 +735,7 @@ local function Character(model_scene, start_transform, controllable, anim_scene) 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.MatrixTransform(matrix.LookTo(Vector(),self:GetFacing()):Inverse()) transform.Rotate(Vector(math.pi * 0.5)) transform.Scale(0.1) transform.Translate(collPos) @@ -1006,7 +762,7 @@ local function Character(model_scene, start_transform, controllable, anim_scene) 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.MatrixTransform(matrix.LookTo(Vector(),self:GetFacing()):Inverse()) transform.Rotate(Vector(math.pi * 0.5)) transform.Scale(0.1) transform.Translate(collPos) @@ -1051,10 +807,10 @@ local ResolveCharacters = function(characterA, characterB) humanoidA.SetLookAt(headB) humanoidB.SetLookAt(headA) - local facing_amount = vector.Dot(characterB.face, vector.Subtract(characterA.position, characterB.position).Normalize()) + 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.face_next = vector.Subtract(headB, headA):Normalize() + characterA:Turn(vector.Subtract(headB, headA):Normalize()) conversation:Enter(characterA) end DrawDebugText("", vector.Add(headA, Vector(0,0.4)), Vector(1,1,1,1), 0.1, DEBUG_TEXT_DEPTH_TEST | DEBUG_TEXT_CAMERA_FACING) @@ -1387,12 +1143,12 @@ runProcess(function() action = function() conversation.character.mood = Mood.Happy conversation.character.state = States.WAVE - conversation.character.anim_amount = 0.1 + conversation.character:SetAnimationAmount(0.1) end, action_after = function() conversation.character.mood = Mood.Neutral conversation.character.state = States.IDLE - conversation.character.anim_amount = 1 + conversation.character:SetAnimationAmount(1) conversation:Exit() conversation.character.next_dialog = 1 end @@ -1422,15 +1178,6 @@ runProcess(function() 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 @@ -1469,13 +1216,6 @@ runProcess(function() 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()) @@ -1497,8 +1237,6 @@ runProcess(function() DrawCapsule(capsule) local str = "State: " .. player.state .. "\n" - --str = str .. "Velocity = " .. player.velocity.GetX() .. ", " .. player.velocity.GetY() .. "," .. player.velocity.GetZ() .. "\n" - str = str .. "Velocity = " .. player.velocity.GetY() .. "\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) diff --git a/WickedEngine/wiPathQuery_BindLua.cpp b/WickedEngine/wiPathQuery_BindLua.cpp index 3c8dd4d0b..ae40b8002 100644 --- a/WickedEngine/wiPathQuery_BindLua.cpp +++ b/WickedEngine/wiPathQuery_BindLua.cpp @@ -52,7 +52,7 @@ namespace wi::lua return 0; } - pathquery.process(start->GetFloat3(), goal->GetFloat3(), *voxelgrid->voxelgrid); + pathquery->process(start->GetFloat3(), goal->GetFloat3(), *voxelgrid->voxelgrid); return 0; } @@ -90,7 +90,7 @@ namespace wi::lua return 0; } - bool result = pathquery.search_cover( + bool result = pathquery->search_cover( observer->GetFloat3(), subject->GetFloat3(), direction->GetFloat3(), @@ -104,12 +104,12 @@ namespace wi::lua } int PathQuery_BindLua::IsSuccessful(lua_State* L) { - wi::lua::SSetBool(L, pathquery.is_succesful()); + wi::lua::SSetBool(L, pathquery->is_succesful()); return 1; } int PathQuery_BindLua::GetNextWaypoint(lua_State* L) { - Luna::push(L, pathquery.get_next_waypoint()); + Luna::push(L, pathquery->get_next_waypoint()); return 1; } int PathQuery_BindLua::SetDebugDrawWaypointsEnabled(lua_State* L) @@ -120,7 +120,7 @@ namespace wi::lua wi::lua::SError(L, "PathQuery::SetDebugDrawWaypointsEnabled(bool value) not enough arguments!"); return 0; } - pathquery.debug_waypoints = wi::lua::SGetBool(L, 1); + pathquery->debug_waypoints = wi::lua::SGetBool(L, 1); return 0; } int PathQuery_BindLua::SetFlying(lua_State* L) @@ -131,12 +131,12 @@ namespace wi::lua wi::lua::SError(L, "PathQuery::SetFlying(bool value) not enough arguments!"); return 0; } - pathquery.flying = wi::lua::SGetBool(L, 1); + pathquery->flying = wi::lua::SGetBool(L, 1); return 0; } int PathQuery_BindLua::IsFlying(lua_State* L) { - wi::lua::SSetBool(L, pathquery.flying); + wi::lua::SSetBool(L, pathquery->flying); return 1; } int PathQuery_BindLua::SetAgentHeight(lua_State* L) @@ -147,12 +147,12 @@ namespace wi::lua wi::lua::SError(L, "PathQuery::SetAgentHeight(int value) not enough arguments!"); return 0; } - pathquery.agent_height = wi::lua::SGetInt(L, 1); + pathquery->agent_height = wi::lua::SGetInt(L, 1); return 0; } int PathQuery_BindLua::GetAgentHeight(lua_State* L) { - wi::lua::SSetInt(L, pathquery.agent_height); + wi::lua::SSetInt(L, pathquery->agent_height); return 1; } int PathQuery_BindLua::SetAgentWidth(lua_State* L) @@ -163,17 +163,17 @@ namespace wi::lua wi::lua::SError(L, "PathQuery::SetAgentWidth(int value) not enough arguments!"); return 0; } - pathquery.agent_width = wi::lua::SGetInt(L, 1); + pathquery->agent_width = wi::lua::SGetInt(L, 1); return 0; } int PathQuery_BindLua::GetAgentWidth(lua_State* L) { - wi::lua::SSetInt(L, pathquery.agent_width); + wi::lua::SSetInt(L, pathquery->agent_width); return 1; } int PathQuery_BindLua::GetWaypointCount(lua_State* L) { - wi::lua::SSetInt(L, (int)pathquery.get_waypoint_count()); + wi::lua::SSetInt(L, (int)pathquery->get_waypoint_count()); return 1; } int PathQuery_BindLua::GetWaypoint(lua_State* L) @@ -185,12 +185,12 @@ namespace wi::lua return 0; } int index = wi::lua::SGetInt(L, 1); - Luna::push(L, pathquery.get_waypoint(size_t(index))); + Luna::push(L, pathquery->get_waypoint(size_t(index))); return 1; } int PathQuery_BindLua::GetGoal(lua_State* L) { - Luna::push(L, pathquery.get_goal()); + Luna::push(L, pathquery->get_goal()); return 1; } diff --git a/WickedEngine/wiPathQuery_BindLua.h b/WickedEngine/wiPathQuery_BindLua.h index 5d1e13fe8..fef8f4fd7 100644 --- a/WickedEngine/wiPathQuery_BindLua.h +++ b/WickedEngine/wiPathQuery_BindLua.h @@ -8,14 +8,16 @@ namespace wi::lua { class PathQuery_BindLua { + wi::PathQuery owning; public: - wi::PathQuery pathquery; + wi::PathQuery* pathquery = &owning; inline static constexpr char className[] = "PathQuery"; static Luna::FunctionType methods[]; static Luna::PropertyType properties[]; PathQuery_BindLua() = default; PathQuery_BindLua(lua_State* L) {} + PathQuery_BindLua(wi::PathQuery* component) :pathquery(component) {} int Process(lua_State* L); int SearchCover(lua_State* L); diff --git a/WickedEngine/wiPrimitive.h b/WickedEngine/wiPrimitive.h index dd8697ac1..eb35a38b9 100644 --- a/WickedEngine/wiPrimitive.h +++ b/WickedEngine/wiPrimitive.h @@ -116,6 +116,12 @@ namespace wi::primitive { assert(radius >= 0); } + Capsule(XMVECTOR base, XMVECTOR tip, float radius) :radius(radius) + { + assert(radius >= 0); + XMStoreFloat3(&this->base, base); + XMStoreFloat3(&this->tip, tip); + } Capsule(const Sphere& sphere, float height) : base(XMFLOAT3(sphere.center.x, sphere.center.y - sphere.radius, sphere.center.z)), tip(XMFLOAT3(base.x, base.y + height, base.z)), diff --git a/WickedEngine/wiRenderer_BindLua.cpp b/WickedEngine/wiRenderer_BindLua.cpp index 829a49a93..bdb4242b5 100644 --- a/WickedEngine/wiRenderer_BindLua.cpp +++ b/WickedEngine/wiRenderer_BindLua.cpp @@ -477,7 +477,7 @@ namespace wi::lua::renderer PathQuery_BindLua* a = Luna::lightcheck(L, 1); if (a) { - wi::renderer::DrawPathQuery(&a->pathquery); + wi::renderer::DrawPathQuery(a->pathquery); } else wi::lua::SError(L, "DrawPathQuery(PathQuery pathquery) first argument must be a PathQuery type!"); diff --git a/WickedEngine/wiScene.cpp b/WickedEngine/wiScene.cpp index 19e79c6f3..4027a2142 100644 --- a/WickedEngine/wiScene.cpp +++ b/WickedEngine/wiScene.cpp @@ -33,6 +33,9 @@ namespace wi::scene wi::jobsystem::context ctx; + wi::jobsystem::Wait(character_pathfinding_ctx); + character_pathfinding_ctx.priority = wi::jobsystem::Priority::Low; + // Script system runs first, because it could create new entities and components // So GPU persistent resources need to be created accordingly for them too: RunScriptUpdateSystem(ctx); @@ -231,6 +234,8 @@ namespace wi::scene }); } + RunCharacterUpdateSystem(ctx); + RunAnimationUpdateSystem(ctx); wi::physics::RunPhysicsUpdateSystem(ctx, *this, dt); @@ -3126,6 +3131,119 @@ namespace wi::scene auto range = wi::profiler::BeginRangeCPU("Procedural Animations"); + // Character IK foot placement, should be after animations and hierarchy update: + wi::jobsystem::Dispatch(ctx, (uint32_t)characters.GetCount(), 1, [&](wi::jobsystem::JobArgs args) { + CharacterComponent& character = characters[args.jobIndex]; + if (character.humanoidEntity == INVALID_ENTITY) + return; + HumanoidComponent* humanoid = humanoids.GetComponent(character.humanoidEntity); + if (humanoid == nullptr) + return; + + Entity entity = characters.GetEntity(args.jobIndex); + uint32_t layer = ~0u; + LayerComponent* layercomponent = layers.GetComponent(entity); + if (layercomponent != nullptr) + { + layer = layercomponent->GetLayerMask(); + } + + Entity left_foot = humanoid->bones[size_t(HumanoidComponent::HumanoidBone::LeftFoot)]; + Entity right_foot = humanoid->bones[size_t(HumanoidComponent::HumanoidBone::RightFoot)]; + if (left_foot != INVALID_ENTITY && right_foot != INVALID_ENTITY) + { + float base_y = character.position.y; + Entity ik_foot = INVALID_ENTITY; + XMFLOAT3 ik_pos = XMFLOAT3(0, 0, 0); + + if (character.IsFootPlacementEnabled() && character.ground_intersect && XMVectorGetX(XMVector3Length(XMVectorSetY(XMLoadFloat3(&character.velocity), 0))) < 0.1f) + { + TransformComponent* left_transform = transforms.GetComponent(left_foot); + TransformComponent* right_transform = transforms.GetComponent(right_foot); + if (left_transform != nullptr && right_transform != nullptr) + { + // Compute root offset : + // I determine which foot wants to step on lower ground, that will offset whole root downwards + // The other foot will be the upper foot which will be later attached an Inverse Kinematics(IK) effector + XMFLOAT3 left_pos = left_transform->GetPosition(); + XMFLOAT3 right_pos = right_transform->GetPosition(); + Ray left_ray(XMFLOAT3(left_pos.x, left_pos.y + 1, left_pos.z), XMFLOAT3(0, -1, 0), 0, 1.8f); + Ray right_ray(XMFLOAT3(right_pos.x, right_pos.y + 1, right_pos.z), XMFLOAT3(0, -1, 0), 0, 1.8f); + RayIntersectionResult left_result = Intersects(left_ray, FILTER_NAVIGATION_MESH, FILTER_COLLIDER, layer); + RayIntersectionResult right_result = Intersects(right_ray, FILTER_NAVIGATION_MESH, FILTER_COLLIDER, layer); + float left_diff = 0; + float right_diff = 0; + if (left_result.entity != INVALID_ENTITY) + { + left_diff = left_result.position.y - base_y; + } + if (right_result.entity != INVALID_ENTITY) + { + right_diff = right_result.position.y - base_y; + } + float diff = left_diff; + if (left_result.position.y > right_result.position.y) + { + diff = right_diff; + if (left_result.entity != INVALID_ENTITY) + { + ik_foot = left_foot; + ik_pos = left_result.position; + } + } + else + { + if (right_result.entity != INVALID_ENTITY) + { + ik_foot = right_foot; + ik_pos = right_result.position; + } + } + character.root_offset = wi::math::Lerp(character.root_offset, diff, 0.1f); + } + } + else + { + character.root_offset = wi::math::Lerp(character.root_offset, 0.0f, 0.1f); + } + + TransformComponent* humanoid_transform = transforms.GetComponent(character.humanoidEntity); + if (humanoid_transform != nullptr) + { + // Offset root transform to lower foot pos: + humanoid_transform->translation_local.y = character.root_offset; + humanoid_transform->SetDirty(); + } + + // Because IK component removals and creates can be performed below, we must lock: + locker.lock(); + + // Remove IK effectors by default: + if (inverse_kinematics.Contains(left_foot)) + { + inverse_kinematics.Remove(left_foot); + } + if (inverse_kinematics.Contains(right_foot)) + { + inverse_kinematics.Remove(right_foot); + } + + // The upper foot will use IK: + if (ik_foot != INVALID_ENTITY) + { + InverseKinematicsComponent& ik = inverse_kinematics.Create(ik_foot); + ik.use_target_position = true; + ik.target_position = ik_pos; + ik.target_position.y += 0.15f; + ik.chain_length = 2; + ik.iteration_count = 10; + } + + locker.unlock(); + } + }); + wi::jobsystem::Wait(ctx); + if (inverse_kinematics.GetCount() > 0 || humanoids.GetCount() > 0) { transforms_temp = transforms.GetComponentArray(); // make copy @@ -3141,16 +3259,29 @@ namespace wi::scene } Entity entity = inverse_kinematics.GetEntity(i); size_t transform_index = transforms.GetIndex(entity); - size_t target_index = transforms.GetIndex(ik.target); const HierarchyComponent* hier = hierarchy.GetComponent(entity); - if (transform_index == ~0ull || target_index == ~0ull || hier == nullptr) + if (transform_index == ~0ull || hier == nullptr) { continue; } TransformComponent& transform = transforms_temp[transform_index]; - TransformComponent& target = transforms_temp[target_index]; - const XMVECTOR target_pos = target.GetPositionV(); + XMVECTOR target_pos; + if (ik.use_target_position) + { + target_pos = XMLoadFloat3(&ik.target_position); + } + else + { + size_t target_index = transforms.GetIndex(ik.target); + if (target_index == ~0ull) + { + continue; + } + TransformComponent& target = transforms_temp[target_index]; + target_pos = target.GetPositionV(); + } + for (uint32_t iteration = 0; iteration < ik.iteration_count; ++iteration) { TransformComponent* stack[32] = {}; @@ -5124,6 +5255,257 @@ namespace wi::scene font.Update(dt); }); } + void Scene::RunCharacterUpdateSystem(wi::jobsystem::context& ctx) + { + static const XMVECTOR up = XMVectorSet(0, 1, 0, 0); + static const XMMATRIX rotY = XMMatrixRotationY(XM_PI); + + wi::jobsystem::Dispatch(ctx, (uint32_t)characters.GetCount(), 1, [&](wi::jobsystem::JobArgs args) { + CharacterComponent& character = characters[args.jobIndex]; + if (!character.IsActive()) + return; + Entity entity = characters.GetEntity(args.jobIndex); + uint32_t layer = ~0u; + LayerComponent* layercomponent = layers.GetComponent(entity); + if (layercomponent != nullptr) + { + layer = layercomponent->GetLayerMask(); + } + + const float fixed_update_fps = character.fixed_update_fps; + const float timestep = 1.0f / fixed_update_fps; + const float ground_friction = character.ground_friction; + const float water_friction = character.water_friction; + const float slope_threshold = character.slope_threshold; + const float leaning_limit = character.leaning_limit; + const XMVECTOR gravity = XMVectorSet(0, character.gravity * timestep, 0, 0); + + if (character.humanoidEntity == INVALID_ENTITY) + { + // Search for humanoid entity that is either this entity or a child: + if (humanoids.Contains(entity)) + { + character.humanoidEntity = entity; + } + else + { + for (size_t i = 0; i < humanoids.GetCount(); ++i) + { + Entity humanoidEntity = humanoids.GetEntity(i); + if (Entity_IsDescendant(humanoidEntity, entity)) + { + character.humanoidEntity = humanoidEntity; + } + } + } + } + const HumanoidComponent* humanoid = humanoids.GetComponent(character.humanoidEntity); + if (humanoid != nullptr && humanoid->IsRagdollPhysicsEnabled()) + return; + + XMVECTOR velocity = XMLoadFloat3(&character.velocity); + XMVECTOR movement = XMLoadFloat3(&character.movement); + XMVECTOR position = XMLoadFloat3(&character.position); + XMVECTOR height = XMVectorSet(0, character.height, 0, 0); + XMVECTOR platform_velocity_accumulation = XMVectorZero(); + float platform_velocity_count = 0; + + XMMATRIX facing_rot = XMMatrixLookToLH(XMVectorZero(), XMLoadFloat3(&character.facing), up); + + // Swimming: + character.swimming = false; + float swim_offset = 0; + if (humanoid != nullptr && humanoid->bones[size_t(HumanoidComponent::HumanoidBone::Neck)] != INVALID_ENTITY) + { + Entity neck_entity = humanoid->bones[size_t(HumanoidComponent::HumanoidBone::Neck)]; + TransformComponent* neck_transform = transforms.GetComponent(neck_entity); + if (neck_transform != nullptr) + { + XMFLOAT3 neck_pos = neck_transform->GetPosition(); + neck_pos.y += character.water_vertical_offset; + XMFLOAT3 ocean_pos = GetOceanPosAt(neck_pos); + float water_distance = ocean_pos.y - neck_pos.y; + if (water_distance > 0) + { + // Ocean is above the neck: + character.swimming = true; + swim_offset = water_distance; + } + else + { + Ray ray(neck_pos, XMFLOAT3(0, 1, 0), 0, 100); + RayIntersectionResult result = Intersects(ray, FILTER_WATER); + if (result.entity != INVALID_ENTITY) + { + character.swimming = true; + swim_offset = result.distance; + } + } + } + } + + character.accumulator += dt; + + const bool timestep_occurred = character.accumulator >= timestep; + if (timestep_occurred) + { + character.ground_intersect = false; + } + + // Fixed timestep logic: + while (character.accumulator >= timestep) + { + XMStoreFloat3(&character.position_prev, position); + character.accumulator -= timestep; + if (character.swimming) + { + velocity *= water_friction; + } + if (character.velocity.y > character.gravity && !character.swimming) + { + velocity += gravity; + } + velocity += movement; + + position += velocity * timestep; + + // Check ground: + Capsule capsule = Capsule(position, position + height, character.width); + CapsuleIntersectionResult result = Intersects(capsule, FILTER_NAVIGATION_MESH | FILTER_COLLIDER, ~layer); + if (result.entity != INVALID_ENTITY) + { + XMVECTOR collisionNormal = XMLoadFloat3(&result.normal); + const float slope = XMVectorGetX(XMVector3Dot(collisionNormal, up)); + if (slope > slope_threshold) + { + character.ground_intersect = true; + velocity *= ground_friction; + position += XMVectorSet(0, result.depth, 0, 0); + platform_velocity_accumulation += XMLoadFloat3(&result.velocity); + platform_velocity_count += 1.0f; + } + } + + // Check wall: + capsule = Capsule(position, position + height, character.width); + result = Intersects(capsule, FILTER_NAVIGATION_MESH | FILTER_COLLIDER, ~layer); + if (result.entity != INVALID_ENTITY) + { + XMVECTOR collisionNormal = XMLoadFloat3(&result.normal); + const float slope = XMVectorGetX(XMVector3Dot(collisionNormal, up)); + if (slope <= slope_threshold) + { + float velocityLen = XMVectorGetX(XMVector3Length(velocity)); + XMVECTOR velocityNormalized = XMVector3Normalize(velocity); + XMVECTOR undesiredMotion = collisionNormal * XMVector3Dot(velocityNormalized, collisionNormal); + XMVECTOR desiredMotion = velocityNormalized - undesiredMotion; + velocity = desiredMotion * velocityLen; + position += collisionNormal * result.depth; + } + } + + // Some other things also updated at fixed rate: + character.facing = wi::math::Lerp(character.facing, character.facing_next, 0.05f); // smooth turning + character.facing.y = 0; + XMVECTOR facing_next = XMVector3Normalize(XMLoadFloat3(&character.facing_next)); + XMVECTOR facing = XMVector3Normalize(XMLoadFloat3(&character.facing)); + XMStoreFloat3(&character.facing, facing); + XMVECTOR facediff = XMVector3TransformNormal(facing_next - facing, facing_rot); + float velocity_leaning = clamp(XMVectorGetX(facediff * XMVector3Length(XMVectorSetY(velocity, 0))) * 0.08f, -leaning_limit, leaning_limit); + character.leaning_next = lerp(character.leaning_next, velocity_leaning, 0.05f); + character.leaning = lerp(character.leaning, character.leaning_next, 0.05f); + } + character.alpha = character.accumulator / timestep; + + if (platform_velocity_count > 0) + { + position += platform_velocity_accumulation / platform_velocity_count; + } + position += XMVectorSet(0, swim_offset, 0, 0); + + // Simple animation blending: + for (Entity animEntity : character.animations) + { + AnimationComponent* animation = animations.GetComponent(animEntity); + if (animation == nullptr) + continue; + if (animEntity == character.currentAnimation) + { + if (character.reset_anim) + { + character.reset_anim = false; + animation->timer = animation->start; + } + animation->amount = clamp(animation->amount + dt, 0.0f, character.anim_amount); + animation->Play(); + character.anim_ended = animation->timer >= animation->end; + } + else + { + animation->amount = clamp(animation->amount - dt, 0.0f, 0.1f); + if (animation->amount <= 0) + { + animation->Stop(); + } + } + } + + // Try to put water ripple under character: + float horizontal_velocity_length = XMVectorGetX(XMVector3Length(XMVectorSetY(velocity, 0))); + if (horizontal_velocity_length > 0.01) + { + XMFLOAT3 ocean_pos = GetOceanPosAt(character.position); + if (character.position.y < ocean_pos.y) + { + PutWaterRipple(XMFLOAT3(character.position.x, ocean_pos.y, character.position.z)); + } + else + { + Capsule capsule = Capsule(position, position + height, character.width); + CapsuleIntersectionResult result = Intersects(capsule, FILTER_WATER); + if (result.entity != INVALID_ENTITY) + { + PutWaterRipple(result.position); + } + } + } + + XMStoreFloat3(&character.position, position); + XMStoreFloat3(&character.velocity, velocity); + character.movement = XMFLOAT3(0, 0, 0); + + // Apply character transformation on transform component: + TransformComponent* transform = transforms.GetComponent(entity); + if (transform != nullptr) + { + facing_rot = XMMatrixInverse(nullptr, facing_rot); + + XMVECTOR quat = XMQuaternionRotationMatrix(facing_rot * rotY); + XMStoreFloat4(&transform->rotation_local, quat); + transform->RotateRollPitchYaw(XMFLOAT3(0, 0, character.leaning * XM_PI)); + + transform->scale_local = XMFLOAT3(character.scale, character.scale, character.scale); + + transform->translation_local = character.GetPositionInterpolated(); + + XMVECTOR offset = XMLoadFloat3(&character.relative_offset); + offset = XMVector3TransformNormal(offset, facing_rot); + transform->Translate(offset); + + transform->SetDirty(); + } + + if (character.process_goal && character.voxelgrid != nullptr) + { + character.process_goal = false; + wi::jobsystem::Execute(character_pathfinding_ctx, [&](wi::jobsystem::JobArgs args) { + character.pathquery.process(character.position, character.goal, *character.voxelgrid); + }); + } + + }); + wi::jobsystem::Wait(ctx); + } Scene::RayIntersectionResult Scene::Intersects(const Ray& ray, uint32_t filterMask, uint32_t layerMask, uint32_t lod) const { @@ -6434,7 +6816,9 @@ namespace wi::scene img.params.siz = XMFLOAT2(1, 1); img.params.quality = wi::image::QUALITY_ANISOTROPIC; img.params.pivot = XMFLOAT2(0.5f, 0.5f); + locker.lock(); waterRipples.push_back(img); + locker.unlock(); } void Scene::PutWaterRipple(const XMFLOAT3& pos) { @@ -6450,7 +6834,9 @@ namespace wi::scene img.params.siz = XMFLOAT2(1, 1); img.params.quality = wi::image::QUALITY_ANISOTROPIC; img.params.pivot = XMFLOAT2(0.5f, 0.5f); + locker.lock(); waterRipples.push_back(img); + locker.unlock(); } XMVECTOR SkinVertex(const MeshComponent& mesh, const wi::vector& boneData, uint32_t index, XMVECTOR* N) diff --git a/WickedEngine/wiScene.h b/WickedEngine/wiScene.h index 62b669e83..c811c12e5 100644 --- a/WickedEngine/wiScene.h +++ b/WickedEngine/wiScene.h @@ -62,6 +62,7 @@ namespace wi::scene wi::ecs::ComponentManager& fonts = componentLibrary.Register("wi::scene::Scene::fonts"); wi::ecs::ComponentManager& voxel_grids = componentLibrary.Register("wi::scene::Scene::voxel_grids"); wi::ecs::ComponentManager& metadatas = componentLibrary.Register("wi::scene::Scene::metadatas"); + wi::ecs::ComponentManager& characters = componentLibrary.Register("wi::scene::Scene::characters"); // Non-serialized attributes: float dt = 0; @@ -86,6 +87,7 @@ namespace wi::scene void SetAccelerationStructureUpdateRequested(bool value = true) { acceleration_structure_update_requested = value; } bool IsAccelerationStructureUpdateRequested() const { return acceleration_structure_update_requested; } wi::Archive optimized_instatiation_data; + wi::jobsystem::context character_pathfinding_ctx; // AABB culling streams: wi::vector aabb_objects; @@ -462,6 +464,7 @@ namespace wi::scene void RunScriptUpdateSystem(wi::jobsystem::context& ctx); void RunSpriteUpdateSystem(wi::jobsystem::context& ctx); void RunFontUpdateSystem(wi::jobsystem::context& ctx); + void RunCharacterUpdateSystem(wi::jobsystem::context& ctx); struct RayIntersectionResult diff --git a/WickedEngine/wiScene_BindLua.cpp b/WickedEngine/wiScene_BindLua.cpp index a609ed9c5..e6c664756 100644 --- a/WickedEngine/wiScene_BindLua.cpp +++ b/WickedEngine/wiScene_BindLua.cpp @@ -13,6 +13,7 @@ #include "wiVoxelGrid_BindLua.h" #include "wiAudio_BindLua.h" #include "wiAsync_BindLua.h" +#include "wiPathQuery_BindLua.h" #include @@ -514,6 +515,7 @@ void Bind() Luna::Register(L); Luna::Register(L); Luna::Register(L); + Luna::Register(L); wi::lua::RunText(value_bindings); } @@ -560,6 +562,7 @@ Luna::FunctionType Scene_BindLua::methods[] = { lunamethod(Scene_BindLua, Component_CreateFont), lunamethod(Scene_BindLua, Component_CreateVoxelGrid), lunamethod(Scene_BindLua, Component_CreateMetadata), + lunamethod(Scene_BindLua, Component_CreateCharacter), lunamethod(Scene_BindLua, Component_GetName), lunamethod(Scene_BindLua, Component_GetLayer), @@ -588,6 +591,7 @@ Luna::FunctionType Scene_BindLua::methods[] = { lunamethod(Scene_BindLua, Component_GetFont), lunamethod(Scene_BindLua, Component_GetVoxelGrid), lunamethod(Scene_BindLua, Component_GetMetadata), + lunamethod(Scene_BindLua, Component_GetCharacter), lunamethod(Scene_BindLua, Component_GetNameArray), lunamethod(Scene_BindLua, Component_GetLayerArray), @@ -616,6 +620,7 @@ Luna::FunctionType Scene_BindLua::methods[] = { lunamethod(Scene_BindLua, Component_GetFontArray), lunamethod(Scene_BindLua, Component_GetVoxelGridArray), lunamethod(Scene_BindLua, Component_GetMetadataArray), + lunamethod(Scene_BindLua, Component_GetCharacterArray), lunamethod(Scene_BindLua, Entity_GetNameArray), lunamethod(Scene_BindLua, Entity_GetLayerArray), @@ -644,6 +649,7 @@ Luna::FunctionType Scene_BindLua::methods[] = { lunamethod(Scene_BindLua, Entity_GetSpriteArray), lunamethod(Scene_BindLua, Entity_GetVoxelGridArray), lunamethod(Scene_BindLua, Entity_GetMetadataArray), + lunamethod(Scene_BindLua, Entity_GetCharacterArray), lunamethod(Scene_BindLua, Component_RemoveName), lunamethod(Scene_BindLua, Component_RemoveLayer), @@ -673,6 +679,7 @@ Luna::FunctionType Scene_BindLua::methods[] = { lunamethod(Scene_BindLua, Component_RemoveFont), lunamethod(Scene_BindLua, Component_RemoveVoxelGrid), lunamethod(Scene_BindLua, Component_RemoveMetadata), + lunamethod(Scene_BindLua, Component_RemoveCharacter), lunamethod(Scene_BindLua, Component_Attach), lunamethod(Scene_BindLua, Component_Detach), @@ -1444,6 +1451,23 @@ int Scene_BindLua::Component_CreateMetadata(lua_State* L) } return 0; } +int Scene_BindLua::Component_CreateCharacter(lua_State* L) +{ + int argc = wi::lua::SGetArgCount(L); + if (argc > 0) + { + Entity entity = (Entity)wi::lua::SGetLongLong(L, 1); + + CharacterComponent& component = scene->characters.Create(entity); + Luna::push(L, &component); + return 1; + } + else + { + wi::lua::SError(L, "Scene::Component_CreateCharacter(Entity entity) not enough arguments!"); + } + return 0; +} int Scene_BindLua::Component_GetName(lua_State* L) { @@ -2039,6 +2063,28 @@ int Scene_BindLua::Component_GetMetadata(lua_State* L) } return 0; } +int Scene_BindLua::Component_GetCharacter(lua_State* L) +{ + int argc = wi::lua::SGetArgCount(L); + if (argc > 0) + { + Entity entity = (Entity)wi::lua::SGetLongLong(L, 1); + + CharacterComponent* component = scene->characters.GetComponent(entity); + if (component == nullptr) + { + return 0; + } + + Luna::push(L, component); + return 1; + } + else + { + wi::lua::SError(L, "Scene::Component_GetCharacter(Entity entity) not enough arguments!"); + } + return 0; +} int Scene_BindLua::Component_GetNameArray(lua_State* L) { @@ -2337,6 +2383,17 @@ int Scene_BindLua::Component_GetMetadataArray(lua_State* L) } return 1; } +int Scene_BindLua::Component_GetCharacterArray(lua_State* L) +{ + lua_createtable(L, (int)scene->characters.GetCount(), 0); + int newTable = lua_gettop(L); + for (size_t i = 0; i < scene->characters.GetCount(); ++i) + { + Luna::push(L, &scene->characters[i]); + lua_rawseti(L, newTable, lua_Integer(i + 1)); + } + return 1; +} int Scene_BindLua::Entity_GetNameArray(lua_State* L) { @@ -2646,6 +2703,17 @@ int Scene_BindLua::Entity_GetMetadataArray(lua_State* L) } return 1; } +int Scene_BindLua::Entity_GetCharacterArray(lua_State* L) +{ + lua_createtable(L, (int)scene->characters.GetCount(), 0); + int newTable = lua_gettop(L); + for (size_t i = 0; i < scene->characters.GetCount(); ++i) + { + wi::lua::SSetLongLong(L, scene->characters.GetEntity(i)); + lua_rawseti(L, newTable, lua_Integer(i + 1)); + } + return 1; +} int Scene_BindLua::Component_RemoveName(lua_State* L) { @@ -3123,6 +3191,23 @@ int Scene_BindLua::Component_RemoveMetadata(lua_State* L) } return 0; } +int Scene_BindLua::Component_RemoveCharacter(lua_State* L) +{ + int argc = wi::lua::SGetArgCount(L); + if (argc > 0) + { + Entity entity = (Entity)wi::lua::SGetLongLong(L, 1); + if (scene->characters.Contains(entity)) + { + scene->characters.Remove(entity); + } + } + else + { + wi::lua::SError(L, "Scene::Component_RemoveCharacter(Entity entity) not enough arguments!"); + } + return 0; +} int Scene_BindLua::Component_Attach(lua_State* L) { @@ -7396,4 +7481,497 @@ int MetadataComponent_BindLua::SetString(lua_State* L) return 0; } + + + + + + +Luna::FunctionType CharacterComponent_BindLua::methods[] = { + lunamethod(CharacterComponent_BindLua, Move), + lunamethod(CharacterComponent_BindLua, Strafe), + lunamethod(CharacterComponent_BindLua, Jump), + lunamethod(CharacterComponent_BindLua, Turn), + + lunamethod(CharacterComponent_BindLua, AddAnimation), + lunamethod(CharacterComponent_BindLua, PlayAnimation), + lunamethod(CharacterComponent_BindLua, SetAnimationAmount), + lunamethod(CharacterComponent_BindLua, GetAnimationAmount), + lunamethod(CharacterComponent_BindLua, IsAnimationEnded), + + lunamethod(CharacterComponent_BindLua, SetGroundFriction), + lunamethod(CharacterComponent_BindLua, SetWaterFriction), + lunamethod(CharacterComponent_BindLua, SetSlopeThreshold), + lunamethod(CharacterComponent_BindLua, SetLeaningLimit), + lunamethod(CharacterComponent_BindLua, SetFixedUpdateFPS), + lunamethod(CharacterComponent_BindLua, SetGravity), + lunamethod(CharacterComponent_BindLua, SetWaterVerticalOffset), + + lunamethod(CharacterComponent_BindLua, SetActive), + lunamethod(CharacterComponent_BindLua, SetHealth), + lunamethod(CharacterComponent_BindLua, SetWidth), + lunamethod(CharacterComponent_BindLua, SetHeight), + lunamethod(CharacterComponent_BindLua, SetScale), + lunamethod(CharacterComponent_BindLua, SetPosition), + lunamethod(CharacterComponent_BindLua, SetVelocity), + lunamethod(CharacterComponent_BindLua, SetFacing), + lunamethod(CharacterComponent_BindLua, SetRelativeOffset), + lunamethod(CharacterComponent_BindLua, SetFootPlacementEnabled), + + lunamethod(CharacterComponent_BindLua, GetHealth), + lunamethod(CharacterComponent_BindLua, GetWidth), + lunamethod(CharacterComponent_BindLua, GetHeight), + lunamethod(CharacterComponent_BindLua, GetScale), + lunamethod(CharacterComponent_BindLua, GetPosition), + lunamethod(CharacterComponent_BindLua, GetPositionInterpolated), + lunamethod(CharacterComponent_BindLua, GetVelocity), + lunamethod(CharacterComponent_BindLua, GetMovement), + lunamethod(CharacterComponent_BindLua, IsActive), + lunamethod(CharacterComponent_BindLua, IsGrounded), + lunamethod(CharacterComponent_BindLua, IsSwimming), + lunamethod(CharacterComponent_BindLua, IsFootPlacementEnabled), + lunamethod(CharacterComponent_BindLua, GetCapsule), + lunamethod(CharacterComponent_BindLua, GetFacing), + lunamethod(CharacterComponent_BindLua, GetFacingSmoothed), + lunamethod(CharacterComponent_BindLua, GetRelativeOffset), + + lunamethod(CharacterComponent_BindLua, SetPathGoal), + lunamethod(CharacterComponent_BindLua, GetPathQuery), + { NULL, NULL } +}; +Luna::PropertyType CharacterComponent_BindLua::properties[] = { + { NULL, NULL } +}; + +int CharacterComponent_BindLua::Move(lua_State* L) +{ + int argc = wi::lua::SGetArgCount(L); + if (argc < 1) + { + wi::lua::SError(L, "Move(Vector value) not enough arguments!"); + return 0; + } + Vector_BindLua* v = Luna::lightcheck(L, 1); + if (v == nullptr) + { + wi::lua::SError(L, "Move(Vector value) first argument is not a Vector!"); + return 0; + } + component->Move(v->GetFloat3()); + return 0; +} +int CharacterComponent_BindLua::Strafe(lua_State* L) +{ + int argc = wi::lua::SGetArgCount(L); + if (argc < 1) + { + wi::lua::SError(L, "Strafe(Vector value) not enough arguments!"); + return 0; + } + Vector_BindLua* v = Luna::lightcheck(L, 1); + if (v == nullptr) + { + wi::lua::SError(L, "Strafe(Vector value) first argument is not a Vector!"); + return 0; + } + component->Strafe(v->GetFloat3()); + return 0; +} +int CharacterComponent_BindLua::Jump(lua_State* L) +{ + int argc = wi::lua::SGetArgCount(L); + if (argc < 1) + { + wi::lua::SError(L, "Jump(float amount) not enough arguments!"); + return 0; + } + component->Jump(wi::lua::SGetFloat(L, 1)); + return 0; +} +int CharacterComponent_BindLua::Turn(lua_State* L) +{ + int argc = wi::lua::SGetArgCount(L); + if (argc < 1) + { + wi::lua::SError(L, "Turn(Vector value) not enough arguments!"); + return 0; + } + Vector_BindLua* v = Luna::lightcheck(L, 1); + if (v == nullptr) + { + wi::lua::SError(L, "Turn(Vector value) first argument is not a Vector!"); + return 0; + } + component->Turn(v->GetFloat3()); + return 0; +} + +int CharacterComponent_BindLua::AddAnimation(lua_State* L) +{ + int argc = wi::lua::SGetArgCount(L); + if (argc < 1) + { + wi::lua::SError(L, "AddAnimation(Entity entity) not enough arguments!"); + return 0; + } + component->AddAnimation((Entity)wi::lua::SGetLongLong(L, 1)); + return 0; +} +int CharacterComponent_BindLua::PlayAnimation(lua_State* L) +{ + int argc = wi::lua::SGetArgCount(L); + if (argc < 1) + { + wi::lua::SError(L, "PlayAnimation(Entity entity) not enough arguments!"); + return 0; + } + Entity entity = (Entity)wi::lua::SGetLongLong(L, 1); + component->PlayAnimation(entity); + return 0; +} +int CharacterComponent_BindLua::SetAnimationAmount(lua_State* L) +{ + int argc = wi::lua::SGetArgCount(L); + if (argc < 1) + { + wi::lua::SError(L, "SetAnimationAmount(float value) not enough arguments!"); + return 0; + } + component->SetAnimationAmount(wi::lua::SGetFloat(L, 1)); + return 0; +} +int CharacterComponent_BindLua::GetAnimationAmount(lua_State* L) +{ + wi::lua::SSetFloat(L, component->GetAnimationAmount()); + return 1; +} +int CharacterComponent_BindLua::IsAnimationEnded(lua_State* L) +{ + wi::lua::SSetBool(L, component->IsAnimationEnded()); + return 1; +} + +int CharacterComponent_BindLua::SetGroundFriction(lua_State* L) +{ + int argc = wi::lua::SGetArgCount(L); + if (argc < 1) + { + wi::lua::SError(L, "SetGroundFriction(float value) not enough arguments!"); + return 0; + } + component->ground_friction = wi::lua::SGetFloat(L, 1); + return 0; +} +int CharacterComponent_BindLua::SetWaterFriction(lua_State* L) +{ + int argc = wi::lua::SGetArgCount(L); + if (argc < 1) + { + wi::lua::SError(L, "SetWaterFriction(float value) not enough arguments!"); + return 0; + } + component->water_friction = wi::lua::SGetFloat(L, 1); + return 0; +} +int CharacterComponent_BindLua::SetSlopeThreshold(lua_State* L) +{ + int argc = wi::lua::SGetArgCount(L); + if (argc < 1) + { + wi::lua::SError(L, "SetSlopeThreshold(float value) not enough arguments!"); + return 0; + } + component->slope_threshold = wi::lua::SGetFloat(L, 1); + return 0; +} +int CharacterComponent_BindLua::SetLeaningLimit(lua_State* L) +{ + int argc = wi::lua::SGetArgCount(L); + if (argc < 1) + { + wi::lua::SError(L, "SetLeaningLimit(float value) not enough arguments!"); + return 0; + } + component->leaning_limit = wi::lua::SGetFloat(L, 1); + return 0; +} +int CharacterComponent_BindLua::SetFixedUpdateFPS(lua_State* L) +{ + int argc = wi::lua::SGetArgCount(L); + if (argc < 1) + { + wi::lua::SError(L, "SetFixedUpdateFPS(float value) not enough arguments!"); + return 0; + } + component->fixed_update_fps = wi::lua::SGetFloat(L, 1); + return 0; +} +int CharacterComponent_BindLua::SetGravity(lua_State* L) +{ + int argc = wi::lua::SGetArgCount(L); + if (argc < 1) + { + wi::lua::SError(L, "SetGravity(float value) not enough arguments!"); + return 0; + } + component->gravity = wi::lua::SGetFloat(L, 1); + return 0; +} +int CharacterComponent_BindLua::SetWaterVerticalOffset(lua_State* L) +{ + int argc = wi::lua::SGetArgCount(L); + if (argc < 1) + { + wi::lua::SError(L, "SetWaterVerticalOffset(float value) not enough arguments!"); + return 0; + } + component->water_vertical_offset = wi::lua::SGetFloat(L, 1); + return 0; +} + +int CharacterComponent_BindLua::SetActive(lua_State* L) +{ + int argc = wi::lua::SGetArgCount(L); + if (argc < 1) + { + wi::lua::SError(L, "SetActive(int value) not enough arguments!"); + return 0; + } + component->SetActive(wi::lua::SGetBool(L, 1)); + return 0; +} +int CharacterComponent_BindLua::SetHealth(lua_State* L) +{ + int argc = wi::lua::SGetArgCount(L); + if (argc < 1) + { + wi::lua::SError(L, "SetHealth(int value) not enough arguments!"); + return 0; + } + component->health = wi::lua::SGetInt(L, 1); + return 0; +} +int CharacterComponent_BindLua::SetWidth(lua_State* L) +{ + int argc = wi::lua::SGetArgCount(L); + if (argc < 1) + { + wi::lua::SError(L, "SetWidth(float value) not enough arguments!"); + return 0; + } + component->width = wi::lua::SGetFloat(L, 1); + return 0; +} +int CharacterComponent_BindLua::SetHeight(lua_State* L) +{ + int argc = wi::lua::SGetArgCount(L); + if (argc < 1) + { + wi::lua::SError(L, "SetHeight(float value) not enough arguments!"); + return 0; + } + component->height = wi::lua::SGetFloat(L, 1); + return 0; +} +int CharacterComponent_BindLua::SetScale(lua_State* L) +{ + int argc = wi::lua::SGetArgCount(L); + if (argc < 1) + { + wi::lua::SError(L, "SetScale(float value) not enough arguments!"); + return 0; + } + component->scale = wi::lua::SGetFloat(L, 1); + return 0; +} +int CharacterComponent_BindLua::SetPosition(lua_State* L) +{ + int argc = wi::lua::SGetArgCount(L); + if (argc < 1) + { + wi::lua::SError(L, "SetPosition(Vector value) not enough arguments!"); + return 0; + } + Vector_BindLua* v = Luna::lightcheck(L, 1); + if (v == nullptr) + { + wi::lua::SError(L, "SetPosition(Vector value) first argument is not a Vector!"); + return 0; + } + component->SetPosition(v->GetFloat3()); + return 0; +} +int CharacterComponent_BindLua::SetVelocity(lua_State* L) +{ + int argc = wi::lua::SGetArgCount(L); + if (argc < 1) + { + wi::lua::SError(L, "SetVelocity(Vector value) not enough arguments!"); + return 0; + } + Vector_BindLua* v = Luna::lightcheck(L, 1); + if (v == nullptr) + { + wi::lua::SError(L, "SetVelocity(Vector value) first argument is not a Vector!"); + return 0; + } + component->SetVelocity(v->GetFloat3()); + return 0; +} +int CharacterComponent_BindLua::SetFacing(lua_State* L) +{ + int argc = wi::lua::SGetArgCount(L); + if (argc < 1) + { + wi::lua::SError(L, "SetFacing(Vector value) not enough arguments!"); + return 0; + } + Vector_BindLua* v = Luna::lightcheck(L, 1); + if (v == nullptr) + { + wi::lua::SError(L, "SetFacing(Vector value) first argument is not a Vector!"); + return 0; + } + component->SetFacing(v->GetFloat3()); + return 0; +} +int CharacterComponent_BindLua::SetRelativeOffset(lua_State* L) +{ + int argc = wi::lua::SGetArgCount(L); + if (argc < 1) + { + wi::lua::SError(L, "SetRelativeOffset(Vector value) not enough arguments!"); + return 0; + } + Vector_BindLua* v = Luna::lightcheck(L, 1); + if (v == nullptr) + { + wi::lua::SError(L, "SetRelativeOffset(Vector value) first argument is not a Vector!"); + return 0; + } + component->relative_offset = v->GetFloat3(); + return 0; +} +int CharacterComponent_BindLua::SetFootPlacementEnabled(lua_State* L) +{ + int argc = wi::lua::SGetArgCount(L); + if (argc < 1) + { + wi::lua::SError(L, "SetFootPlacementEnabled(bool value) not enough arguments!"); + return 0; + } + component->SetFootPlacementEnabled(wi::lua::SGetBool(L, 1)); + return 0; +} + +int CharacterComponent_BindLua::GetHealth(lua_State* L) +{ + wi::lua::SSetInt(L, component->health); + return 1; +} +int CharacterComponent_BindLua::GetWidth(lua_State* L) +{ + wi::lua::SSetFloat(L, component->width); + return 1; +} +int CharacterComponent_BindLua::GetHeight(lua_State* L) +{ + wi::lua::SSetFloat(L, component->height); + return 1; +} +int CharacterComponent_BindLua::GetScale(lua_State* L) +{ + wi::lua::SSetFloat(L, component->scale); + return 1; +} +int CharacterComponent_BindLua::GetPosition(lua_State* L) +{ + Luna::push(L, component->GetPosition()); + return 1; +} +int CharacterComponent_BindLua::GetPositionInterpolated(lua_State* L) +{ + Luna::push(L, component->GetPositionInterpolated()); + return 1; +} +int CharacterComponent_BindLua::GetVelocity(lua_State* L) +{ + Luna::push(L, component->GetVelocity()); + return 1; +} +int CharacterComponent_BindLua::GetMovement(lua_State* L) +{ + Luna::push(L, component->movement); + return 1; +} +int CharacterComponent_BindLua::IsActive(lua_State* L) +{ + wi::lua::SSetBool(L, component->IsActive()); + return 1; +} +int CharacterComponent_BindLua::IsGrounded(lua_State* L) +{ + wi::lua::SSetBool(L, component->IsGrounded()); + return 1; +} +int CharacterComponent_BindLua::IsSwimming(lua_State* L) +{ + wi::lua::SSetBool(L, component->IsSwimming()); + return 1; +} +int CharacterComponent_BindLua::IsFootPlacementEnabled(lua_State* L) +{ + wi::lua::SSetBool(L, component->IsFootPlacementEnabled()); + return 1; +} +int CharacterComponent_BindLua::GetCapsule(lua_State* L) +{ + Luna::push(L, component->GetCapsule()); + return 1; +} +int CharacterComponent_BindLua::GetFacing(lua_State* L) +{ + Luna::push(L, component->GetFacing()); + return 1; +} +int CharacterComponent_BindLua::GetFacingSmoothed(lua_State* L) +{ + Luna::push(L, component->GetFacingSmoothed()); + return 1; +} +int CharacterComponent_BindLua::GetRelativeOffset(lua_State* L) +{ + Luna::push(L, component->relative_offset); + return 1; +} + +int CharacterComponent_BindLua::SetPathGoal(lua_State* L) +{ + int argc = wi::lua::SGetArgCount(L); + if (argc < 2) + { + wi::lua::SError(L, "SetPathGoal(Vector goal, VoxelGrid voxelgrid) not enough arguments!"); + return 0; + } + Vector_BindLua* v = Luna::lightcheck(L, 1); + if (v == nullptr) + { + wi::lua::SError(L, "SetPathGoal(Vector goal, VoxelGrid voxelgrid) first argument is not a Vector!"); + return 0; + } + VoxelGrid_BindLua* p = Luna::lightcheck(L, 2); + if (p == nullptr) + { + wi::lua::SError(L, "SetPathGoal(Vector goal, VoxelGrid voxelgrid) second argument is not a VoxelGrid!"); + return 0; + } + component->SetPathGoal(v->GetFloat3(), p->voxelgrid); + return 0; +} +int CharacterComponent_BindLua::GetPathQuery(lua_State* L) +{ + Luna::push(L, &component->pathquery); + return 1; +} + } diff --git a/WickedEngine/wiScene_BindLua.h b/WickedEngine/wiScene_BindLua.h index bf5c3808b..b8c0b1410 100644 --- a/WickedEngine/wiScene_BindLua.h +++ b/WickedEngine/wiScene_BindLua.h @@ -75,6 +75,7 @@ namespace wi::lua::scene int Component_CreateFont(lua_State* L); int Component_CreateVoxelGrid(lua_State* L); int Component_CreateMetadata(lua_State* L); + int Component_CreateCharacter(lua_State* L); int Component_GetName(lua_State* L); int Component_GetLayer(lua_State* L); @@ -103,6 +104,7 @@ namespace wi::lua::scene int Component_GetFont(lua_State* L); int Component_GetVoxelGrid(lua_State* L); int Component_GetMetadata(lua_State* L); + int Component_GetCharacter(lua_State* L); int Component_GetNameArray(lua_State* L); int Component_GetLayerArray(lua_State* L); @@ -131,6 +133,7 @@ namespace wi::lua::scene int Component_GetFontArray(lua_State* L); int Component_GetVoxelGridArray(lua_State* L); int Component_GetMetadataArray(lua_State* L); + int Component_GetCharacterArray(lua_State* L); int Entity_GetNameArray(lua_State* L); int Entity_GetLayerArray(lua_State* L); @@ -160,6 +163,7 @@ namespace wi::lua::scene int Entity_GetFontArray(lua_State* L); int Entity_GetVoxelGridArray(lua_State* L); int Entity_GetMetadataArray(lua_State* L); + int Entity_GetCharacterArray(lua_State* L); int Component_RemoveName(lua_State* L); int Component_RemoveLayer(lua_State* L); @@ -189,6 +193,7 @@ namespace wi::lua::scene int Component_RemoveFont(lua_State* L); int Component_RemoveVoxelGrid(lua_State* L); int Component_RemoveMetadata(lua_State* L); + int Component_RemoveCharacter(lua_State* L); int Component_Attach(lua_State* L); int Component_Detach(lua_State* L); @@ -1830,5 +1835,70 @@ namespace wi::lua::scene int SetFloat(lua_State* L); int SetString(lua_State* L); }; + + class CharacterComponent_BindLua + { + private: + wi::scene::CharacterComponent owning; + public: + wi::scene::CharacterComponent* component = nullptr; + + inline static constexpr char className[] = "CharacterComponent"; + static Luna::FunctionType methods[]; + static Luna::PropertyType properties[]; + + CharacterComponent_BindLua(wi::scene::CharacterComponent* component) :component(component) {} + CharacterComponent_BindLua(lua_State* L) : component(&owning) {} + + int Move(lua_State* L); + int Strafe(lua_State* L); + int Jump(lua_State* L); + int Turn(lua_State* L); + + int AddAnimation(lua_State* L); + int PlayAnimation(lua_State* L); + int SetAnimationAmount(lua_State* L); + int GetAnimationAmount(lua_State* L); + int IsAnimationEnded(lua_State* L); + + int SetGroundFriction(lua_State* L); + int SetWaterFriction(lua_State* L); + int SetSlopeThreshold(lua_State* L); + int SetLeaningLimit(lua_State* L); + int SetFixedUpdateFPS(lua_State* L); + int SetGravity(lua_State* L); + int SetWaterVerticalOffset(lua_State* L); + + int SetActive(lua_State* L); + int SetHealth(lua_State* L); + int SetWidth(lua_State* L); + int SetHeight(lua_State* L); + int SetScale(lua_State* L); + int SetPosition(lua_State* L); + int SetVelocity(lua_State* L); + int SetFacing(lua_State* L); + int SetRelativeOffset(lua_State* L); + int SetFootPlacementEnabled(lua_State* L); + + int GetHealth(lua_State* L); + int GetWidth(lua_State* L); + int GetHeight(lua_State* L); + int GetScale(lua_State* L); + int GetPosition(lua_State* L); + int GetPositionInterpolated(lua_State* L); + int GetVelocity(lua_State* L); + int GetMovement(lua_State* L); + int IsActive(lua_State* L); + int IsGrounded(lua_State* L); + int IsSwimming(lua_State* L); + int GetCapsule(lua_State* L); + int GetFacing(lua_State* L); + int GetFacingSmoothed(lua_State* L); + int GetRelativeOffset(lua_State* L); + int IsFootPlacementEnabled(lua_State* L); + + int SetPathGoal(lua_State* L); + int GetPathQuery(lua_State* L); + }; } diff --git a/WickedEngine/wiScene_Components.cpp b/WickedEngine/wiScene_Components.cpp index 52bbe5954..ea6a8fd0e 100644 --- a/WickedEngine/wiScene_Components.cpp +++ b/WickedEngine/wiScene_Components.cpp @@ -2345,4 +2345,125 @@ namespace wi::scene wi::audio::CreateSoundInstance(&soundResource.GetSound(), &soundinstance); } + void CharacterComponent::Move(const XMFLOAT3& direction) + { + movement = direction; + } + void CharacterComponent::Strafe(const XMFLOAT3& direction) + { + XMMATRIX facing_rot = XMMatrixLookToLH(XMVectorZero(), XMLoadFloat3(&facing), XMVectorSet(0, 1, 0, 0)); + facing_rot = XMMatrixInverse(nullptr, facing_rot); + XMVECTOR dir = XMLoadFloat3(&direction); + dir = XMVector3TransformNormal(dir, facing_rot); + XMFLOAT3 absolute_direction; + XMStoreFloat3(&absolute_direction, dir); + Move(absolute_direction); + } + void CharacterComponent::Jump(float amount) + { + velocity.y = amount; + } + void CharacterComponent::Turn(const XMFLOAT3& direction) + { + XMVECTOR F = XMLoadFloat3(&facing); + float dot = XMVectorGetX(XMVector3Dot(F, XMLoadFloat3(&direction))); + if (dot < 0) + { + // help with turning around 180 degrees: + XMStoreFloat3(&facing, XMVector3TransformNormal(F, XMMatrixRotationY(XM_PI * 0.01f))); + } + facing_next = direction; + } + void CharacterComponent::AddAnimation(wi::ecs::Entity entity) + { + animations.push_back(entity); + } + void CharacterComponent::PlayAnimation(wi::ecs::Entity entity) + { + if (currentAnimation != entity) + { + reset_anim = true; + currentAnimation = entity; + } + } + void CharacterComponent::SetAnimationAmount(float amount) + { + anim_amount = amount; + } + float CharacterComponent::GetAnimationAmount() const + { + return anim_amount; + } + bool CharacterComponent::IsAnimationEnded() const + { + return anim_ended; + } + void CharacterComponent::SetPosition(const XMFLOAT3& value) + { + position = value; + position_prev = value; + } + XMFLOAT3 CharacterComponent::GetPosition() const + { + return position; + } + XMFLOAT3 CharacterComponent::GetPositionInterpolated() const + { + return wi::math::Lerp(position_prev, position, alpha); + } + void CharacterComponent::SetVelocity(const XMFLOAT3& value) + { + velocity = value; + } + XMFLOAT3 CharacterComponent::GetVelocity() const + { + return velocity; + } + Capsule CharacterComponent::GetCapsule() const + { + return Capsule(Sphere(position, width), height); + } + void CharacterComponent::SetFacing(const XMFLOAT3& value) + { + facing_next = value; + facing = value; + } + XMFLOAT3 CharacterComponent::GetFacing() const + { + return facing_next; + } + XMFLOAT3 CharacterComponent::GetFacingSmoothed() const + { + return facing; + } + bool CharacterComponent::IsGrounded() const + { + return ground_intersect; + } + bool CharacterComponent::IsSwimming() const + { + return swimming; + } + void CharacterComponent::SetFootPlacementEnabled(bool value) + { + foot_placement_enabled = value; + } + bool CharacterComponent::IsFootPlacementEnabled() const + { + return foot_placement_enabled; + } + void CharacterComponent::SetPathGoal(const XMFLOAT3& goal, const wi::VoxelGrid* voxelgrid) + { + this->goal = goal; + this->voxelgrid = voxelgrid; + process_goal = true; + } + void CharacterComponent::SetActive(bool value) + { + active = value; + } + bool CharacterComponent::IsActive() const + { + return active; + } } diff --git a/WickedEngine/wiScene_Components.h b/WickedEngine/wiScene_Components.h index 759728eef..bd9ccc83f 100644 --- a/WickedEngine/wiScene_Components.h +++ b/WickedEngine/wiScene_Components.h @@ -14,6 +14,7 @@ #include "wiRectPacker.h" #include "wiUnorderedSet.h" #include "wiBVH.h" +#include "wiPathQuery.h" namespace wi::scene { @@ -1622,6 +1623,10 @@ namespace wi::scene uint32_t chain_length = 0; // recursive depth uint32_t iteration_count = 1; // computation step count. Increase this too for greater chain length + // Non-serialized attributes: + bool use_target_position = false; + XMFLOAT3 target_position = XMFLOAT3(0, 0, 0); + inline void SetDisabled(bool value = true) { if (value) { _flags |= DISABLED; } else { _flags &= ~DISABLED; } } inline bool IsDisabled() const { return _flags & DISABLED; } @@ -2060,4 +2065,117 @@ namespace wi::scene void Serialize(wi::Archive& archive, wi::ecs::EntitySerializer& seri); }; + + struct CharacterComponent + { + enum FLAGS + { + NONE = 0, + }; + uint32_t _flags = NONE; + + int health = 100; + float width = 0.4f; // capsule radius + float height = 1.5f; // capsule height + float scale = 1.0f; // overall scaling + + // Non-serialized attributes: + float ground_friction = 0.92f; + float water_friction = 0.9f; + float slope_threshold = 0.2f; + float leaning_limit = 0.12f; + float fixed_update_fps = 120; + float gravity = -30; + float water_vertical_offset = 0; + XMFLOAT3 movement = XMFLOAT3(0, 0, 0); + XMFLOAT3 velocity = XMFLOAT3(0, 0, 0); + XMFLOAT3 position = XMFLOAT3(0, 0, 0); + XMFLOAT3 position_prev = XMFLOAT3(0, 0, 0); + XMFLOAT3 relative_offset = XMFLOAT3(0, 0, 0); + bool active = true; + float accumulator = 0; // fixed timestep accumulation + float alpha = 0; // fixed timestep interpolation + XMFLOAT3 facing = XMFLOAT3(0, 0, 1); // forward facing direction (smoothed) + XMFLOAT3 facing_next = XMFLOAT3(0, 0, 1); // forward facing direction (immediate) + float leaning = 0; // sideways leaning (smoothed) + float leaning_next = 0; // sideways leaning (immediate) + bool ground_intersect = false; + bool swimming = false; + wi::ecs::Entity humanoidEntity = wi::ecs::INVALID_ENTITY; + float root_offset = 0; + bool foot_placement_enabled = true; + wi::PathQuery pathquery; + wi::vector animations; + wi::ecs::Entity currentAnimation = wi::ecs::INVALID_ENTITY; + float anim_amount = 1; + bool reset_anim = true; + bool anim_ended = true; + XMFLOAT3 goal = XMFLOAT3(0, 0, 0); + bool process_goal = false; + const wi::VoxelGrid* voxelgrid = nullptr; + + // Apply movement to the character in the next update + void Move(const XMFLOAT3& direction); + // Apply movement relative to the character facing in the next update + void Strafe(const XMFLOAT3& direction); + void Jump(float amount); + void Turn(const XMFLOAT3& direction); + + void AddAnimation(wi::ecs::Entity entity); + void PlayAnimation(wi::ecs::Entity entity); + void SetAnimationAmount(float amount); + float GetAnimationAmount() const; + bool IsAnimationEnded() const; + + // Teleport character to position immediately + void SetPosition(const XMFLOAT3& position); + + // Retrieve the current position without interpolation (this is the raw value from fixed timestep update) + XMFLOAT3 GetPosition() const; + + // Retrieve the current position with interpolation (this is the position that is rendered) + XMFLOAT3 GetPositionInterpolated() const; + + // Sets velocity immediately + void SetVelocity(const XMFLOAT3& position); + + // Gets the current velocity + XMFLOAT3 GetVelocity() const; + + // Returns the capsule representing the character, that is also used in collisions + wi::primitive::Capsule GetCapsule() const; + + // Set the facing direction immediately + void SetFacing(const XMFLOAT3& value); + + // Get the immediate facing direction + XMFLOAT3 GetFacing() const; + + // Get the smoothed facing direction + XMFLOAT3 GetFacingSmoothed() const; + + // Returns whether the character is currently stading on ground or not + bool IsGrounded() const; + + // Returns whether the character is currently swimming or not + bool IsSwimming() const; + + // Enable/disable foot placement with inverse kinematics + void SetFootPlacementEnabled(bool value); + + // Returns whether foot placement with inverse kinematics is currently enabled or not + bool IsFootPlacementEnabled() const; + + // Set the goal for path finding, it will be processed the next time the scene is updated. + // You can get the results by accessing the pathquery object of the character. + void SetPathGoal(const XMFLOAT3& goal, const wi::VoxelGrid* voxelgrid); + + // Enable/disable the character's processing + void SetActive(bool value); + + // Returns whether character processing is active or not + bool IsActive() const; + + void Serialize(wi::Archive& archive, wi::ecs::EntitySerializer& seri); + }; } diff --git a/WickedEngine/wiScene_Serializers.cpp b/WickedEngine/wiScene_Serializers.cpp index dde13308b..4a40f8972 100644 --- a/WickedEngine/wiScene_Serializers.cpp +++ b/WickedEngine/wiScene_Serializers.cpp @@ -2215,6 +2215,27 @@ namespace wi::scene } } } + void CharacterComponent::Serialize(wi::Archive& archive, EntitySerializer& seri) + { + if (archive.IsReadMode()) + { + archive >> _flags; + + archive >> health; + archive >> width; + archive >> height; + archive >> scale; + } + else + { + archive << _flags; + + archive << health; + archive << width; + archive << height; + archive << scale; + } + } void Scene::Serialize(wi::Archive& archive) { diff --git a/WickedEngine/wiVersion.cpp b/WickedEngine/wiVersion.cpp index c6b1e69c9..340777462 100644 --- a/WickedEngine/wiVersion.cpp +++ b/WickedEngine/wiVersion.cpp @@ -9,7 +9,7 @@ namespace wi::version // minor features, major updates, breaking compatibility changes const int minor = 71; // minor bug fixes, alterations, refactors, updates - const int revision = 534; + const int revision = 535; const std::string version_string = std::to_string(major) + "." + std::to_string(minor) + "." + std::to_string(revision); @@ -50,7 +50,7 @@ All contributors: https://github.com/turanszkij/WickedEngine/graphs/contributors Patreon supporters --------------------------- -Nemerle, James Webb, Quifeng Jin, TheGameCreators, Joseph Goldin, Yuri, Sergey K, Yukawa Kanta, Dragon Josh, John, LurkingNinja, Bernardo Del Castillo, Invictus, Scott Hunt, Yazan Altaki, Tuan NV, Robert MacGregor, cybernescence, Alexander Dahlin, blueapples, Delhills, NI NI, Sherief, ktopoet, Justin Macklin, Cédric Fabre, TogetherTeam, Bartosz Boczula, Arne Koenig, Ivan Trajchev, nathants, Fahd Ahmed, Gabriel Jadderson, SAS_Controller, Dominik Madarász, Segfault, Mike amanfo, Dennis Brakhane, rookie, Peter Moore, therealjtgill, Nicolas Embleton, Desuuc, radino1977, Anthony Curtis, manni heck, Matthias Hölzl, Phyffer, Lucas Pinheiro, Tapkaara, gpman, Anthony Python, Gnowos, Klaus, slaughternaut, Paul Brain, Connor Greaves, Alexandr, Lee Bamber, MCAlarm MC2, Titoutan, Willow, Aldo, lokimx, K. Osterman, Nomad, ykl, Alex Krokos, Timmy, Avaflow, mat, Hexegonel Samael Michael, Joe Spataro, soru, GeniokV, Mammoth, Ignacio, datae, Jason Rice, MarsBEKET, Tim, Twisty, Zelf ieats kiezen, Romildo Franco +Nemerle, James Webb, Quifeng Jin, TheGameCreators, Joseph Goldin, Yuri, Sergey K, Yukawa Kanta, Dragon Josh, John, LurkingNinja, Bernardo Del Castillo, Invictus, Scott Hunt, Yazan Altaki, Tuan NV, Robert MacGregor, cybernescence, Alexander Dahlin, blueapples, Delhills, NI NI, Sherief, ktopoet, Justin Macklin, Cédric Fabre, TogetherTeam, Bartosz Boczula, Arne Koenig, Ivan Trajchev, nathants, Fahd Ahmed, Gabriel Jadderson, SAS_Controller, Dominik Madarász, Segfault, Mike amanfo, Dennis Brakhane, rookie, Peter Moore, therealjtgill, Nicolas Embleton, Desuuc, radino1977, Anthony Curtis, manni heck, Matthias Hölzl, Phyffer, Lucas Pinheiro, Tapkaara, gpman, Anthony Python, Gnowos, Klaus, slaughternaut, Paul Brain, Connor Greaves, Alexandr, Lee Bamber, MCAlarm MC2, Titoutan, Willow, Aldo, lokimx, K. Osterman, Nomad, ykl, Alex Krokos, Timmy, Avaflow, mat, Hexegonel Samael Michael, Joe Spataro, soru, GeniokV, Mammoth, Ignacio, datae, Jason Rice, MarsBEKET, Tim, Twisty, Zelf ieats kiezen, Romildo Franco, zNachoh, Dmitriy )"; return credits;