Compare commits

...

6 Commits

  1. 6
      game.tscn
  2. 126
      objects/Item.cs
  3. 28
      player/AnimationController.cs
  4. 7
      player/PickupController.cs
  5. 1
      player/Player.cs
  6. 156
      scenes/ItemManager.cs
  7. 0
      scripts/Players.cs
  8. 1
      scripts/globals/RPC.cs

@ -4,7 +4,7 @@
[ext_resource type="Script" path="res://Game.cs" id="1_uywdd"]
[ext_resource type="PackedScene" uid="uid://dmd7w2r8s0x6y" path="res://player/player.tscn" id="2_iv2f7"]
[ext_resource type="PackedScene" uid="uid://bwfuet1irfi17" path="res://scenes/workshop.tscn" id="3_4u5ql"]
[ext_resource type="Script" path="res://Players.cs" id="4_l8q75"]
[ext_resource type="Script" path="res://scripts/Players.cs" id="4_l8q75"]
[ext_resource type="Material" uid="uid://c0q35rri3vb07" path="res://assets/shaders/outline_material.tres" id="5_a3fxj"]
[ext_resource type="Script" path="res://scripts/OutlineCamera.cs" id="5_qpc14"]
[ext_resource type="PackedScene" uid="uid://c5ooi36ibspfo" path="res://ui/menu.tscn" id="6_ol0j5"]
@ -21,11 +21,11 @@ PlayerScene = ExtResource("2_iv2f7")
[node name="Players" type="Node" parent="."]
script = ExtResource("4_l8q75")
[node name="Local" parent="Players" instance=ExtResource("2_iv2f7")]
[node name="1" parent="Players" instance=ExtResource("2_iv2f7")]
[node name="Workshops" type="Node" parent="."]
[node name="Workshop" parent="Workshops" instance=ExtResource("3_4u5ql")]
[node name="1" parent="Workshops" instance=ExtResource("3_4u5ql")]
[node name="OutlineViewportContainer" type="SubViewportContainer" parent="."]
material = ExtResource("5_a3fxj")

@ -57,130 +57,4 @@ public partial class Item : RigidBody3D
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);
// Throw item forward and up a bit.
var basis = player.Camera.Camera.GlobalBasis;
var direction = -basis.Z + basis.Y;
ApplyImpulse(direction * 2);
}
}

@ -124,6 +124,10 @@ public partial class AnimationController : Node3D
}
}
float _yawFactorLowerBody = 0.15f;
float _yawFactorUpperBody = 0.20f;
float _yawFactorNeck = 0.45f;
void HandleWalkingAnimation(double delta)
{
const float ForwardAngle = 95.0f;
@ -146,9 +150,19 @@ public partial class AnimationController : Node3D
var prevWalkSpeed = (float)_animTree.Get(WalkSpeedParam);
_animTree.Set(WalkSpeedParam, Lerp(prevWalkSpeed, walkSpeed, 10 * (float)delta));
const float YawFactorLowerBody = 0.15f;
const float YawFactorUpperBody = 0.20f;
const float YawFactorNeck = 0.45f;
const float EmptyYawFactorLowerBody = 0.15f;
const float EmptyYawFactorUpperBody = 0.20f;
const float EmptyYawFactorNeck = 0.45f;
const float HoldingYawFactorLowerBody = 0.35f;
const float HoldingYawFactorUpperBody = 0.25f;
const float HoldingYawFactorNeck = 0.10f;
// TODO: This should probably be using delta somehow, but I'm not sure how.
var held = _player.Pickup.HasItemsHeld;
_yawFactorLowerBody = (_yawFactorLowerBody + (held ? HoldingYawFactorLowerBody : EmptyYawFactorLowerBody)) / 2;
_yawFactorUpperBody = (_yawFactorLowerBody + (held ? HoldingYawFactorUpperBody : EmptyYawFactorUpperBody)) / 2;
_yawFactorNeck = (_yawFactorLowerBody + (held ? HoldingYawFactorNeck : EmptyYawFactorNeck )) / 2;
if (movement.IsMoving) {
var targetBodyYaw = localAngle;
@ -157,10 +171,10 @@ public partial class AnimationController : Node3D
} else
_bodyYaw -= _bodyYaw * (float)delta * 2;
_bones["Root" ].GlobalRotate(Vector3.Up, _bodyYaw * (YawFactorLowerBody + YawFactorUpperBody + YawFactorNeck));
_bones["LowerBody"].GlobalRotate(Vector3.Up, -_bodyYaw * YawFactorLowerBody);
_bones["UpperBody"].GlobalRotate(Vector3.Up, -_bodyYaw * YawFactorUpperBody);
_bones["Neck" ].GlobalRotate(Vector3.Up, -_bodyYaw * YawFactorNeck);
_bones["Root" ].GlobalRotate(Vector3.Up, _bodyYaw * (_yawFactorLowerBody + _yawFactorUpperBody + _yawFactorNeck));
_bones["LowerBody"].GlobalRotate(Vector3.Up, -_bodyYaw * _yawFactorLowerBody);
_bones["UpperBody"].GlobalRotate(Vector3.Up, -_bodyYaw * _yawFactorUpperBody);
_bones["Neck" ].GlobalRotate(Vector3.Up, -_bodyYaw * _yawFactorNeck);
}
Vector3? _defaultPickupPosition;

@ -39,7 +39,7 @@ public partial class PickupController : Node3D
if (@event.IsActionPressed("interact_pickup")
&& (TargetedItem != null) && !HasItemsHeld)
{
TargetedItem.TryPickup();
TargetedItem.Manager.TryPickup(TargetedItem);
GetViewport().SetInputAsHandled();
}
@ -48,9 +48,10 @@ public partial class PickupController : Node3D
{
if (_preview.Visible && (_targetedGrid != null)) {
var inverse = _targetedGrid.GlobalTransform.AffineInverse();
HeldItem.TryPlace(_targetedGrid, inverse * _preview.GlobalTransform);
HeldItem.Manager.TryPlace(HeldItem,
_targetedGrid, inverse * _preview.GlobalTransform);
} else
HeldItem.TryThrow();
HeldItem.Manager.TryThrow(HeldItem);
GetViewport().SetInputAsHandled();
}
}

@ -3,7 +3,6 @@ public partial class Player : CharacterBody3D
public int PeerId => GetMultiplayerAuthority();
public bool IsLocal => this.IsAuthority();
// TODO: Add "Controller" suffix to these.
public MovementController Movement { get; private set; }
public CameraController Camera { get; private set; }
public AnimationController Animation { get; private set; }

@ -1,5 +1,6 @@
public partial class ItemManager : Node
{
uint _trackingIdCounter = 0;
public Dictionary<uint, Item> TrackedItems { get; } = [];
public override void _Ready()
@ -13,8 +14,10 @@ public partial class ItemManager : Node
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));
// FIXME: Synchronize items so we can get rid of this temporary fix.
// // Generate a random, unused id for this item.
// uint id; do { id = Randi(); } while (TrackedItems.ContainsKey(id));
var id = _trackingIdCounter++;
TrackedItems.Add(id, item);
item.Manager = this;
@ -33,30 +36,147 @@ public partial class ItemManager : Node
}
// FIXME: This should be fixed in 4.3 and thus not required anymore?
// https://github.com/godotengine/godot/issues/85883
public void TryPickup(Item item)
{
VerifyManager(item);
VerifyAuthority(item, "Item");
RPC.To(item.GetMultiplayerAuthority()).Send(RequestPickup, item.TrackingId);
}
[Rpc(RpcMode.AnyPeer, CallLocal = true)]
void RequestPickup(uint itemTrackingId)
{
// TODO: Print debug messages instead of just returning?
// Pickup will be requested by any player to the owner of the item.
// If this message is received by anyone else, ignore it.
if (!IsMultiplayerAuthority()) return;
// Find the item being requested to be picked up, return if not found.
if (!TrackedItems.TryGetValue(itemTrackingId, out var item)) return;
// Make sure that the item shares its authority with this Manager.
if (!item.IsMultiplayerAuthority()) return;
var player = Multiplayer.GetRemoteSenderPlayer();
if (player.Pickup.HasItemsHeld) return;
// TODO: Check if player is in range.
// Have the item be rotated in the player's hands depending on how it's currently rotated.
var localBasis = player.Pickup.GlobalBasis.Inverse() * item.GlobalBasis;
var localTransform = new Transform3D(localBasis, Vector3.Zero);
RPC.ToAll().Send(AcceptPickup, itemTrackingId, player.PeerId, localTransform);
}
[Rpc(CallLocal = true)]
void AcceptPickup(uint itemTrackingId, int playerPeerId, Transform3D localTransform)
{
var item = VerifyAuthority(TrackedItems[itemTrackingId], "Item");
var player = Game.Players.ByPeerId(playerPeerId);
item.Reparent(player.Pickup);
item.Transform = localTransform;
}
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)
public void TryPlace(Item item, Grid grid, Transform3D localTransform)
{
var item = this.GetNodeOrThrow<Item>(itemPath);
var callable = new Callable(item, func.ToString());
callable.Call(args.ToArray());
VerifyManager(item);
VerifyAuthority(item, "Item");
RPC.To(item.GetMultiplayerAuthority()).Send(RequestPlace,
item.TrackingId, grid.GetPath(), localTransform);
}
[Rpc(RpcMode.AnyPeer, CallLocal = true)]
void RequestPlace(uint itemTrackingId, NodePath gridPath, Transform3D localTransform)
{
if (!IsMultiplayerAuthority()) return;
if (!TrackedItems.TryGetValue(itemTrackingId, out var item)) return;
if (!item.IsMultiplayerAuthority()) return;
var player = Multiplayer.GetRemoteSenderPlayer();
// Ensure that the item is currently being held by the player.
if (item.FindParentOrNull<Player>() != player) return;
if (GetNodeOrNull(gridPath) is not Grid grid) return;
if (!grid.IsMultiplayerAuthority()) return;
// TODO: Further verify localTransform.
if (!grid.CanPlaceAt(item, localTransform)) return;
RPC.ToAll().Send(AcceptPlace, itemTrackingId, gridPath, localTransform);
}
[Rpc(CallLocal = true)]
internal void RelayAccept(NodePath itemPath, AcceptFunc func, Godot.Collections.Array args)
void AcceptPlace(uint itemTrackingId, NodePath gridPath, Transform3D localTransform)
{
var item = this.GetNodeOrThrow<Item>(itemPath);
var item = VerifyAuthority(TrackedItems[itemTrackingId], "Item");
var grid = VerifyAuthority(this.GetNodeOrThrow<Grid>(gridPath), "Grid");
if (GetMultiplayerAuthority() != item.GetMultiplayerAuthority())
throw new InvalidOperationException(
$"Item {item.GetPath()} not owned by correct player");
item.Reparent(grid);
item.Transform = localTransform;
}
var callable = new Callable(item, func.ToString());
callable.Call(args.ToArray());
public void TryThrow(Item item)
{
VerifyManager(item);
VerifyAuthority(item, "Item");
RPC.To(item.GetMultiplayerAuthority()).Send(RequestThrow, item.TrackingId);
}
[Rpc(RpcMode.AnyPeer, CallLocal = true)]
void RequestThrow(uint itemTrackingId)
{
if (!IsMultiplayerAuthority()) return;
if (!TrackedItems.TryGetValue(itemTrackingId, out var item)) return;
if (!item.IsMultiplayerAuthority()) return;
var player = Multiplayer.GetRemoteSenderPlayer();
// Ensure that the item is currently being held by the player.
if (item.FindParentOrNull<Player>() != player) return;
RPC.ToAll().Send(AcceptThrow, itemTrackingId);
}
[Rpc(CallLocal = true)]
void AcceptThrow(uint itemTrackingId)
{
var item = VerifyAuthority(TrackedItems[itemTrackingId], "Item");
var player = item.FindParentOrThrow<Player>();
var world = GetParent<Node3D>();
item.Reparent(world, true);
// Throw item forward and up a bit.
var basis = player.Camera.Camera.GlobalBasis;
var direction = -basis.Z + basis.Y;
item.ApplyImpulse(direction * 2);
}
/// <summary>
/// Verifies that this item is managed by this
/// Manager instance, throwing an exception if not.
/// </summary>
void VerifyManager(Item item)
{
if (item.Manager != this) throw new InvalidOperationException(
$"Item manager ({item.Manager?.GetPath().ToString() ?? "null"}) is not this manager ({GetPath()})");
}
/// <summary>
/// Verifies that this node has the same multiplayer authority as this
/// Manager instance, throwing an exception if not. Returns the node
/// that was passed in.
/// </summary>
T VerifyAuthority<T>(T node, string name)
where T : Node
{
var expected = GetMultiplayerAuthority();
var actual = node.GetMultiplayerAuthority();
if (actual != expected) throw new InvalidOperationException(
$"{name} authority ({actual}) is not manager authority ({expected})");
return node;
}
}

@ -43,7 +43,6 @@ public class RPC
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();

Loading…
Cancel
Save