Вторая часть из серии статей на тему создания сетевого чата в Unity с использованием Netcode for GameObjects.
И снова привет, юный создатель игр!
ВАЖНО: Эта часть требует завершенный код из первой части!
В первой части мы создали скелет нашего чата. Он работает, но ему не хватает души. Сегодня мы вдохнем в него жизнь: добавим команды, чтобы игроки использовали эмодзи для выражения ярких эмоций и форматирование, чтобы выделять главное.
Представь, что твоя рация из первой части превратилась в настоящий командный центр!
Важное изменение: Где обрабатывать сообщения?
В первой части мы отправляли "сырое" сообщение на сервер, а он просто пересылал его всем остальным. Это просто и работает.
Но для команд, эмодзи и форматирования этот подход не годится. Почему?
Команды (вроде
/help
) должны выполняться только у того, кто их ввел. Нет смысла показывать справку всем игрокам.Обработка эмодзи и форматирования на каждом клиенте - лишняя работа. Гораздо эффективнее один раз подготовить красивое сообщение и разослать уже готовое.
Поэтому мы изменим логику: вся обработка будет происходить на стороне отправителя, перед тем как сообщение уйдет в сеть.
Шаг 1: Система команд
Команды, начинающиеся с /
, будут перехватываться и выполняться локально.
Сначала добавим в ChatSystem.cs
все необходимые методы для обработки команд.
ВАЖНО: Убедись, что в начале файла ChatSystem.cs
есть все using из оригинала + новые:
// Из оригинальной статьи:
using Unity.Netcode;
using UnityEngine;
using UnityEngine.UI;
using TMPro;
using System.Collections.Generic;
// Новые для второй части:
using System.Text.RegularExpressions;
Теперь вставь этот код внутрь класса ChatSystem
, после всех переменных из первой части:
// ... после всех переменных из первой части
private void ProcessCommand(string commandMessage)
{
// Убираем символ "/" из начала и приводим к нижнему регистру
string command = commandMessage.Substring(1).ToLower();
if (command.StartsWith("help"))
{
ShowHelpMessage();
}
else if (command.StartsWith("color"))
{
// Извлекаем название цвета из команды
string[] parts = command.Split(' ', 2);
string colorName = parts.Length > 1 ? parts[1] : "";
if (!string.IsNullOrEmpty(colorName))
{
ChangePlayerColor(colorName);
}
else
{
CreateSystemMessage("Укажите цвет. Например: /color red");
}
}
else if (command.StartsWith("time"))
{
ShowCurrentTime();
}
else if (command.StartsWith("players"))
{
// Теперь мы не вызываем метод напрямую, а отправляем запрос на сервер
RequestPlayerListServerRpc();
}
else if (command.StartsWith("roll"))
{
RollDice();
}
else if (command.StartsWith("joke"))
{
TellJoke();
}
else
{
CreateSystemMessage($"Неизвестная команда: {command.Split(' ')[0]}");
}
}
private void ShowHelpMessage()
{
string helpText = "<b>Доступные команды:</b>\n" +
"/help - показать эту справку\n" +
"/color [цвет] - сменить цвет (например, red, blue, green, #ff00ff)\n" +
"/time - показать текущее время\n" +
"/players - показать список игроков онлайн\n" +
"/roll - бросить кубик (1-100)\n" +
"/joke - случайный анекдот";
CreateSystemMessage(helpText);
}
private void ChangePlayerColor(string colorName)
{
// Этот метод работает с компонентом PlayerColorChanger,
// который описан ниже в разделе "Шаг 1.5".
// Безопасно получаем локального игрока
if (NetworkManager.Singleton?.SpawnManager == null)
{
CreateSystemMessage("Нет подключения к серверу!");
return;
}
var localPlayer = NetworkManager.Singleton.SpawnManager.GetLocalPlayerObject();
if (localPlayer != null)
{
PlayerColorChanger colorChanger = localPlayer.GetComponent<PlayerColorChanger>();
if (colorChanger != null && colorChanger.IsOwner)
{
colorChanger.ChangeColorServerRpc(colorName);
CreateSystemMessage($"Запрос на смену цвета на {colorName} отправлен.");
}
else
{
CreateSystemMessage("На вашем персонаже нет компонента PlayerColorChanger!");
}
}
else
{
CreateSystemMessage("Локальный игрок не найден!");
}
}
private void ShowCurrentTime()
{
string currentTime = System.DateTime.Now.ToString("HH:mm:ss");
CreateSystemMessage($"Текущее время: {currentTime}");
}
// Этот метод создает сообщение от имени "Системы" только для локального игрока
// ВНИМАНИЕ: Использует переменные messagePrefab, messageContainer, messageObjects,
// maxMessages, chatScrollRect из первой части руководства!
public void CreateSystemMessage(string text)
{
// Проверяем, что шаблон сообщения существует (как в оригинале)
if (messagePrefab == null)
{
Debug.LogError("MessagePrefab не назначен!");
return;
}
// Проверяем, что контейнер существует (как в оригинале)
if (messageContainer == null)
{
Debug.LogError("MessageContainer не назначен!");
return;
}
// Создаем новый объект сообщения
GameObject messageObj = Instantiate(messagePrefab, messageContainer);
// Находим текстовый компонент в сообщении
TMP_Text messageText = messageObj.GetComponentInChildren<TMP_Text>();
// Проверяем, что текстовый компонент найден (как в оригинале)
if (messageText == null)
{
Debug.LogError("TMP_Text компонент не найден в MessagePrefab!");
Destroy(messageObj);
return;
}
// Формируем текст системного сообщения
messageText.text = $"<color=yellow>[Система]:</color> {text}";
// Логика удаления старых сообщений, как и в CreateMessageInChat
messageObjects.Add(messageObj);
if (messageObjects.Count > maxMessages)
{
GameObject oldMessage = messageObjects[0];
messageObjects.RemoveAt(0);
Destroy(oldMessage);
}
// Обязательно обновляем прокрутку
Canvas.ForceUpdateCanvases();
chatScrollRect.verticalNormalizedPosition = 0f;
}
// Новые методы для правильной работы команды /players
[ServerRpc(RequireOwnership = false)]
private void RequestPlayerListServerRpc(ServerRpcParams serverRpcParams = default)
{
var clientId = serverRpcParams.Receive.SenderClientId;
string playersList = "<b>Игроки онлайн:</b>\n";
foreach (var client in NetworkManager.Singleton.ConnectedClientsList)
{
string playerName = client.ClientId == clientId ? $"Вы (ID: {client.ClientId})" : $"Игрок {client.ClientId}";
playersList += $"- {playerName}\n";
}
ClientRpcParams clientRpcParams = new ClientRpcParams
{
Send = new ClientRpcSendParams
{
TargetClientIds = new ulong[] { clientId }
}
};
ReceivePlayerListClientRpc(playersList, clientRpcParams);
}
[ClientRpc]
private void ReceivePlayerListClientRpc(string playersList, ClientRpcParams clientRpcParams = default)
{
CreateSystemMessage(playersList);
}
// Дополнительные команды для веселья
private void RollDice()
{
int result = UnityEngine.Random.Range(1, 101); // Случайное число от 1 до 100
CreateSystemMessage($"🎲 Бросок кубика (1-100): выпало <b>{result}</b>");
}
private void TellJoke()
{
string[] jokes = {
"Почему программисты путают Рождество и Хэллоуин? Потому что Oct 31 == Dec 25!",
"В чем разница между программистом и богом? Бог не думает, что он программист.",
"Что сказал программист на свадьбе? 'return true;'"
};
string joke = jokes[UnityEngine.Random.Range(0, jokes.Length)];
CreateSystemMessage($"😄 Анекдот: {joke}");
}
Что здесь есть?
ProcessCommand
обрабатывает все команды:/help
,/color
,/time
,/players
,/roll
,/joke
.Команда
/players
работает через сетевые RPC для корректного получения списка игроков.CreateSystemMessage
создает локальное сообщение от системы. Оно не уходит в сеть и видно только тебе.RollDice
иTellJoke
добавляют веселье в чат.Логика
ChangePlayerColor
корректно находит компонент на сетевом объекте игрока.
Шаг 1.5: Создаем скрипт для смены цвета
Команда /color
в нашем коде пытается найти компонент PlayerColorChanger
, но мы его еще не создали. Давайте это исправим.
Создайте новый скрипт с именем PlayerColorChanger.cs
:
using Unity.Netcode;
using UnityEngine;
public class PlayerColorChanger : NetworkBehaviour
{
// Сетевая переменная для синхронизации цвета между всеми клиентами
public NetworkVariable<Color> PlayerColor = new NetworkVariable<Color>(Color.white);
private Renderer playerRenderer;
private Material originalMaterial; // Кэшируем оригинальный материал
private Material playerMaterial; // Наша копия материала
public override void OnNetworkSpawn()
{
// Находим рендерер, чтобы менять цвет материала
playerRenderer = GetComponentInChildren<Renderer>();
if (playerRenderer != null)
{
// Корректно получаем оригинальный материал
originalMaterial = playerRenderer.sharedMaterial;
playerMaterial = new Material(originalMaterial);
playerRenderer.material = playerMaterial;
}
// Подписываемся на изменение цвета, чтобы сразу применить его
PlayerColor.OnValueChanged += OnColorChanged;
// Устанавливаем начальный цвет при появлении объекта в сети
OnColorChanged(PlayerColor.Value, PlayerColor.Value);
}
public override void OnNetworkDespawn()
{
// Отписываемся, чтобы избежать утечек памяти
if (PlayerColor != null)
{
PlayerColor.OnValueChanged -= OnColorChanged;
}
// Очищаем созданный материал
if (playerMaterial != null)
{
Destroy(playerMaterial);
}
}
// Этот метод вызывается у всех клиентов, когда PlayerColor.Value меняется на сервере
private void OnColorChanged(Color previousValue, Color newValue)
{
if (playerMaterial != null)
{
playerMaterial.color = newValue;
}
}
[ServerRpc]
public void ChangeColorServerRpc(string colorName)
{
Color newColor = Color.white; // по умолчанию
bool colorRecognized = true;
switch (colorName.ToLower())
{
case "red": newColor = Color.red; break;
case "blue": newColor = Color.blue; break;
case "green": newColor = Color.green; break;
case "yellow": newColor = Color.yellow; break;
case "white": newColor = Color.white; break;
case "black": newColor = Color.black; break;
default:
// Пробуем как HEX-код (должен начинаться с #)
if (colorName.StartsWith("#") && ColorUtility.TryParseHtmlString(colorName, out newColor))
{
// HEX-код распознан успешно
}
else
{
colorRecognized = false;
}
break;
}
if (!colorRecognized)
{
// Отправляем системное сообщение только себе
if (IsOwner)
{
// Ищем ChatSystem через NetworkObject (быстрее чем FindObjectOfType)
var networkObjects = FindObjectsOfType<NetworkObject>();
ChatSystem chatSystem = null;
foreach (var netObj in networkObjects)
{
chatSystem = netObj.GetComponent<ChatSystem>();
if (chatSystem != null) break;
}
if (chatSystem != null)
{
chatSystem.CreateSystemMessage($"Цвет '{colorName}' не распознан! Используй, например: red, blue, green или HEX-код вроде #ff00ff, #00ff00.");
}
else
{
Debug.LogWarning("Не удалось найти ChatSystem для отображения ошибки смены цвета");
}
}
return;
}
PlayerColor.Value = newColor;
}
}
Что делать с этим скриптом:
Создайте этот скрипт в вашем проекте Unity.
Добавьте этот компонент на ваш префаб игрока.
Убедитесь, что на префабе игрока или на одном из его дочерних объектов есть компонент
Renderer
(например, отCube
илиCapsule
), чтобы было что перекрашивать.
Шаг 2: Эмодзи и форматирование
Теперь научим чат понимать красоту. Добавь эти два метода в ChatSystem.cs
. Они будут превращать :)
в 😊 и **текст**
в жирный текст.
private string ProcessEmojis(string message)
{
// Просто заменяем текст на символы
message = message.Replace(":)", "😊");
message = message.Replace(":(", "😢");
message = message.Replace(":D", "😄");
message = message.Replace("<3", "❤️");
message = message.Replace(":fire:", "🔥");
message = message.Replace(":star:", "⭐");
return message;
}
private string ProcessFormatting(string message)
{
// Защита от слишком длинных сообщений (могут замедлить регулярные выражения)
if (message.Length > 1000)
{
return message; // Не обрабатываем форматирование в очень длинных сообщениях
}
// Обрабатываем в правильном порядке, чтобы избежать конфликтов
// Сначала __подчёркнутый__ (самый длинный маркер)
message = Regex.Replace(message, @"__(.*?)__", "<u>$1</u>");
// Затем **жирный** (двойные звёздочки)
message = Regex.Replace(message, @"\*\*(.*?)\*\*", "<b>$1</b>");
// В конце *курсив* (одинарные звёздочки)
message = Regex.Replace(message, @"\*(.*?)\*", "<i>$1</i>");
return message;
}
Шаг 3: Собираем все вместе
Мы написали все вспомогательные методы. Теперь нужно переписать главный метод SendMessage
из первой части, чтобы он использовал всю нашу новую логику.
Полностью замени старый метод SendMessage
из первой части на этот новый код с поддержкой команд и форматирования:
void SendMessage() // Как в оригинале - модификатор по умолчанию
{
// Проверяем, что поле не пустое
if (string.IsNullOrEmpty(messageInput.text))
return;
// Проверяем, что игрок подключен к сети
if (!IsClient)
{
Debug.LogWarning("Нельзя отправить сообщение без подключения к сети!");
return;
}
string message = messageInput.text;
// 1. ПРОВЕРКА НА КОМАНДУ
// Если сообщение - это команда, обрабатываем ее локально и выходим
if (message.StartsWith("/"))
{
messageInput.text = ""; // Очищаем поле только после успешной обработки
ProcessCommand(message);
return;
}
// 2. ОБРАБОТКА ФОРМАТИРОВАНИЯ И ЭМОДЗИ
// Превращаем :) в 😊 и **текст** в <b>текст</b>
message = ProcessEmojis(message);
message = ProcessFormatting(message);
// 3. ОТПРАВКА НА СЕРВЕР (как в оригинале!)
SendMessageServerRpc(message);
// Очищаем поле ввода ПОСЛЕ отправки (как в оригинале)
messageInput.text = "";
}
Что здесь происходит?
Идентичные проверки и комментарии: Точно те же
IsNullOrEmpty
,!IsClient
,Debug.LogWarning
и комментарии, что и в оригинале.Сохранена оригинальная структура: Проверки → отправка → очистка поля (как в оригинале).
Единственное изменение: Вместо
SendMessageServerRpc(messageInput.text)
используетсяSendMessageServerRpc(message)
, гдеmessage
- это обработанная версия с эмодзи и форматированием.Команды: Обрабатываются локально и НЕ отправляются на сервер (не нарушают оригинальную логику).
Полная обратная совместимость: Если убрать обработку команд, эмодзи и форматирования - получится точно оригинальный метод.
Система безопасности:
CreateSystemMessage
использует те же проверки на null, что иCreateMessageInChat
в оригинале.
При таком подходе SendMessageServerRpc
, ReceiveMessageClientRpc
и CreateMessageInChat
из первой части остаются без изменений! Они по-прежнему просто передают и отображают сообщение, но теперь это сообщение уже заранее обработано.
ВАЖНО: Убедись, что твой метод
Start()
из первой части остался без изменений:
void Start() { // Подписываемся на нажатие кнопки "Отправить" sendButton.onClick.AddListener(SendMessage); // Подписываемся на нажатие Enter в поле ввода messageInput.onSubmit.AddListener((_) => SendMessage()); }
Это обеспечивает полную совместимость с оригинальным кодом!
ВАЖНОЕ ЗАМЕЧАНИЕ: Чтобы форматирование <b>
, <i>
и <u>
работало, вам нужно включить поддержку "Rich Text". Для этого:
Найдите ваш
messagePrefab
(шаблон сообщения).Выберите дочерний объект с компонентом
TextMeshPro - Text
.В инспекторе, в настройках этого компонента, найдите и поставьте галочку
Rich Text
. Без этого вы будете видеть теги прямо в тексте.
Готово! Все команды уже работают
Команды /roll
и /joke
уже включены в основной код выше и готовы к использованию!
/roll
- бросает кубик и показывает случайное число от 1 до 100/joke
- рассказывает случайный программистский анекдот
Если хочешь добавить еще команд, просто расширь блок if-else if
в методе ProcessCommand
и добавь соответствующие методы.
Что мы получили
Теперь твой чат - это мощный инструмент:
Команды:
/help
,/time
,/players
,/color
,/roll
,/joke
.Эмодзи:
:)
,<3
,:fire:
и другие.Форматирование: Жирный, курсивный и подчеркнутый текст.
Системные сообщения: Чат может общаться с игроком лично.
В следующей части мы добавим звуки, приватные сообщения и продвинутые функции
Продолжение следует...