Everything related to chat functionality, and updating the projectile prefab to rotate properly
This commit is contained in:
parent
d92d00c83f
commit
66f84652dc
14 changed files with 1133 additions and 121 deletions
159
Assets/_Project/Scripts/Gameplay/ChatService.cs
Normal file
159
Assets/_Project/Scripts/Gameplay/ChatService.cs
Normal file
|
|
@ -0,0 +1,159 @@
|
|||
// Assets/_Project/Scripts/Gameplay/ChatService.cs
|
||||
using Unity.Collections;
|
||||
using Unity.Netcode;
|
||||
using UnityEngine;
|
||||
using TD.Core;
|
||||
|
||||
namespace TD.Gameplay
|
||||
{
|
||||
/// <summary>
|
||||
/// Networked chat singleton. Carries player-typed messages between peers and
|
||||
/// exposes a local-only entry point for system messages (life lost, income
|
||||
/// changes, etc.). UI consumers subscribe to <see cref="OnMessageReceived"/>
|
||||
/// to display the feed.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <b>Authority model.</b> Player messages go client → server (via
|
||||
/// <see cref="SubmitMessage"/> + <see cref="SubmitMessageServerRpc"/>) so the
|
||||
/// server gets a chance to validate/filter, then the server broadcasts to
|
||||
/// every peer via <see cref="BroadcastMessageClientRpc"/>. The host receives
|
||||
/// its own broadcast like any other client, so a single subscription path
|
||||
/// handles every message type uniformly.
|
||||
///
|
||||
/// <b>System messages.</b> <see cref="PostLocalSystem"/> is local-only and
|
||||
/// does NOT cross the network. Callers typically invoke it from inside an
|
||||
/// already-replicated event (e.g. <see cref="WaveManager.OnLifeLost"/>, which
|
||||
/// fires on every peer via its own ClientRpc) so each peer posts the system
|
||||
/// message itself. This avoids paying a second round-trip for events that
|
||||
/// are inherently broadcast already.
|
||||
///
|
||||
/// <b>Scene setup.</b> Drop a <c>ChatService</c> GameObject (with a
|
||||
/// <c>NetworkObject</c>) into the gameplay scene. NGO 2.x auto-discovers
|
||||
/// the prefab — no manual registration needed.
|
||||
/// </remarks>
|
||||
[RequireComponent(typeof(NetworkObject))]
|
||||
public class ChatService : NetworkBehaviour
|
||||
{
|
||||
// ----- Singleton --------------------------------------------------
|
||||
|
||||
public static ChatService Instance { get; private set; }
|
||||
|
||||
// ----- Message types ----------------------------------------------
|
||||
|
||||
public enum MessageKind
|
||||
{
|
||||
Player,
|
||||
System,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// One chat feed entry. <see cref="SenderName"/> is empty for system messages.
|
||||
/// </summary>
|
||||
public readonly struct ChatEntry
|
||||
{
|
||||
public readonly MessageKind Kind;
|
||||
public readonly string SenderName;
|
||||
public readonly string Text;
|
||||
|
||||
public ChatEntry(MessageKind kind, string senderName, string text)
|
||||
{
|
||||
Kind = kind;
|
||||
SenderName = senderName;
|
||||
Text = text;
|
||||
}
|
||||
}
|
||||
|
||||
// ----- Local events -----------------------------------------------
|
||||
|
||||
/// <summary>
|
||||
/// Fires on every peer when a player message arrives (after the
|
||||
/// server's ClientRpc) OR when a local system message is posted via
|
||||
/// <see cref="PostLocalSystem"/>. HUD subscribes to render the feed.
|
||||
/// </summary>
|
||||
public static event System.Action<ChatEntry> OnMessageReceived;
|
||||
|
||||
// ----- NGO lifecycle ----------------------------------------------
|
||||
|
||||
public override void OnNetworkSpawn()
|
||||
{
|
||||
if (Instance != null && Instance != this)
|
||||
{
|
||||
Debug.LogError("[ChatService] Duplicate instance detected. " +
|
||||
"Only one ChatService should exist per scene.");
|
||||
return;
|
||||
}
|
||||
Instance = this;
|
||||
}
|
||||
|
||||
public override void OnNetworkDespawn()
|
||||
{
|
||||
if (Instance == this) Instance = null;
|
||||
}
|
||||
|
||||
// ----- Player messages (network round-trip) -----------------------
|
||||
|
||||
/// <summary>
|
||||
/// Submits a chat message authored by the local player. Empty / whitespace
|
||||
/// strings are dropped. Text is trimmed and truncated to fit the wire
|
||||
/// payload (~120 chars).
|
||||
/// </summary>
|
||||
public void SubmitMessage(string text)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(text)) return;
|
||||
text = text.Trim();
|
||||
if (text.Length > 120) text = text.Substring(0, 120);
|
||||
|
||||
FixedString128Bytes payload = default;
|
||||
payload.Append(text);
|
||||
SubmitMessageRpc(payload);
|
||||
}
|
||||
|
||||
// NGO 2.x unified RPC attribute. SendTo.Server + InvokePermission.Everyone
|
||||
// is the modern replacement for [ServerRpc(RequireOwnership = false)] —
|
||||
// any client (not just the NetworkObject's owner) may invoke it.
|
||||
[Rpc(SendTo.Server, InvokePermission = RpcInvokePermission.Everyone)]
|
||||
private void SubmitMessageRpc(FixedString128Bytes text, RpcParams rpcParams = default)
|
||||
{
|
||||
// Validation / filtering hook — drop spam, profanity, etc. For now we
|
||||
// just broadcast unchanged. The server origin tag means whatever
|
||||
// appears in clients' chat is what the server allowed through.
|
||||
BroadcastMessageRpc(rpcParams.Receive.SenderClientId, text);
|
||||
}
|
||||
|
||||
// SendTo.Everyone matches the old [ClientRpc] behavior under host mode —
|
||||
// the body runs on every peer including the host's local client, so the
|
||||
// host sees its own messages in the feed without a separate local call.
|
||||
[Rpc(SendTo.Everyone)]
|
||||
private void BroadcastMessageRpc(ulong senderClientId, FixedString128Bytes text)
|
||||
{
|
||||
string senderName = ResolveSenderName(senderClientId);
|
||||
OnMessageReceived?.Invoke(
|
||||
new ChatEntry(MessageKind.Player, senderName, text.ToString()));
|
||||
}
|
||||
|
||||
// ----- System messages (local only) -------------------------------
|
||||
|
||||
/// <summary>
|
||||
/// Posts a system message to the local chat feed only. Does NOT cross the
|
||||
/// network. Callers should invoke this from a code path that's already
|
||||
/// replicated on every peer (e.g. a ClientRpc handler or an event that's
|
||||
/// fired on every peer) so each peer sees the message exactly once.
|
||||
/// </summary>
|
||||
public static void PostLocalSystem(string text)
|
||||
{
|
||||
if (string.IsNullOrEmpty(text)) return;
|
||||
OnMessageReceived?.Invoke(
|
||||
new ChatEntry(MessageKind.System, "", text));
|
||||
}
|
||||
|
||||
// ----- Helpers ----------------------------------------------------
|
||||
|
||||
private static string ResolveSenderName(ulong clientId)
|
||||
{
|
||||
var pms = PlayerMatchState.GetForClient(clientId);
|
||||
if (pms != null && pms.Slot != PlayerSlot.None)
|
||||
return $"P{(int)pms.Slot}";
|
||||
return $"Client {clientId}";
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue