Skip to content

Commit

Permalink
added animations for remote players, code quality improvements
Browse files Browse the repository at this point in the history
  • Loading branch information
13on4rd committed Jan 28, 2025
1 parent 3717b43 commit 19e2bf5
Show file tree
Hide file tree
Showing 8 changed files with 397 additions and 111 deletions.
13 changes: 13 additions & 0 deletions Assets/Prefabs/Player/remote_player.prefab

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

4 changes: 2 additions & 2 deletions Assets/Scenes/Player/Player.unity

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

40 changes: 34 additions & 6 deletions Assets/Scripts/Multiplayer/EventManager.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
using System;
using UnityEngine;

/// <summary>
/// This class manages movement events by the player, trigger multiplayer communication.
/// </summary>
public class EventManager : MonoBehaviour
{
#region singelton
Expand All @@ -13,7 +17,6 @@ private void Awake()
if (Instance == null)
{
Instance = this;
//DontDestroyOnLoad(gameObject);
}
else
{
Expand All @@ -22,11 +25,36 @@ private void Awake()
}
#endregion

public delegate void DataChangedEventHandler(object sender, object data);
public static event DataChangedEventHandler OnPositionChanged;
//public delegate void DataChangedEventHandler(object sender, object data);
public event EventHandler<PositionChangedEventArgs> OnPositionChanged;

public void TriggerPositionChanged(Vector2 newPosition)
/// <summary>
/// Trigger that sends the players position and movement vector.
/// </summary>
/// <param name="newPosition">players new position</param>
/// <param name="movement">players normalized movement vector</param>
public void TriggerPositionChanged(Vector2 newPosition, Vector2 movement)
{
OnPositionChanged?.Invoke(this, new(newPosition, movement));
}
}

#region custom event argument
/// <summary>
/// Custom event for player movement und position.
/// </summary>
public class PositionChangedEventArgs : EventArgs
{
private Vector2 newPosition;
private Vector2 movement;

public PositionChangedEventArgs(Vector2 newPosition, Vector2 movement)
{
OnPositionChanged?.Invoke(this, newPosition);
this.newPosition = newPosition;
this.movement = movement;
}
}

public Vector2 GetPosition() { return newPosition; }
public Vector2 GetMovement() { return movement; }
}
#endregion
144 changes: 91 additions & 53 deletions Assets/Scripts/Multiplayer/MultiplayerManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,21 @@
using System.Threading.Tasks;
using UnityEngine;

/// <summary>
/// Manages the multiplayer communication with other clients.
/// Includes communication protocol definition, (de)serializing and the websocket connection.
/// Inlcudes,
/// </summary>
public class MultiplayerManager : MonoBehaviour
{
[DllImport("__Internal")]
private static extern string GetToken(string tokenName);
private WebSocket websocket;
private bool connected = false;
[SerializeField] private GameObject prefab;
[SerializeField] private GameObject parent;
private Dictionary<string, GameObject> connectedPlayers;
private string playerID;
[SerializeField] private GameObject remotePlayerParent;
private Dictionary<string, GameObject> connectedRemotePlayers;
private string playerId;

#region singelton
public static MultiplayerManager Instance { get; private set; }
Expand All @@ -27,7 +32,6 @@ private void Awake()
if (Instance == null)
{
Instance = this;
//DontDestroyOnLoad(gameObject);
}
else
{
Expand All @@ -37,32 +41,43 @@ private void Awake()
#endregion

#region event methods
/// <summary>
/// Subscribes to events used as triggers for communication.
/// </summary>
private void EnableEvents()
{
EventManager.OnPositionChanged += SendData;
EventManager.Instance.OnPositionChanged += SendData;
}

/// <summary>
/// Unsubscribes events when multiplayer is quit.
/// </summary>
private void DisableEvents()
{
EventManager.OnPositionChanged -= SendData;
EventManager.Instance.OnPositionChanged -= SendData;
}
#endregion

private void Start()
{
// get player id
try
{
playerID = GetToken("userId");
Debug.Log("player id:" + playerID);
playerId = GetToken("userId");
Debug.Log("player id:" + playerId);
}
catch (EntryPointNotFoundException e)
{
Debug.LogError("Function not found: " + e);
playerID = "c858aea9-a744-4709-a169-9df329fe4d96";

// use mock id for development
#if UNITY_EDITOR
playerId = "c858aea9-a744-4709-a169-9df329fe4d96";
#endif
}
}

void Update()
private void Update()
{
if (connected)
{
Expand All @@ -73,9 +88,8 @@ void Update()
}

/// <summary>
/// Creates a websocket for communication with the multiplayer server
/// Creates a websocket for communicating with the multiplayer server
/// </summary>
/// <returns></returns>
public async Task Initialize()
{
connected = true;
Expand All @@ -84,13 +98,13 @@ public async Task Initialize()
websocket.OnOpen += () =>
{
Debug.Log("Connection open!");
connectedPlayers = new Dictionary<string, GameObject>();
connectedRemotePlayers = new();
EnableEvents();
};

websocket.OnError += (e) =>
{
Debug.Log("Error! " + e);
Debug.Log("Error: " + e);
};

websocket.OnClose += (e) =>
Expand All @@ -103,7 +117,7 @@ public async Task Initialize()
{
try
{
UpdatePlayer(bytes);
UpdateRemotePlayer(DeserializePlayerData(bytes));
}
catch (Exception e)
{
Expand All @@ -114,30 +128,60 @@ public async Task Initialize()
// Keep sending messages at every
//InvokeRepeating(nameof(SendWebSocketMessage), 1.0f, 0.03f);

// waiting for messages
// wait for connection
await websocket.Connect();
}

/// <summary>
/// Sends the player's data to the server if one of its events is triggered
/// Sends the player's data to the server if one of the players events is triggered.
/// </summary>
/// <param name="newPosition">trigger event</param>
private async void SendData(object sender, object data)
/// <param name="data">triggering event</param>
private async void SendData(object sender, PositionChangedEventArgs data)
{
if (websocket.State == WebSocketState.Open)
{
Debug.Log(data.ToString());
if (data is Vector2 newPosition)
{
// Sending plain text
await websocket.Send(SerializePlayerData(playerID, 1, newPosition));
}
await websocket.Send(SerializePlayerData(playerId, 1, data.GetPosition(), data.GetMovement()));
}
}

private byte[] SerializePlayerData(string playerId, byte character, Vector2 position)
/// <summary>
/// Updates the remote player using received data.
/// </summary>
/// <param name="data">new data of hte remote player</param>
private void UpdateRemotePlayer(RemotePlayerData data)
{
byte[] data = new byte[25];
GameObject remotePlayerPrefab;

if (!connectedRemotePlayers.ContainsKey(data.GetId()))
{
remotePlayerPrefab = Instantiate(prefab, data.GetPosition(), Quaternion.identity, remotePlayerParent.transform);
connectedRemotePlayers.Add(data.GetId(), remotePlayerPrefab);
}

remotePlayerPrefab = connectedRemotePlayers[data.GetId()];

RemotePlayerAnimation remotePlayerAnimation = remotePlayerPrefab.GetComponent<RemotePlayerAnimation>();
remotePlayerAnimation.UpdatePosition(data.GetPosition(), data.GetMovement());

// TODO: add clipping mechanics

//Rigidbody2D rb = playerPrefab.GetComponent<Rigidbody2D>();

//rb.MovePosition(playerData.GetPosition());
}

#region (de)serializing
/// <summary>
/// Serializes data to be send with a custom communication protocol.
/// </summary>
/// <param name="playerId">unique id of the player</param>
/// <param name="character">unique character representation</param>
/// <param name="position">player position</param>
/// <param name="movement">normalized player movement vector</param>
/// <returns></returns>
private byte[] SerializePlayerData(string playerId, byte character, Vector2 position, Vector2 movement)
{
byte[] data = new byte[33];

// player id (16 Bytes)
Guid uuid = Guid.Parse(playerId);
Expand All @@ -150,45 +194,39 @@ private byte[] SerializePlayerData(string playerId, byte character, Vector2 posi
Buffer.BlockCopy(BitConverter.GetBytes(position.x), 0, data, 17, 4);
Buffer.BlockCopy(BitConverter.GetBytes(position.y), 0, data, 21, 4);

// movement (2 * 4 Byte)
Buffer.BlockCopy(BitConverter.GetBytes(movement.x), 0, data, 25, 4);
Buffer.BlockCopy(BitConverter.GetBytes(movement.y), 0, data, 29, 4);

// area id (2 Bytes)
//Buffer.BlockCopy(BitConverter.GetBytes(areaId.GetWorldIndex()), 0, data, 13, 1);
//Buffer.BlockCopy(BitConverter.GetBytes(areaId.GetDungeonIndex()), 0, data, 14, 1);

return data;
}

/// <summary>
/// Deserializes received data and converts it to an RemotePlayerData object.
/// </summary>
/// <param name="data">received message</param>
/// <returns>data of the remote player</returns>
private RemotePlayerData DeserializePlayerData(byte[] data)
{
// TODO drop message if wrong protocol
// TODO: discard message if wrong protocol

// guid
// extract uuid from message
#if !UNITY_EDITOR
byte[] playerIdBytes = new byte[16];
Buffer.BlockCopy(data, 0, playerIdBytes, 0, 16);
Guid playerId = new Guid(playerIdBytes);

return new RemotePlayerData(playerID.ToString(), new Vector2(BitConverter.ToSingle(data, 17), BitConverter.ToSingle(data, 21)));
}

private void UpdatePlayer(byte[] data)
{
RemotePlayerData playerData = DeserializePlayerData(data);
GameObject playerPrefab;

if (!connectedPlayers.ContainsKey(playerData.GetId()))
{
Debug.Log("creating new player");
playerPrefab = Instantiate(prefab, playerData.GetPosition(), Quaternion.identity, parent.transform);
connectedPlayers.Add(playerData.GetId(), playerPrefab);
}

playerPrefab = connectedPlayers[playerData.GetId()];

// TODO: add clipping mechanics

Rigidbody2D rb = playerPrefab.GetComponent<Rigidbody2D>();

rb.MovePosition(playerData.GetPosition());
Guid playerId = new(playerIdBytes);
#endif
return new RemotePlayerData(
playerId.ToString(),
new Vector2(BitConverter.ToSingle(data, 17), BitConverter.ToSingle(data, 21)),
new Vector2(BitConverter.ToSingle(data, 25), BitConverter.ToSingle(data, 29))
);
}
#endregion

private async void OnApplicationQuit()
{
Expand Down
Loading

0 comments on commit 19e2bf5

Please sign in to comment.