- More / better static Game properties - Instance now initialized on _EnterTree - Players moved in from MultiplayerManager - MultiplayerManager renamed to Multiplayer - LocalPlayer now static - Minor preparation for multiple workshops - Add Players script for easy enumerating of players and getting them by peer id - Simplify Player.PeerId and .IsLocal - Remove unnecessary Synchronizer script ("Player:velocity" property specified manually) - Add RpcMode and TransferMode enums to GlobalUsings - Add a bunch more extension functions - Add RPC class to send RPCs in a fancy, type-safe way Relating to item synchronization: - Grid now assumes Grid-local transforms etc - Item functions to pickup, place and throw - This is no longer handled by PickupController - ItemManager is used to relay RPCs due to a bugmain
parent
b1112f6006
commit
6edcb9c455
19 changed files with 540 additions and 231 deletions
@ -0,0 +1,14 @@ |
|||||||
|
public partial class Players : Node |
||||||
|
, IReadOnlyCollection<Player> |
||||||
|
{ |
||||||
|
public int Count |
||||||
|
=> GetChildCount(); |
||||||
|
|
||||||
|
public Player ByPeerId(int peerId) |
||||||
|
=> this.GetNodeOrThrow<Player>(peerId.ToString()); |
||||||
|
|
||||||
|
public IEnumerator<Player> GetEnumerator() |
||||||
|
=> GetChildren().Cast<Player>().GetEnumerator(); |
||||||
|
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() |
||||||
|
=> GetEnumerator(); |
||||||
|
} |
@ -1,27 +1,186 @@ |
|||||||
|
using Godot.NativeInterop; |
||||||
|
|
||||||
public partial class Item : RigidBody3D |
public partial class Item : RigidBody3D |
||||||
{ |
{ |
||||||
[Export] public Vector3I Size { get; set; } |
public MultiplayerSynchronizer Sync { get; internal set; } |
||||||
|
public ItemManager Manager { get; internal set; } = null; // TODO: Replace with Owner? |
||||||
|
public uint TrackingId { get; internal set; } |
||||||
|
|
||||||
|
/// <summary> Child node representing the 3D model of this item. </summary> |
||||||
|
public virtual Node3D Model => GetNode<Node3D>("Model"); |
||||||
|
|
||||||
|
/// <summary> Whether this item is attached to a grid. </summary> |
||||||
|
public bool IsAttached => GetParent() is Grid; |
||||||
|
|
||||||
public virtual Node3D Model |
/// <summary> Size of the item in grid spaces. </summary> |
||||||
=> GetNode<Node3D>("Model"); |
[Export] public Vector3I Size { get; set; } |
||||||
|
|
||||||
public override void _Ready() |
public override void _Ready() |
||||||
{ |
{ |
||||||
|
// Set the collision properties here so we don't have to specify them in each item scene separately. |
||||||
|
CollisionLayer = (uint)(PhysicsLayer.Item | PhysicsLayer.Pickup); |
||||||
|
CollisionMask = (uint)(PhysicsLayer.Static | PhysicsLayer.Dynamic | PhysicsLayer.Player | PhysicsLayer.Item); |
||||||
|
|
||||||
// TODO: Find a better way to better import models with colliders. |
// TODO: Find a better way to better import models with colliders. |
||||||
// TODO: Import items dynamically at runtime? |
// TODO: Import items dynamically at runtime? |
||||||
// TODO: Use PostImport tool script? |
// TODO: Use PostImport tool script? |
||||||
foreach (var body in FindChildren("*", "StaticBody3D").Cast<StaticBody3D>()) { |
foreach (var body in FindChildren("*", "StaticBody3D").Cast<StaticBody3D>()) { |
||||||
foreach (var shape in body.GetChildren().OfType<CollisionShape3D>()) { |
foreach (var shape in body.GetChildren().OfType<CollisionShape3D>()) |
||||||
body.RemoveChild(shape); |
shape.Reparent(this); |
||||||
AddChild(shape); |
|
||||||
} |
|
||||||
body.GetParent().RemoveChild(body); |
body.GetParent().RemoveChild(body); |
||||||
} |
} |
||||||
|
|
||||||
// Set the collision properties here so we don't have to specify them in each item scene separately. |
// Set up syncronization for this item when its physics are enabled. |
||||||
CollisionLayer = (uint)(PhysicsLayer.Item | PhysicsLayer.Pickup); |
// Sync = new() { RootPath = ".." }; |
||||||
CollisionMask = (uint)(PhysicsLayer.Static | PhysicsLayer.Dynamic | PhysicsLayer.Player | PhysicsLayer.Item); |
// var config = Sync.ReplicationConfig = new(); |
||||||
|
// config.AddProperty(":position"); |
||||||
|
// config.AddProperty(":rotation"); |
||||||
|
// config.AddProperty(":linear_velocity"); |
||||||
|
// config.AddProperty(":angular_velocity"); |
||||||
|
// AddChild(Sync); |
||||||
|
|
||||||
|
UpdatePhysicsState(); |
||||||
|
} |
||||||
|
|
||||||
|
public override void _Notification(int what) |
||||||
|
{ |
||||||
|
switch ((long)what) { |
||||||
|
case NotificationParented: |
||||||
|
if (IsInsideTree()) |
||||||
|
UpdatePhysicsState(); |
||||||
|
break; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
void UpdatePhysicsState() |
||||||
|
{ |
||||||
|
Freeze = IsAttached; |
||||||
|
// Sync.PublicVisibility = !IsAttached; |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
public void TryPickup() |
||||||
|
{ |
||||||
|
if (this.IsAuthority()) |
||||||
|
RPC.ToAll().Send(Manager.RelayAccept, GetPath(), |
||||||
|
ItemManager.AcceptFunc.AcceptPickup, |
||||||
|
new Godot.Collections.Array { Multiplayer.GetUniqueId() }); |
||||||
|
// RPC.ToAll().Send(AcceptPickup, Multiplayer.GetUniqueId()); |
||||||
|
else |
||||||
|
RPC.To(GetMultiplayerAuthority()).Send(Manager.RelayRequest, GetPath(), |
||||||
|
ItemManager.RequestFunc.RequestPickup, |
||||||
|
new Godot.Collections.Array()); |
||||||
|
// RPC.To(GetMultiplayerAuthority()).Send(RequestPickup); |
||||||
|
} |
||||||
|
|
||||||
|
[Rpc(RpcMode.AnyPeer)] |
||||||
|
void RequestPickup() |
||||||
|
{ |
||||||
|
// Pickup will be requested by any player to the owner of the item. |
||||||
|
// If this message is received by anyone else, ignore it (for now). |
||||||
|
if (!IsMultiplayerAuthority()) return; |
||||||
|
|
||||||
|
var player = Multiplayer.GetRemoteSenderPlayer(); |
||||||
|
if (player.Pickup.HasItemsHeld) return; |
||||||
|
// TODO: Check if player is in range. |
||||||
|
|
||||||
|
RPC.ToAll().Send(Manager.RelayAccept, GetPath(), |
||||||
|
ItemManager.AcceptFunc.AcceptPickup, |
||||||
|
new Godot.Collections.Array { player.PeerId }); |
||||||
|
// RPC.ToAll().Send(AcceptPickup, player.PeerId); |
||||||
|
} |
||||||
|
|
||||||
|
[Rpc(CallLocal = true)] |
||||||
|
void AcceptPickup(int peerId) |
||||||
|
{ |
||||||
|
var player = Game.Players.ByPeerId(peerId); |
||||||
|
Reparent(player.Pickup); |
||||||
|
Transform = Transform3D.Identity; // TODO: Rotate item? |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
public void TryPlace(Grid grid, Transform3D localTransform) |
||||||
|
{ |
||||||
|
if (this.IsAuthority()) |
||||||
|
RPC.ToAll().Send(Manager.RelayAccept, GetPath(), |
||||||
|
ItemManager.AcceptFunc.AcceptPlace, |
||||||
|
new Godot.Collections.Array { grid.GetPath(), localTransform }); |
||||||
|
// RPC.ToAll().Send(AcceptPlace, grid.GetPath(), localTransform); |
||||||
|
else |
||||||
|
RPC.To(GetMultiplayerAuthority()).Send(Manager.RelayRequest, GetPath(), |
||||||
|
ItemManager.RequestFunc.RequestPlace, |
||||||
|
new Godot.Collections.Array { grid.GetPath(), localTransform }); |
||||||
|
// RPC.To(GetMultiplayerAuthority()).Send(RequestPlace, grid.GetPath(), localTransform); |
||||||
|
} |
||||||
|
|
||||||
|
[Rpc(RpcMode.AnyPeer)] |
||||||
|
void RequestPlace(NodePath gridPath, Transform3D localTransform) |
||||||
|
{ |
||||||
|
// Must be received by the owner of this item. |
||||||
|
if (!IsMultiplayerAuthority()) return; |
||||||
|
|
||||||
|
if (GetNodeOrNull(gridPath) is not Grid grid) return; |
||||||
|
// Item and Grid must be owned by the same peer. |
||||||
|
if (!grid.IsMultiplayerAuthority()) return; |
||||||
|
if (!grid.CanPlaceAt(this, localTransform)) return; |
||||||
|
|
||||||
|
RPC.ToAll().Send(Manager.RelayAccept, GetPath(), |
||||||
|
ItemManager.AcceptFunc.AcceptPlace, |
||||||
|
new Godot.Collections.Array { gridPath, localTransform }); |
||||||
|
// RPC.ToAll().Send(AcceptPlace, gridPath, localTransform); |
||||||
|
} |
||||||
|
|
||||||
|
[Rpc(CallLocal = true)] |
||||||
|
void AcceptPlace(NodePath gridPath, Transform3D localTransform) |
||||||
|
{ |
||||||
|
if (GetNodeOrNull(gridPath) is not Grid grid) return; |
||||||
|
// Item and Grid must be owned by the same peer. |
||||||
|
if (grid.GetMultiplayerAuthority() != GetMultiplayerAuthority()) return; |
||||||
|
|
||||||
|
Reparent(grid); |
||||||
|
Transform = localTransform; |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
public void TryThrow() |
||||||
|
{ |
||||||
|
if (this.IsAuthority()) |
||||||
|
RPC.ToAll().Send(Manager.RelayAccept, GetPath(), |
||||||
|
ItemManager.AcceptFunc.AcceptThrow, |
||||||
|
new Godot.Collections.Array()); |
||||||
|
// RPC.ToAll().Send(AcceptThrow); |
||||||
|
else |
||||||
|
RPC.To(GetMultiplayerAuthority()).Send(Manager.RelayRequest, GetPath(), |
||||||
|
ItemManager.RequestFunc.RequestThrow, |
||||||
|
new Godot.Collections.Array()); |
||||||
|
// RPC.To(GetMultiplayerAuthority()).Send(RequestThrow); |
||||||
|
} |
||||||
|
|
||||||
|
[Rpc(RpcMode.AnyPeer)] |
||||||
|
void RequestThrow() |
||||||
|
{ |
||||||
|
if (!IsMultiplayerAuthority()) return; |
||||||
|
|
||||||
|
var player = Multiplayer.GetRemoteSenderPlayer(); |
||||||
|
if (this.FindParentOrNull<Player>() != player) return; |
||||||
|
|
||||||
|
RPC.ToAll().Send(Manager.RelayAccept, GetPath(), |
||||||
|
ItemManager.AcceptFunc.AcceptThrow, |
||||||
|
new Godot.Collections.Array()); |
||||||
|
// RPC.ToAll().Send(AcceptThrow); |
||||||
|
} |
||||||
|
|
||||||
|
[Rpc(CallLocal = true)] |
||||||
|
void AcceptThrow() |
||||||
|
{ |
||||||
|
var player = this.FindParentOrThrow<Player>(); |
||||||
|
var world = Manager.GetParent<Node3D>(); |
||||||
|
|
||||||
|
Reparent(world, true); |
||||||
|
|
||||||
Freeze = FindParent("Grid") != null; |
// Throw item forward and up a bit. |
||||||
|
var basis = player.Camera.Camera.GlobalBasis; |
||||||
|
var direction = -basis.Z + basis.Y; |
||||||
|
ApplyImpulse(direction * 2); |
||||||
} |
} |
||||||
} |
} |
||||||
|
@ -1,12 +0,0 @@ |
|||||||
public partial class Synchronizer : MultiplayerSynchronizer |
|
||||||
{ |
|
||||||
// Required because `Velocity` can't be synchronized automatically. |
|
||||||
[Export] public Vector3 PlayerVelocity { |
|
||||||
get => _player.Velocity; |
|
||||||
set => _player.Velocity = value; |
|
||||||
} |
|
||||||
|
|
||||||
Player _player; |
|
||||||
public override void _Ready() |
|
||||||
=> _player = GetParent<Player>(); |
|
||||||
} |
|
@ -0,0 +1,62 @@ |
|||||||
|
public partial class ItemManager : Node |
||||||
|
{ |
||||||
|
public Dictionary<uint, Item> TrackedItems { get; } = []; |
||||||
|
|
||||||
|
public override void _Ready() |
||||||
|
{ |
||||||
|
var items = GetParent().FindChildren("*", "res://objects/Item.cs", owned: false).Cast<Item>(); |
||||||
|
foreach (var item in items) Add(item); |
||||||
|
} |
||||||
|
|
||||||
|
public void Add(Item item) |
||||||
|
{ |
||||||
|
if (item.Manager != null) throw new ArgumentException( |
||||||
|
$"Item '{item.GetPath()}' is already part of another scene"); |
||||||
|
|
||||||
|
// Generate a random, unused id for this item. |
||||||
|
uint id; do { id = Randi(); } while (TrackedItems.ContainsKey(id)); |
||||||
|
TrackedItems.Add(id, item); |
||||||
|
|
||||||
|
item.Manager = this; |
||||||
|
item.TrackingId = id; |
||||||
|
} |
||||||
|
|
||||||
|
public void Remove(Item item) |
||||||
|
{ |
||||||
|
if (item.Manager != this) throw new ArgumentException( |
||||||
|
$"Item '{item.GetPath()}' is not part of this scene"); |
||||||
|
|
||||||
|
TrackedItems.Remove(item.TrackingId); |
||||||
|
|
||||||
|
item.Manager = null; |
||||||
|
item.TrackingId = 0; |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
// FIXME: This should be fixed in 4.3 and thus not required anymore? |
||||||
|
// https://github.com/godotengine/godot/issues/85883 |
||||||
|
|
||||||
|
internal enum RequestFunc { RequestPickup, RequestPlace, RequestThrow } |
||||||
|
internal enum AcceptFunc { AcceptPickup, AcceptPlace, AcceptThrow } |
||||||
|
|
||||||
|
[Rpc(RpcMode.AnyPeer)] |
||||||
|
internal void RelayRequest(NodePath itemPath, RequestFunc func, Godot.Collections.Array args) |
||||||
|
{ |
||||||
|
var item = this.GetNodeOrThrow<Item>(itemPath); |
||||||
|
var callable = new Callable(item, func.ToString()); |
||||||
|
callable.Call(args.ToArray()); |
||||||
|
} |
||||||
|
|
||||||
|
[Rpc(CallLocal = true)] |
||||||
|
internal void RelayAccept(NodePath itemPath, AcceptFunc func, Godot.Collections.Array args) |
||||||
|
{ |
||||||
|
var item = this.GetNodeOrThrow<Item>(itemPath); |
||||||
|
|
||||||
|
if (GetMultiplayerAuthority() != item.GetMultiplayerAuthority()) |
||||||
|
throw new InvalidOperationException( |
||||||
|
$"Item {item.GetPath()} not owned by correct player"); |
||||||
|
|
||||||
|
var callable = new Callable(item, func.ToString()); |
||||||
|
callable.Call(args.ToArray()); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,5 @@ |
|||||||
|
public static class MultiplayerExtensions |
||||||
|
{ |
||||||
|
public static Player GetRemoteSenderPlayer(this MultiplayerApi self) |
||||||
|
=> Game.Players.ByPeerId(self.GetRemoteSenderId()); |
||||||
|
} |
@ -0,0 +1,44 @@ |
|||||||
|
public static class NodeExtensions |
||||||
|
{ |
||||||
|
/// <summary> |
||||||
|
/// Similar to <see cref="Node.GetNodeOrNull{T}"/>, but throws a |
||||||
|
/// <see cref="InvalidOperationException"/> if the node at the given |
||||||
|
/// path does not exist, or a <see cref="InvalidCastException"/> if |
||||||
|
/// it is not of the correct type. |
||||||
|
/// </summary> |
||||||
|
public static T GetNodeOrThrow<T>(this Node self, NodePath path) |
||||||
|
where T : class
|
||||||
|
{ |
||||||
|
var result = self.GetNodeOrNull(path); |
||||||
|
if (result == null) throw new InvalidOperationException( |
||||||
|
$"Node at '{path}' not found"); |
||||||
|
if (result is not T resultOfType) throw new InvalidCastException( |
||||||
|
$"Node at '{path}' is {result.GetType()}, expected {typeof(T)}"); |
||||||
|
return resultOfType; |
||||||
|
} |
||||||
|
|
||||||
|
/// <summary> |
||||||
|
/// Finds the first parent of this node that is of |
||||||
|
/// type <paramref name="T"/> or null if not found. |
||||||
|
/// </summary> |
||||||
|
public static T FindParentOrNull<T>(this Node self) |
||||||
|
where T : class
|
||||||
|
{ |
||||||
|
while ((self = self.GetParent()) != null) |
||||||
|
if (self is T result) return result; |
||||||
|
return null; |
||||||
|
} |
||||||
|
|
||||||
|
/// <summary> |
||||||
|
/// Finds the first parent of this node that is of type <paramref name="T"/> |
||||||
|
/// or throws a <see cref="InvalidOperationException"/> if not found. |
||||||
|
/// </summary> |
||||||
|
public static T FindParentOrThrow<T>(this Node self) |
||||||
|
where T : class
|
||||||
|
=> self.FindParentOrNull<T>() ?? throw new InvalidOperationException( |
||||||
|
$"No parent node of type {typeof(T)} found in hierarchy of '{self.GetPath()}'"); |
||||||
|
|
||||||
|
/// <summary> Returns whether the current peer has authority over this node. </summary> |
||||||
|
public static bool IsAuthority(this Node self) |
||||||
|
=> self.Multiplayer.GetUniqueId() == self.GetMultiplayerAuthority(); |
||||||
|
} |
@ -0,0 +1,55 @@ |
|||||||
|
public class RPC |
||||||
|
{ |
||||||
|
readonly IEnumerable<int> _targets; // null for "everyone" |
||||||
|
IEnumerable<int> _except = null; |
||||||
|
|
||||||
|
RPC(IEnumerable<int> targets) => _targets = targets; |
||||||
|
|
||||||
|
public static RPC ToAll() => new(null); |
||||||
|
|
||||||
|
public static RPC To(IEnumerable<int> peerIds) => new(peerIds); |
||||||
|
public static RPC To(params int[] peerIds) => To((IEnumerable<int>)peerIds); |
||||||
|
public static RPC To(int peerId) => To([ peerId ]); |
||||||
|
|
||||||
|
public static RPC To(IEnumerable<Player> players) => To(players.Select(p => p.PeerId)); |
||||||
|
public static RPC To(params Player[] players) => To((IEnumerable<Player>)players); |
||||||
|
public static RPC To(Player player) => To(player.PeerId); |
||||||
|
|
||||||
|
public RPC Except(IEnumerable<int> peerIds) { _except = peerIds; return this; } |
||||||
|
public RPC Except(params int[] peerIds) => Except((IEnumerable<int>)peerIds); |
||||||
|
public RPC Except(int peerId) => Except([ peerId ]); |
||||||
|
|
||||||
|
public RPC Except(IEnumerable<Player> players) => Except(players.Select(p => p.PeerId)); |
||||||
|
public RPC Except(params Player[] players) => Except((IEnumerable<Player>)players); |
||||||
|
public RPC Except(Player player) => Except(player.PeerId); |
||||||
|
|
||||||
|
public void Send(Action action) |
||||||
|
=> SendInternal(GetNode(action), action.Method.Name, []); |
||||||
|
public void Send<[MustBeVariant] T>(Action<T> action, T arg) |
||||||
|
=> SendInternal(GetNode(action), action.Method.Name, Variant.From(arg)); |
||||||
|
public void Send<[MustBeVariant] T0, [MustBeVariant] T1>(Action<T0, T1> action, T0 arg0, T1 arg1) |
||||||
|
=> SendInternal(GetNode(action), action.Method.Name, Variant.From(arg0), Variant.From(arg1)); |
||||||
|
public void Send<[MustBeVariant] T0, [MustBeVariant] T1, [MustBeVariant] T2>(Action<T0, T1, T2> action, T0 arg0, T1 arg1, T2 arg2) |
||||||
|
=> SendInternal(GetNode(action), action.Method.Name, Variant.From(arg0), Variant.From(arg1), Variant.From(arg2)); |
||||||
|
public void Send<[MustBeVariant] T0, [MustBeVariant] T1, [MustBeVariant] T2, [MustBeVariant] T3>(Action<T0, T1, T2, T3> action, T0 arg0, T1 arg1, T2 arg2, T3 arg3) |
||||||
|
=> SendInternal(GetNode(action), action.Method.Name, Variant.From(arg0), Variant.From(arg1), Variant.From(arg2), Variant.From(arg3)); |
||||||
|
|
||||||
|
|
||||||
|
/// <summary> Returns the target of the instance method <paramref name="action"/> as a <see cref="Node"/>. </summary> |
||||||
|
static Node GetNode(Delegate action) |
||||||
|
=> (action.Target as Node) ?? throw new ArgumentException( |
||||||
|
$"Target ({action.Target?.GetType().ToString() ?? "null"}) must be a Node", nameof(action)); |
||||||
|
|
||||||
|
void SendInternal(Node node, StringName method, params Variant[] args) |
||||||
|
{ |
||||||
|
if ((_targets == null) && (_except == null)) |
||||||
|
// TODO: Check if RpcId is recommended for client -> server messages over just Rpc. |
||||||
|
node.Rpc(method, args); |
||||||
|
else { |
||||||
|
var targets = _targets ?? node.Multiplayer.GetPeers(); |
||||||
|
if (_except != null) targets = targets.Except(_except); |
||||||
|
foreach (var target in targets) |
||||||
|
node.RpcId(target, method, args); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
Loading…
Reference in new issue