Skip to content

Commit

Permalink
#243 Fix player cursor
Browse files Browse the repository at this point in the history
  • Loading branch information
polycone committed Oct 24, 2023
1 parent fe35618 commit d623ef8
Show file tree
Hide file tree
Showing 2 changed files with 121 additions and 99 deletions.
166 changes: 97 additions & 69 deletions src/MultiplayerMod/Multiplayer/Components/CursorComponent.cs
Original file line number Diff line number Diff line change
@@ -1,157 +1,180 @@
using System;
using System.Collections.Generic;
using System.Linq;
using MultiplayerMod.Core.Dependency;
using MultiplayerMod.Core.Events;
using MultiplayerMod.Core.Unity;
using MultiplayerMod.Multiplayer.Players.Events;
using TMPro;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;

namespace MultiplayerMod.Multiplayer.Components;

public class CursorComponent : MonoBehaviour {
public class CursorComponent : MultiplayerKMonoBehaviour {

// Perhaps we should provide a setting to enable this feature later on.
private const bool ENABLE_CURSORS_RELATIVE_TO_GUI = false;

private Camera camera = null!;
private Image imageComponent = null!;
private TextMeshProUGUI textComponent = null!;
[InjectDependency]
private readonly EventDispatcher events = null!;

private bool initialized;
[MyCmpAdd]
private readonly Canvas canvas = null!;

public readonly SmoothCursor CursorWithinWorld = new();
public readonly SmoothCursor CursorWithinScreen = new();
public string PlayerName { get; set; } = null!;
[MyCmpReq]
private readonly AssignedMultiplayerPlayer assignedPlayer = null!;

public string? ScreenName { get; set; }
public Type? ScreenType { get; set; }
private Camera camera = null!;
private Image cursorImage = null!;
private TextMeshProUGUI cursorText = null!;

private void OnEnable() {
var parent = GameScreenManager.Instance.GetParent(GameScreenManager.UIRenderTarget.ScreenSpaceOverlay);
private readonly SmoothCursor worldCursor = new();
private readonly SmoothCursor screenCursor = new();

var cursorTexture = Assets.GetTexture("cursor_arrow");
var canvas = CreateCanvas(parent);
CreateCursorGameObject(gameObject, cursorTexture);
CreateTextGameObject(gameObject, cursorTexture);
private EventSubscription subscription = null!;

gameObject.transform.SetParent(canvas.transform, false);
gameObject.SetLayerRecursively(LayerMask.NameToLayer("UI"));
private string? playerName;
private string? screenName;
private Type? screenType;

protected override void OnSpawn() {
camera = GameScreenManager.Instance.GetCamera(GameScreenManager.UIRenderTarget.ScreenSpaceCamera);

initialized = true;
}
var cursorTexture = Assets.GetTexture("cursor_arrow");
var cursor = new GameObject(name);
cursorImage = CreateCursorImage(cursor, cursorTexture);
cursorText = CreateCursorText(cursor, new Vector3(cursorTexture.width, -cursorTexture.height, 0));
cursor.transform.SetParent(transform, false);
gameObject.SetLayerRecursively(LayerMask.NameToLayer("UI"));

private GameObject CreateCanvas(GameObject parent) {
var canvasGameObject = new GameObject { transform = { parent = parent.transform } };
var canvas = canvasGameObject.AddComponent<Canvas>();
playerName = assignedPlayer.Player.Profile.PlayerName;
canvas.overrideSorting = true;
canvas.sortingOrder = 100;
return canvasGameObject;

subscription = events.Subscribe<PlayerCursorPositionUpdatedEvent>(OnPlayerCursorPositionUpdated);
}

private void CreateCursorGameObject(GameObject parent, Texture2D cursorTexture) {
var imageGameObject = new GameObject { transform = { parent = parent.transform } };
private Image CreateCursorImage(GameObject parent, Texture2D cursorTexture) {
var imageGameObject = new GameObject(name) { transform = { parent = parent.transform } };
var rectTransform = imageGameObject.AddComponent<RectTransform>();
rectTransform.transform.parent = imageGameObject.transform;
rectTransform.sizeDelta = new Vector2(cursorTexture.width, cursorTexture.height);
rectTransform.pivot = new Vector2(0, 1); // Align to top left corner.

imageComponent = imageGameObject.AddComponent<Image>();
var imageComponent = imageGameObject.AddComponent<Image>();
imageComponent.sprite = Sprite.Create(
cursorTexture,
new Rect(0, 0, cursorTexture.width, cursorTexture.height),
Vector2.zero
);
imageComponent.raycastTarget = false;
return imageComponent;
}

private void CreateTextGameObject(GameObject parent, Texture2D cursorTexture) {
var textGameObject = new GameObject { transform = { parent = parent.transform } };
private TextMeshProUGUI CreateCursorText(GameObject parent, Vector3 offset) {
var textGameObject = new GameObject(name) { transform = { parent = parent.transform } };

var rectTransform = textGameObject.AddComponent<RectTransform>();
rectTransform.transform.parent = textGameObject.transform;
rectTransform.sizeDelta = new Vector2(50, 50);
rectTransform.pivot = new Vector2(0, 1); // Align to top left corner.
rectTransform.position = new Vector3(cursorTexture.width, -cursorTexture.height, 0);
rectTransform.position = offset;

textComponent = textGameObject.AddComponent<TextMeshProUGUI>();
var textComponent = textGameObject.AddComponent<TextMeshProUGUI>();
textComponent.fontSize = 14;
textComponent.font = Localization.FontAsset;
textComponent.color = Color.white;
textComponent.raycastTarget = false;
textComponent.enableWordWrapping = false;

return textComponent;
}

private void Update() {
if (!initialized)
protected override void OnForcedCleanUp() => subscription.Cancel();

private void OnPlayerCursorPositionUpdated(PlayerCursorPositionUpdatedEvent @event) {
if (@event.Player != assignedPlayer.Player)
return;

var otherClientScreen =
KScreenManager.Instance.screenStack.FirstOrDefault(screen => screen.GetType() == ScreenType);
var args = @event.MouseMovedEventArgs;

screenName = args.ScreenName;
screenType = args.ScreenType;
worldCursor.Trace(args.Position);
screenCursor.Trace(args.PositionWithinScreen);
}

private void Update() {
var screenStack = KScreenManager.Instance.screenStack;
var playerScreen = screenStack.FirstOrDefault(screen => screen.GetType() == screenType);

// If we see a screen where other player is - show cursor within that screen.
var showScreenOrWorldCursor =
CursorWithinScreen.CurrentPosition != null && (otherClientScreen?.isActive ?? false);
gameObject.transform.position = showScreenOrWorldCursor && ENABLE_CURSORS_RELATIVE_TO_GUI
? ScreenToWorld(otherClientScreen!, (Vector3) CursorWithinScreen.CurrentPosition!)
: camera.WorldToScreenPoint((Vector3) CursorWithinWorld.CurrentPosition!);
var screenUnderCursor = FindScreenUnderCursor(gameObject.transform.position);
var showScreeName = screenUnderCursor?.GetType() != ScreenType;
textComponent.text = PlayerName + (showScreeName ? $" ({ScreenName ?? "World"})" : "");
imageComponent.color = textComponent.color = showScreeName ? new Color(1, 1, 1, 0.5f) : Color.white;
var showScreenOrWorldCursor = screenCursor.CurrentPosition != null && (playerScreen?.isActive ?? false);
if (showScreenOrWorldCursor && ENABLE_CURSORS_RELATIVE_TO_GUI) {
if (screenCursor.CurrentPosition != null)
transform.position = ScreenToWorld(playerScreen!, screenCursor.CurrentPosition.Value);
} else {
if (worldCursor.CurrentPosition != null)
transform.position = camera.WorldToScreenPoint(worldCursor.CurrentPosition.Value);
}

var screenUnderCursor = FindScreenUnderCursor(transform.position);
var showScreenName = screenUnderCursor?.GetType() != screenType;

cursorText.text = playerName + (showScreenName ? $" ({screenName ?? "World"})" : "");
cursorImage.color = cursorText.color = showScreenName ? new Color(1, 1, 1, 0.5f) : Color.white;
}

private KScreen? FindScreenUnderCursor(Vector2 cursor) {
var eventSystem = UnityEngine.EventSystems.EventSystem.current;
if (eventSystem == null)
return null;

var eventData = new PointerEventData(eventSystem) {
position = cursor
};

var eventData = new PointerEventData(eventSystem) { position = cursor };
var results = new List<RaycastResult>();
UnityEngine.EventSystems.EventSystem.current.RaycastAll(eventData, results);
if (results.Count == 0) return null;
if (results.Count == 0)
return null;

return KScreenManager.Instance.screenStack.FirstOrDefault(
screen => IsParentOf(screen.gameObject, results[0].gameObject)
);
var raycastResult = results[0].gameObject;
var screenStack = KScreenManager.Instance.screenStack;

return screenStack.FirstOrDefault(screen => IsParentOf(screen.gameObject, raycastResult));
}

private static bool IsParentOf(GameObject potentialParent, GameObject potentialChild) {
var current = potentialChild.transform;
while (current != null) {
if (current.gameObject == potentialParent) {
if (current.gameObject == potentialParent)
return true;
}

current = current.parent;
}

return false;
}

private static Vector3 ScreenToWorld(KScreen screen, Vector3 pos) {
var screenRectTransform = screen.transform as RectTransform;
if (screenRectTransform == null) return Vector3.zero;
if (screenRectTransform == null)
return Vector3.zero;

return new Vector2(
screenRectTransform.position.x + pos.x * screenRectTransform.rect.width,
screenRectTransform.position.y + pos.y * screenRectTransform.rect.height
);
var position = screenRectTransform.position;
var rect = screenRectTransform.rect;

return new Vector2(position.x + pos.x * rect.width, position.y + pos.y * rect.height);
}

public class SmoothCursor {

private record TimedCursor(Vector2? Position, long Time);

private TimedCursor previous = null!;
private TimedCursor current = null!;
private TimedCursor previous;
private TimedCursor current;

public void SetPosition(Vector2? position) {
public SmoothCursor() {
var ticks = System.DateTime.Now.Ticks;
previous = new TimedCursor(position, ticks);
current = new TimedCursor(position, ticks);
previous = new TimedCursor(null, ticks);
current = new TimedCursor(null, ticks);
}

public void Trace(Vector2? position) {
Expand All @@ -161,13 +184,18 @@ public void Trace(Vector2? position) {

public Vector3? CurrentPosition {
get {
if (previous.Position == null) return current.Position;
if (current.Position == null) return null;
if (previous.Position == null)
return current.Position;

if (current.Position == null)
return null;

float updateDelta = current.Time - previous.Time;
var timeDiff = (System.DateTime.Now.Ticks - current.Time) / updateDelta;
return Vector2.Lerp((Vector2) previous.Position, (Vector2) current.Position, timeDiff);
}
}

}

}
54 changes: 24 additions & 30 deletions src/MultiplayerMod/Multiplayer/Components/CursorManager.cs
Original file line number Diff line number Diff line change
@@ -1,47 +1,41 @@
using System.Collections.Generic;
using MultiplayerMod.Core.Dependency;
using MultiplayerMod.Core.Dependency;
using MultiplayerMod.Core.Events;
using MultiplayerMod.Core.Extensions;
using MultiplayerMod.Core.Unity;
using MultiplayerMod.Multiplayer.Players;
using MultiplayerMod.Multiplayer.Players.Events;
using UnityEngine;

namespace MultiplayerMod.Multiplayer.Components;

public class CursorManager : MultiplayerMonoBehaviour {
public class CursorManager : MultiplayerKMonoBehaviour {

[InjectDependency] private readonly EventDispatcher eventDispatcher = null!;
[InjectDependency]
private readonly EventDispatcher events = null!;

private readonly Dictionary<MultiplayerPlayer, CursorComponent> cursors = new();
private EventSubscriptions subscriptions = null!;
[InjectDependency]
private readonly MultiplayerGame multiplayer = null!;

private void OnEnable() {
subscriptions = new EventSubscriptions()
.Add(eventDispatcher.Subscribe<PlayerCursorPositionUpdatedEvent>(OnCursorUpdated))
.Add(eventDispatcher.Subscribe<PlayerLeftEvent>(OnPlayerLeft));
}
private EventSubscription subscription = null!;

private void OnPlayerLeft(PlayerLeftEvent @event) {
Destroy(cursors[@event.Player]);
cursors.Remove(@event.Player);
protected override void OnSpawn() {
subscription = events.Subscribe<PlayerJoinedEvent>(OnPlayerJoined);
multiplayer.Players.ForEach(CreatePlayerCursor);
}

private void OnDisable() => subscriptions.Cancel();

private void OnCursorUpdated(PlayerCursorPositionUpdatedEvent updatedEvent) {
if (!cursors.TryGetValue(updatedEvent.Player, out var cursorComponent)) {
cursorComponent = gameObject.AddComponent<CursorComponent>();
cursorComponent.PlayerName = updatedEvent.Player.Profile.PlayerName;
cursorComponent.CursorWithinWorld.SetPosition(updatedEvent.MouseMovedEventArgs.Position);
cursorComponent.CursorWithinScreen.SetPosition(updatedEvent.MouseMovedEventArgs.PositionWithinScreen);
cursorComponent.ScreenName = updatedEvent.MouseMovedEventArgs.ScreenName;
cursorComponent.ScreenType = updatedEvent.MouseMovedEventArgs.ScreenType;
cursors[updatedEvent.Player] = cursorComponent;
private void OnPlayerJoined(PlayerJoinedEvent @event) => CreatePlayerCursor(@event.Player);

protected override void OnForcedCleanUp() => subscription.Cancel();

private void CreatePlayerCursor(MultiplayerPlayer player) {
if (player == multiplayer.Players.Current)
return;
}
cursorComponent.ScreenName = updatedEvent.MouseMovedEventArgs.ScreenName;
cursorComponent.ScreenType = updatedEvent.MouseMovedEventArgs.ScreenType;
cursorComponent.CursorWithinWorld.Trace(updatedEvent.MouseMovedEventArgs.Position);
cursorComponent.CursorWithinScreen.Trace(updatedEvent.MouseMovedEventArgs.PositionWithinScreen);
var canvas = GameScreenManager.Instance.GetParent(GameScreenManager.UIRenderTarget.ScreenSpaceOverlay);
var cursorName = $"{player.Profile.PlayerName}'s cursor";
var cursor = new GameObject(cursorName) { transform = { parent = canvas.transform } };
cursor.AddComponent<AssignedMultiplayerPlayer>().Player = player;
cursor.AddComponent<CursorComponent>();
cursor.AddComponent<DestroyOnPlayerLeave>();
}

}

0 comments on commit d623ef8

Please sign in to comment.