Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
using UnityEngine;

namespace DCL.CharacterMotion.Components
{
/// <summary>
/// Added to the player entity when a smooth movement over duration is requested.
/// The movement bypasses physics and directly interpolates the transform position.
/// Removed after the movement is completed.
/// </summary>
public struct PlayerMoveToWithDurationIntent
{
public Vector3 StartPosition;
public Vector3 TargetPosition;
public Vector3? CameraTarget;
public Vector3? AvatarTarget;
public float Duration;
public float ElapsedTime;

/// <summary>
/// Tracks the position from the last frame for animation speed calculation.
/// </summary>
public Vector3 LastFramePosition;

public PlayerMoveToWithDurationIntent(
Vector3 startPosition,
Vector3 targetPosition,
Vector3? cameraTarget,
Vector3? avatarTarget,
float duration)
{
StartPosition = startPosition;
TargetPosition = targetPosition;
CameraTarget = cameraTarget;
AvatarTarget = avatarTarget;
Duration = duration;
ElapsedTime = 0f;
LastFramePosition = startPosition;
}

public float Progress => Duration > 0f ? Mathf.Clamp01(ElapsedTime / Duration) : 1f;
public bool IsComplete => ElapsedTime >= Duration;
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ protected override void Update(float t)
}

[Query]
[None(typeof(DeleteEntityIntention), typeof(RandomAvatar))]
[None(typeof(DeleteEntityIntention), typeof(RandomAvatar), typeof(PlayerMoveToWithDurationIntent))]
private void ResolveVelocity(
[Data] float dt,
[Data] int physicsTick,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ protected override void Update(float t)
}

[Query]
[None(typeof(PlayerMoveToWithDurationIntent))]
private void UpdateAnimation(
[Data] float dt,
ref CharacterAnimationComponent animationComponent,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ protected override void Update(float _)
}

[Query]
[None(typeof(PlayerTeleportIntent), typeof(DeleteEntityIntention))]
[None(typeof(PlayerTeleportIntent), typeof(DeleteEntityIntention), typeof(PlayerMoveToWithDurationIntent))]
private void ResolvePlatformMovement(
in ICharacterControllerSettings settings,
ref CharacterPlatformComponent platformComponent,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ protected override void Update(float t)
}

[Query]
[None(typeof(StopCharacterMotion), typeof(PlayerTeleportIntent), typeof(DeleteEntityIntention), typeof(PlayerTeleportIntent.JustTeleported))]
[None(typeof(StopCharacterMotion), typeof(PlayerTeleportIntent), typeof(DeleteEntityIntention), typeof(PlayerTeleportIntent.JustTeleported), typeof(PlayerMoveToWithDurationIntent))]
private void Interpolate(
[Data] float dt,
in ICharacterControllerSettings settings,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
using Arch.Core;
using Arch.System;
using Arch.SystemGroups;
using DCL.AvatarRendering.AvatarShape.UnityInterface;
using DCL.Character.Components;
using DCL.CharacterMotion.Animation;
using DCL.CharacterMotion.Components;
using DCL.Diagnostics;
using DCL.Utilities;
using ECS.Abstract;
using UnityEngine;
using Utility.Arch;

namespace DCL.CharacterMotion.Systems
{
/// <summary>
/// Handles smooth player movement over a specified duration.
/// Bypasses physics and directly interpolates the transform position.
/// Mutually exclusive with <see cref="InterpolateCharacterSystem"/> and <see cref="TeleportCharacterSystem"/>.
/// </summary>
[UpdateInGroup(typeof(ChangeCharacterPositionGroup))]
[UpdateAfter(typeof(TeleportCharacterSystem))]
[LogCategory(ReportCategory.MOTION)]
public partial class MovePlayerWithDurationSystem : BaseUnityLoopSystem
{
private MovePlayerWithDurationSystem(World world) : base(world) { }

protected override void Update(float t)
{
InterruptMovementOnInputQuery(World);
MovePlayerQuery(World, t);
}

[Query]
[All(typeof(PlayerMoveToWithDurationIntent))]
private void InterruptMovementOnInput(Entity entity, in MovementInputComponent movementInputComponent)
{
if (movementInputComponent.Kind == MovementKind.IDLE || movementInputComponent.Axes == Vector2.zero)
return;

World.Remove<PlayerMoveToWithDurationIntent>(entity);
}

[Query]
private void MovePlayer(
[Data] float deltaTime,
Entity entity,
ref PlayerMoveToWithDurationIntent moveIntent,
ref CharacterTransform characterTransform,
ref CharacterRigidTransform rigidTransform,
ref CharacterAnimationComponent animationComponent,
in IAvatarView avatarView)
{
// Always enforce the movement direction rotation every frame
// This ensures consistent behavior even if the component is added mid-frame
ApplyMovementDirectionRotation(characterTransform, ref rigidTransform, in moveIntent);

moveIntent.ElapsedTime += deltaTime;

float progress = moveIntent.Progress;

// Smooth step for easing
float smoothProgress = SmoothStep(progress);
Vector3 newPosition = Vector3.Lerp(moveIntent.StartPosition, moveIntent.TargetPosition, smoothProgress);

// We intentionally bypass CharacterController.Move() to avoid physics/collision detection.
// This allows the player to move smoothly to the target position without being blocked by obstacles.
characterTransform.Transform.position = newPosition;

// Update animation based on movement speed
UpdateAnimation(deltaTime, avatarView, ref animationComponent, ref moveIntent, newPosition);

if (moveIntent.IsComplete)
{
// Ensure final position is exact
characterTransform.Transform.position = moveIntent.TargetPosition;

ApplyFinalRotation(characterTransform, ref rigidTransform, in moveIntent);
ResetAnimationToIdle(avatarView, ref animationComponent);

World.Remove<PlayerMoveToWithDurationIntent>(entity);
World.AddOrSet(entity, new MovePlayerToInfo(UnityEngine.Time.frameCount));
}
}

/// <summary>
/// Immediately faces the movement direction (from start to target).
/// Sets both the transform rotation and the LookDirection in CharacterRigidTransform.
/// </summary>
private static void ApplyMovementDirectionRotation(
CharacterTransform characterTransform,
ref CharacterRigidTransform rigidTransform,
in PlayerMoveToWithDurationIntent moveIntent)
{
Vector3 movementDirection = moveIntent.TargetPosition - moveIntent.StartPosition;
movementDirection.y = 0; // Keep rotation horizontal

if (movementDirection.sqrMagnitude < 0.0001f)
return;

Vector3 normalizedDirection = movementDirection.normalized;

// Set the LookDirection so RotateCharacterSystem maintains this rotation
rigidTransform.LookDirection = normalizedDirection;

// Also set the transform rotation immediately
characterTransform.Transform.rotation = Quaternion.LookRotation(normalizedDirection, Vector3.up);
}

/// <summary>
/// Applies final rotation instantly when movement completes.
/// If avatarTarget exists, faces that; otherwise no rotation change.
/// Sets both the transform rotation and the LookDirection in CharacterRigidTransform.
/// </summary>
private static void ApplyFinalRotation(
CharacterTransform characterTransform,
ref CharacterRigidTransform rigidTransform,
in PlayerMoveToWithDurationIntent moveIntent)
{
// Only apply final rotation if avatarTarget is specified
if (moveIntent.AvatarTarget == null)
return;

Vector3 lookDirection = moveIntent.AvatarTarget.Value - moveIntent.TargetPosition;
lookDirection.y = 0;

if (lookDirection.sqrMagnitude < 0.0001f)
return;

Vector3 normalizedDirection = lookDirection.normalized;

// Set the LookDirection so RotateCharacterSystem maintains this rotation
rigidTransform.LookDirection = normalizedDirection;

// Instantly snap to face avatar target
characterTransform.Transform.rotation = Quaternion.LookRotation(normalizedDirection, Vector3.up);
}

private static void UpdateAnimation(
float deltaTime,
IAvatarView avatarView,
ref CharacterAnimationComponent animationComponent,
ref PlayerMoveToWithDurationIntent moveIntent,
Vector3 currentPosition)
{
// Calculate speed based on actual position change
float distance = Vector3.Distance(currentPosition, moveIntent.LastFramePosition);
float speed = deltaTime > 0 ? distance / deltaTime : 0f;

// Update last frame position for next frame's calculation
moveIntent.LastFramePosition = currentPosition;

// Get blend value from speed (0 = idle, 1 = walk, 2 = jog, 3 = run)
float movementBlendValue = speed > 0.01f ? RemotePlayerUtils.GetBlendValueFromSpeed(speed) : 0f;

// Set animation state as grounded movement
animationComponent.IsSliding = false;
animationComponent.States.MovementBlendValue = movementBlendValue;
animationComponent.States.SlideBlendValue = 0;
animationComponent.States.IsGrounded = true;
animationComponent.States.IsJumping = false;
animationComponent.States.IsLongJump = false;
animationComponent.States.IsLongFall = false;
animationComponent.States.IsFalling = false;

// Apply animator parameters
AnimationMovementBlendLogic.SetAnimatorParameters(ref animationComponent, avatarView, isGrounded: true, movementBlendId: 0);
AnimationSlideBlendLogic.SetAnimatorParameters(ref animationComponent, avatarView);
AnimationStatesLogic.SetAnimatorParameters(avatarView, ref animationComponent.States, isJumping: false, jumpTriggered: false, isStunned: false);
}

private static void ResetAnimationToIdle(
IAvatarView avatarView,
ref CharacterAnimationComponent animationComponent)
{
// Reset to idle state
animationComponent.States.MovementBlendValue = 0f;
animationComponent.States.IsGrounded = true;

AnimationMovementBlendLogic.SetAnimatorParameters(ref animationComponent, avatarView, isGrounded: true, movementBlendId: 0);
}

// TODO: Do we want this easing??
/// <summary>
/// Smooth step function for easing (ease-in-out)
/// </summary>
private static float SmoothStep(float t)
=> t * t * (3f - (2f * t));
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ protected override void Update(float t)
}

[Query]
[None(typeof(PlayerLookAtIntent), typeof(PBAvatarShape))]
[None(typeof(PlayerLookAtIntent), typeof(PBAvatarShape), typeof(PlayerMoveToWithDurationIntent))]
private void LerpRotation(
[Data] float dt,
ref ICharacterControllerSettings settings,
Expand All @@ -61,7 +61,7 @@ private void LerpRotation(
}

[Query]
[None(typeof(PBAvatarShape))]
[None(typeof(PBAvatarShape), typeof(PlayerMoveToWithDurationIntent))]
private void ForceLookAt(in Entity entity, ref CharacterRigidTransform rigidTransform, ref CharacterTransform transform, ref CharacterPlatformComponent platformComponent, in PlayerLookAtIntent lookAtIntent)
{
// Rotate player to look at target
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@
"GUID:543b8f091a5947a3880b7f2bca2358bd",
"GUID:60728e6f5a869504aad5d4a84318c7cc",
"GUID:b97826ed91484ac1af953a8afc03316e",
"GUID:571dc9f8bded0034f98595106462e3d0"
"GUID:571dc9f8bded0034f98595106462e3d0",
"GUID:c80c82a8f4e04453b85fbab973d6774a"
],
"includePlatforms": [],
"excludePlatforms": [],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@
using DCL.AvatarRendering.AvatarShape.Components;
using DCL.AvatarRendering.Emotes;
using DCL.AvatarRendering.Loading.Components;
using DCL.Character.Components;
using DCL.CharacterCamera;
using DCL.CharacterMotion.Components;
using DCL.Diagnostics;
using DCL.Ipfs;
using DCL.Multiplayer.Emotes;
using ECS.Abstract;
using ECS.Prioritization.Components;
using ECS.SceneLifeCycle.Components;
using ECS.StreamableLoading.Common;
using SceneRunner.Scene;
using System;
Expand All @@ -29,6 +29,7 @@ namespace CrdtEcsBridge.RestrictedActions
public class GlobalWorldActions : IGlobalWorldActions
{
private const string SCENE_EMOTE_NAMING = "_emote.glb";
private const float MOVE_PLAYER_TO_COMPLETION_THRESHOLD = 0.1f;

private readonly World world;
private readonly Entity playerEntity;
Expand All @@ -47,9 +48,32 @@ public GlobalWorldActions(World world, Entity playerEntity, IEmotesMessageBus me
this.isBuilderCollectionPreview = isBuilderCollectionPreview;
}

public void MoveAndRotatePlayer(Vector3 newPlayerPosition, Vector3? newCameraTarget, Vector3? newAvatarTarget)
public async UniTask<bool> MoveAndRotatePlayerAsync(Vector3 newPlayerPosition, Vector3? newCameraTarget, Vector3? newAvatarTarget, float duration, CancellationToken ct)
{
// Move player to new position (through TeleportCharacterSystem -> TeleportPlayerQuery)
if (duration > 0f)
{
// Smooth movement over duration (through MovePlayerWithDurationSystem)
Vector3 startPosition = world.Get<CharacterTransform>(playerEntity).Transform.position;
world.AddOrSet(playerEntity, new PlayerMoveToWithDurationIntent(
startPosition,
newPlayerPosition,
newCameraTarget,
newAvatarTarget,
duration));

// Wait until the movement intent is removed (either completed or interrupted)
while (world.Has<PlayerMoveToWithDurationIntent>(playerEntity))
{
ct.ThrowIfCancellationRequested();
await UniTask.Yield(PlayerLoopTiming.Update, ct);
}

// Check if we reached the target position (not interrupted by input or anything)
Vector3 finalPosition = world.Get<CharacterTransform>(playerEntity).Transform.position;
return Vector3.Distance(finalPosition, newPlayerPosition) < MOVE_PLAYER_TO_COMPLETION_THRESHOLD;
}

// Instant teleport (through TeleportCharacterSystem -> TeleportPlayerQuery)
world.AddOrSet(playerEntity, new PlayerTeleportIntent(null, Vector2Int.zero, newPlayerPosition, CancellationToken.None, isPositionSet: true));
world.AddOrSet(playerEntity, new MovePlayerToInfo(MultithreadingUtility.FrameCount));

Expand All @@ -64,6 +88,9 @@ public void MoveAndRotatePlayer(Vector3 newPlayerPosition, Vector3? newCameraTar
{
world.AddOrSet(playerEntity, new PlayerLookAtIntent(newCameraTarget.Value));
}

// Instant teleport is always successful
return true;
}

public void RotateCamera(Vector3? newCameraTarget, Vector3 newPlayerPosition)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ namespace CrdtEcsBridge.RestrictedActions
{
public interface IGlobalWorldActions
{
void MoveAndRotatePlayer(Vector3 newPlayerPosition, Vector3? newCameraTarget, Vector3? newAvatarTarget);
UniTask<bool> MoveAndRotatePlayerAsync(Vector3 newPlayerPosition, Vector3? newCameraTarget, Vector3? newAvatarTarget, float duration, CancellationToken ct);
void RotateCamera(Vector3? newCameraTarget, Vector3 newPlayerPosition);
UniTask TriggerSceneEmoteAsync(ISceneData sceneData, string src, string hash, bool loop, CancellationToken ct);
void TriggerEmote(URN urn, bool isLooping = false);
Expand Down
Loading
Loading