[C#, Unity] Основы Unity + Mirror
Автор
Сообщение
news_bot ®
Стаж: 6 лет 9 месяцев
Сообщений: 27286
Хочу поделиться опытом с теми, кто хочет попробовать себя в написании сетевой игры, но не знает с чего начать. Так как информации по этой теме в интернете много, но полезную и актуальную было найти тяжело (а в русскоязычном сегменте и подавно), я решил собрать и структурировать то, что удалось найти.Итак, для написания сетевой игры на Unity сейчас есть несколько вариантов:
- UNet. Устаревшая сетевая технология. На данный момент deprecated и поддержка закончится в ближайшие пару лет. Но что же Unity предлагает взамен?
- NetCode. Потенциально крутая технология, которая будет работать в связке с Entity Component System. Но очень уж медленно она развивается, за пару лет существования вышло 6 версий разной степени багованности, api постоянно меняется и делать что-то серьезное на нем пока рановато. Когда ее доделают – неизвестно. Я слежу за ней уже около года и особого прогресса не заметил.
Что тогда остается? Из бесплатных решений это:
- MLAPI. Альтернатива UNet с широким спектром возможностей. Достойное решение, стоит к нему присмотреться.
- Mirror. Доведенный до ума UNet, который потенциально может использоваться даже в MMO. Может работать как Клиент+Сервер, так и NoGUI-Сервер.
И платные решения (ознакомится с ними не удалось, напишите у кого был опыт как они):
Таблица преимуществ этих решений от Unity:
Мой выбор пал на Mirror, как на ближайший потомок UNet, использующий большинство принципов уже знакомого UNet. На примере простого проекта мы посмотрим основы Mirror, а именно:
- Настройка окружения
- NetworkMessage и spawn игрока в выбранной точке
- Синхронизация переменных посредством SyncVar
- Синхронизация переменных посредством SyncList
- Spawn предмета и взаимодействие с предметом
1. Настройка окурженияДля статьи будем использовать Unity 2020.3.0f1 и Mirror 32.1.4. Добавляем Mirror себе через Asset Store, создаем проект, импортируем Mirror (Window -> Package Manager -> Packages -> My Assets -> Mirror -> Import).Для начала нам нужно создать префаб игрока. Создаем пустой GameObject (назовем его Player), вешаем на него SpriteRenderer, задаем sprite Knob и масштабируем чтобы лучше его рассмотреть. Далее создаем скрипт Player.cs и вешаем его на тот же GameObject. Редактируем скрипт следующим образом:
using Mirror;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Player : NetworkBehaviour //даем системе понять, что это сетевой объект
{
void Update()
{
if (hasAuthority) //проверяем, есть ли у нас права изменять этот объект
{
float h = Input.GetAxis("Horizontal");
float v = Input.GetAxis("Vertical");
float speed = 5f * Time.deltaTime;
transform.Translate(new Vector2(h * speed, v * speed)); //делаем простейшее движение
}
}
}
Подробнее про NetworkBehaviour и NetworkIdentity
- Компонент NetworkIdentity добавится автоматически при добавлении скрипта, наследуемого от NetworkBehaviour.
- В одном GameObject (и всех его потомках) может быть только один NetworkIdentity.
- NetworkIdentity позволяет отличить один сетевой объект от другого (для этого используем netId - его значение всегда будет уникальным).
Добавляем компонент NetworkTransform, чтобы положение нашего игрока синхронизировалось между всеми игроками. Ставим галочку ClientAuthority, чтобы изменения произведенные клиентом, считались валидными.Подробнее про NetworkTransform
- Компонент NetworkIdentity также добавится автоматически при добавлении NetworkTransform (если его еще не было).
- Если вам нужно синхронизировать потомков, добавляйте NetworkTransformChild на тот же объект, где уже есть NetworkIdentity, и указывайте в Target тот transform, который нужно синхронизировать.
Делаем из нашего GameObject префаб. Получилось что-то такое:
Далее создаем скрипт NetMan.cs, создаем пустой GameObject (назовем его NetMan) и вешаем на него скрипт. Это будет наш скрипт, который отвечает за старт сервера и подключение игроков.Пока просто наследуем класс от NetworkManager, на этом этапе этого будет достаточно.
using Mirror;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class NetMan : NetworkManager
{
}
У нас в инспекторе появятся настройки сервера и добавится компонент KcpTransport. Докидываем на тот же GameObject компонент NetworkManagerHUD (он создает необходимое для подключения GUI).Остановимся подробнее на настройках:
- Don’t Destroy On Load. Будет ли объект существовать между сценами?
- Run In Background. Будет ли компонент продолжать работать когда окно программы неактивно?
- Auto Start Server Build. Будет ли сервер стартовать автоматически, если была выбрана опция билда «Server Build»?
- Show Debug Messages. По этой опции не удалось разобраться или найти какую-то информацию.
- Server Tick Rate. Количество обновлений сервера в секунду.
- Server Batching. Должен ли сервер сначала собрать текущую сетевую информацию и отправить ее в LateUpdate разом? Полезно для уменьшения нагрузки на CPU и сеть, но увеличивает задержку.
- Server Batch Interval. Чем выше это значение, тем реже будет отправляться сетевая информация.
Теперь нам нужно указать префаб, который будет спавниться в качестве игрока. Перетаскиваем префаб Player в поле Player Prefab и после этого убираем его со сцены (оставляем только камеру и NetMan).
Первый этап готов. Выставляем выполнение в неполном экране (чтобы несколько экземпляров помещалось), делаем сборку, запускаем 2 экземпляра и проверяем. Один экземпляр стартуем как сервер, второй как клиент. На wasd двигаем своего персонажа, он успешно синхронизируется с другим экземпляром.
2. NetworkMessage и spawn игрока в выбранной точкеНа примере спавна в выбранной точке мы научимся отправлять сообщения на сервер.В настройках NetMan убираем галочку AutoCreatePlayer, дальше мы будем контролировать спавн игрока сами. Для этого мы изменим скрипт NetMan.cs. Начнем с создания struct с данными о позиции:
public struct PosMessage : NetworkMessage //наследуемся от интерфейса NetworkMessage, чтобы система поняла какие данные упаковывать
{
public Vector2 vector2; //нельзя использовать Property
}
Далее создадим метод непосредственно спавна, который будет выполняется только на сервере:
public void OnCreateCharacter(NetworkConnection conn, PosMessage message)
{
GameObject go = Instantiate(playerPrefab, message.vector2, Quaternion.identity); //локально на сервере создаем gameObject
NetworkServer.AddPlayerForConnection(conn, go); //присоеднияем gameObject к пулу сетевых объектов и отправляем информацию об этом остальным игрокам
}
Теперь перегрузим мтод OnStartServer (выполняется только на сервере) и добавим в него обработчик сетевого сообщения:
public override void OnStartServer()
{
base.OnStartServer();
NetworkServer.RegisterHandler<PosMessage>(OnCreateCharacter); //указываем, какой struct должен прийти на сервер, чтобы выполнился свапн
}
Создадим метод, который будет активировать спавн (и выполняться локально на клиенте):
bool playerSpawned;
public void ActivatePlayerSpawn()
{
Vector3 pos = Input.mousePosition;
pos.z = 10f;
pos = Camera.main.ScreenToWorldPoint(pos);
PosMessage m = new PosMessage() { vector2 = pos }; //создаем struct определенного типа, чтобы сервер понял к чему эти данные относятся
connection.Send(m); //отправка сообщения на сервер с координатами спавна
playerSpawned = true;
}
И напоследок зададим условия для активации спавна:
NetworkConnection connection;
bool playerConnected;
public override void OnClientConnect(NetworkConnection conn)
{
base.OnClientConnect(conn);
connection = conn;
playerConnected = true;
}
private void Update()
{
if (Input.GetKeyDown(KeyCode.Mouse0) && !playerSpawned && playerConnected)
{
ActivatePlayerSpawn();
}
}
В итоге получаем такой скрипт NetMan.cs:
using Mirror;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class NetMan : NetworkManager
{
bool playerSpawned;
NetworkConnection connection;
bool playerConnected;
public void OnCreateCharacter(NetworkConnection conn, PosMessage message)
{
GameObject go = Instantiate(playerPrefab, message.vector2, Quaternion.identity); //локально на сервере создаем gameObject
NetworkServer.AddPlayerForConnection(conn, go); //присоеднияем gameObject к пулу сетевых объектов и отправляем информацию об этом остальным игрокам
}
public override void OnStartServer()
{
base.OnStartServer();
NetworkServer.RegisterHandler<PosMessage>(OnCreateCharacter); //указываем, какой struct должен прийти на сервер, чтобы выполнился свапн
}
public void ActivatePlayerSpawn()
{
Vector3 pos = Input.mousePosition;
pos.z = 10f;
pos = Camera.main.ScreenToWorldPoint(pos);
PosMessage m = new PosMessage() { vector2 = pos }; //создаем struct определенного типа, чтобы сервер понял к чему эти данные относятся
connection.Send(m); //отправка сообщения на сервер с координатами спавна
playerSpawned = true;
}
public override void OnClientConnect(NetworkConnection conn)
{
base.OnClientConnect(conn);
connection = conn;
playerConnected = true;
}
private void Update()
{
if (Input.GetKeyDown(KeyCode.Mouse0) && !playerSpawned && playerConnected)
{
ActivatePlayerSpawn();
}
}
}
public struct PosMessage : NetworkMessage //наследуемся от интерфейса NetworkMessage, чтобы система поняла какие данные упаковывать
{
public Vector2 vector2; //нельзя использовать Property
}
Второй этап готов. Делаем сборку, проверяем. После подключения нужно кликнуть левой кнопкой мыши в точку, где игрок хочет засвапниться.
3. Синхронизация переменных посредством SyncVarПереходим к очень интересной фиче – SyncVar. Она позволяет избежать ручной синхронизации данных. Главное правило – меняем переменную только на сервере и не используем ее как данные (только как временное хранилище для данных, которые нам нужно обработать).Для начала подготовим объекты, которые мы будем использовать для наглядной синхронизации. Например, здоровье в виде красных кружков. Открываем редактирование префаба Player и добавляем ему несколько объектов, представляющих собой жизнь (Knob + красный цвет). Располагаем их так, чтобы было хорошо видно.Редактируем скрипт Player.cs, добавляем переменные:
public int Health;
public GameObject[] HealthGos;
Сохраняем, закидываем объекты-жизни в переменную HealthGos и выставляем такое же количество в переменной Health.
Добавляем в Update обновление объектов-жизней в соответствии с количеством жизней:
void Update()
{
...
for (int i = 0; i < HealthGos.Length; i++)
{
HealthGos[i].SetActive(!(Health - 1 < i));
}
}
И переходим к методу на клиенте, который будет выставлять Health в соответствии с синхронизированным значением:
[SyncVar(hook = nameof(SyncHealth))] //задаем метод, который будет выполняться при синхронизации переменной
int _SyncHealth;
void SyncHealth(int oldValue, int newValue) //обязательно делаем два значения - старое и новое.
{
Health = newValue;
}
Теперь нам нужно сделать метод, который будет менять переменную _SyncHealth. Этот метод будет выполняться только на сервере.
[Server] //обозначаем, что этот метод будет вызываться и выполняться только на сервере
public void ChangeHealthValue(int newValue)
{
_SyncHealth = newValue;
}
Далее переходим к методу, который также будет выполняться на сервере, но клиент сможет запросить его выполнение:
[Command] //обозначаем, что этот метод должен будет выполняться на сервере по запросу клиента
public void CmdChangeHealth(int newValue) //обязательно ставим Cmd в начале названия метода
{
ChangeHealthValue(newValue); //переходим к непосредственному изменению переменной
}
Подробнее про Command и Rpc
- Command используется для того, чтобы клиенты могли попросить сервер выполнить заданную команду.
- Rpc используется для того, чтобы сервер мог попросить клиентов выполнить заданную команду.
- Command можно вызывать на сервере+клиенте, но Rpc нельзя вызывать на клиенте.
- Передавать в Rpc и Command можно только ограниченный набор типов.
- Вызов Rpc в режиме сервер+клиент также выполнится на нем самом.
Пример Rpc:
[ClientRpc] //обозначаем, что этот метод будет выполняться на клиенте по запросу сервера
public void RpcTest() //обязательно ставим Rpc в начале названия метода
{
Debug.Log("Сервер попросил меня это написать");
}
Все готово для синхронизации, зададим условия изменения жизней. На этом этапе сделаем простую схему – каждый игрок может только уменьшить свои жизни. Для этого дополним Update:
void Update()
{
if (hasAuthority) //проверяем, есть ли у нас права изменять этот объект
{
...
if (Input.GetKeyDown(KeyCode.H)) //отнимаем у себя жизнь по нажатию клавиши H
{
if (isServer) //если мы являемся сервером, то переходим к непосредственному изменению переменной
ChangeHealthValue(Health - 1);
else
CmdChangeHealth(Health - 1); //в противном случае делаем на сервер запрос об изменении переменной
}
}
...
}
Этап завершен. Теперь у игроков всегда будет актуальное количество жизней, даже у тех, кто присоединяется позднее (после изменения количества жизней у других игроков).
4. Синхронизация переменных посредством SyncListСинхронизировать одну переменную это конечно хорошо, но для серьезных проектов нам понадобится инструмент посерьезнее. SyncList позволяет синхронизировать массивы данных. Разберемся с ним на примере сохранения пройденного пути по нажатию кнопки (просто для наглядности). Редактируем скрипт Player.cs по аналогии с SyncVar.Изменение массива на сервере:
SyncList<Vector3> _SyncVector3Vars = new SyncList<Vector3>(); //В случае SyncList не нужно ставить SyncVar и задавать метод, это делается иначе
[Server]
void ChangeVector3Vars(Vector3 newValue)
{
_SyncVector3Vars.Add(newValue);
}
Команда для запроса с клиента на сервер:
[Command]
public void CmdChangeVector3Vars(Vector3 newValue)
{
ChangeVector3Vars(newValue);
}
И обработчик события изменения массива на клиенте:
public List<Vector3> Vector3Vars;
void SyncVector3Vars(SyncList<Vector3>.Operation op, int index, Vector3 oldItem, Vector3 newItem)
{
switch (op)
{
case SyncList<Vector3>.Operation.OP_ADD:
{
Vector3Vars.Add(newItem);
break;
}
case SyncList<Vector3>.Operation.OP_CLEAR:
{
break;
}
case SyncList<Vector3>.Operation.OP_INSERT:
{
break;
}
case SyncList<Vector3>.Operation.OP_REMOVEAT:
{
break;
}
case SyncList<Vector3>.Operation.OP_SET:
{
break;
}
}
}
Теперь перегрузим метод старта клиента:
public override void OnStartClient()
{
base.OnStartClient();
_SyncVector3Vars.Callback += SyncVector3Vars; //вместо hook, для SyncList используем подписку на Callback
Vector3Vars = new List<Vector3>(_SyncVector3Vars.Count); //так как Callback действует только на изменение массива,
for (int i = 0; i < _SyncVector3Vars.Count; i++) //а у нас на момент подключения уже могут быть какие-то данные в массиве, нам нужно эти данные внести в локальный массив
{
Vector3Vars.Add(_SyncVector3Vars[i]);
}
}
Синхронизация готова, но нам нужно задать условия изменения массива и визуализировать данные. Создадим пустой GameObject + SpriteRenderer + Knob + меняем цвет. Сохраняем как префаб Point.Добавим компонент LineRenderer на префаб Player, выставим ему ноль позиций и немного уменьшим ширину. Отредактируем скрипт Player.cs:
public GameObject PointPrefab; //сюда вешаем ранее созданные префаб Point
public LineRenderer LineRenderer; //сюда кидаем наш же компонент
int pointsCount;
void Update()
{
if (hasAuthority) //проверяем, есть ли у нас права изменять этот объект
{
...
if (Input.GetKeyDown(KeyCode.P))
{
if (isServer)
ChangeVector3Vars(transform.position);
else
CmdChangeVector3Vars(transform.position);
}
}
...
for (int i = pointsCount; i < Vector3Vars.Count; i++)
{
Instantiate(PointPrefab, Vector3Vars[i], Quaternion.identity);
pointsCount++;
LineRenderer.positionCount = Vector3Vars.Count;
LineRenderer.SetPositions(Vector3Vars.ToArray());
}
}
Как будут выглядеть Player и Point
Скрипт Player.cs
using Mirror;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Player : NetworkBehaviour //даем системе понять, что это сетевой объект
{
[SyncVar(hook = nameof(SyncHealth))] //задаем метод, который будет выполняться при синхронизации переменной
int _SyncHealth;
public int Health;
public GameObject[] HealthGos;
SyncList<Vector3> _SyncVector3Vars = new SyncList<Vector3>(); //В случае SyncList не нужно ставить SyncVar и задавать метод, это делается иначе
public List<Vector3> Vector3Vars;
public GameObject PointPrefab; //сюда вешаем ранее созданные префаб Point
public LineRenderer LineRenderer; //сюда кидаем наш же компонент
int pointsCount;
void Update()
{
if (hasAuthority) //проверяем, есть ли у нас права изменять этот объект
{
float h = Input.GetAxis("Horizontal");
float v = Input.GetAxis("Vertical");
float speed = 5f * Time.deltaTime;
transform.Translate(new Vector2(h * speed, v * speed)); //делаем простейшее движение
if (Input.GetKeyDown(KeyCode.H)) //отнимаем у себя жизнь по нажатию клавиши H
{
if (isServer) //если мы являемся сервером, то переходим к непосредственному изменению переменной
ChangeHealthValue(Health - 1);
else
CmdChangeHealth(Health - 1); //в противном случае делаем на сервер запрос об изменении переменной
}
if (Input.GetKeyDown(KeyCode.P))
{
if (isServer)
ChangeVector3Vars(transform.position);
else
CmdChangeVector3Vars(transform.position);
}
}
for (int i = 0; i < HealthGos.Length; i++)
{
HealthGos[i].SetActive(!(Health - 1 < i));
}
for (int i = pointsCount; i < Vector3Vars.Count; i++)
{
Instantiate(PointPrefab, Vector3Vars[i], Quaternion.identity);
pointsCount++;
LineRenderer.positionCount = Vector3Vars.Count;
LineRenderer.SetPositions(Vector3Vars.ToArray());
}
}
void SyncHealth(int oldValue, int newValue) //обязательно делаем два значения - старое и новое.
{
Health = newValue;
}
[Server] //обозначаем, что этот метод будет вызываться и выполняться только на сервере
public void ChangeHealthValue(int newValue)
{
_SyncHealth = newValue;
}
[Command] //обозначаем, что этот метод должен будет выполняться на сервере по запросу клиента
public void CmdChangeHealth(int newValue) //обязательно ставим Cmd в начале названия метода
{
ChangeHealthValue(newValue); //переходим к непосредственному изменению переменной
}
[Server]
void ChangeVector3Vars(Vector3 newValue)
{
_SyncVector3Vars.Add(newValue);
}
[Command]
public void CmdChangeVector3Vars(Vector3 newValue)
{
ChangeVector3Vars(newValue);
}
void SyncVector3Vars(SyncList<Vector3>.Operation op, int index, Vector3 oldItem, Vector3 newItem)
{
switch (op)
{
case SyncList<Vector3>.Operation.OP_ADD:
{
Vector3Vars.Add(newItem);
break;
}
case SyncList<Vector3>.Operation.OP_CLEAR:
{
break;
}
case SyncList<Vector3>.Operation.OP_INSERT:
{
break;
}
case SyncList<Vector3>.Operation.OP_REMOVEAT:
{
break;
}
case SyncList<Vector3>.Operation.OP_SET:
{
break;
}
}
}
public override void OnStartClient()
{
base.OnStartClient();
_SyncVector3Vars.Callback += SyncVector3Vars; //вместо hook для SyncList используем подписку на Callback
Vector3Vars = new List<Vector3>(_SyncVector3Vars.Count); //так как Callback действует только на изменение массива,
for (int i = 0; i < _SyncVector3Vars.Count; i++) //а у нас на момент подключения уже могут быть какие-то данные в массиве, нам нужно эти данные внести в локальный массив
{
Vector3Vars.Add(_SyncVector3Vars[i]);
}
}
}
Этап завершен, посмотрим на результат. Во время выполнения игрок может нажать клавишу P и его позиция отправится в массив для синхронизации всем игрокам. Также точки соединяться линией, чтобы маршрут был виден наглядно.
5. Spawn предмета и взаимодействие с нимНа последнем этапе мы посмотрим как спавнить предметы и взаимодействовать с ними. Добавим нашему игроку возможность сделать пулями.Создадим новый скрипт Bullet.cs:
using Mirror;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Bullet : NetworkBehaviour
{
uint owner;
bool inited;
Vector3 target;
[Server]
public void Init(uint owner, Vector3 target)
{
this.owner = owner; //кто сделал выстрел
this.target = target; //куда должна лететь пуля
inited = true;
}
void Update()
{
if (inited && isServer)
{
transform.Translate((target - transform.position).normalized * 0.04f);
foreach (var item in Physics2D.OverlapCircleAll(transform.position, 0.5f))
{
Player player = item.GetComponent<Player>();
if (player)
{
if (player.netId != owner)
{
player.ChangeHealthValue(player.Health - 1); //отнимаем одну жизнь по аналогии с примером SyncVar
NetworkServer.Destroy(gameObject); //уничтожаем пулю
}
}
}
if (Vector3.Distance(transform.position, target) < 0.1f) //пуля достигла конечной точки
{
NetworkServer.Destroy(gameObject); //значит ее можно уничтожить
}
}
}
}
Также создадим пустой GameObject + SpriteRenderer + Knob + меняем цвет. Вешаем на него скрипт Bullet.cs. Добавляем компонент NetworkTransform. Сохраняем как префаб Bullet.В скрипт Player.cs добавляем спавн пули на сервере:
[Server]
public void SpawnBullet(uint owner, Vector3 target)
{
GameObject bulletGo = Instantiate(BulletPrefab, transform.position, Quaternion.identity); //Создаем локальный объект пули на сервере
NetworkServer.Spawn(bulletGo); //отправляем информацию о сетевом объекте всем игрокам.
bulletGo.GetComponent<Bullet>().Init(owner, target); //инициализируем поведение пули
}
И запрос на свапн со стороны клиента:
[Command]
public void CmdSpawnBullet(uint owner, Vector3 target)
{
SpawnBullet(owner, target);
}
Выставляем условие появления пули:
void Update()
{
if (hasAuthority) //проверяем, есть ли у нас права изменять этот объект
{
...
if (Input.GetKeyDown(KeyCode.Mouse1))
{
Vector3 pos = Input.mousePosition;
pos.z = 10f;
pos = Camera.main.ScreenToWorldPoint(pos);
if (isServer)
SpawnBullet(netId, pos);
else
CmdSpawnBullet(netId, pos);
}
}
...
}
Добавим еще уничтожение игрока, если жизни закончились:
[Server] //обозначаем, что этот метод будет вызываться и выполняться только на сервере
public void ChangeHealthValue(int newValue)
{
_SyncHealth = newValue;
if (_SyncHealth <= 0)
{
NetworkServer.Destroy(gameObject);
}
}
В настройках NetMan выставляем префаб Bullet как доступный для спавна:
Не забываем выставить префаб Bullet в переменную BulletPrefab префаба Player. Напоследок добавляем на префаб Player компонент CircleCollider2D и ставим галочку IsTrigger, чтобы пуля могла отловить попадание.Последний этап завершен. Проверяем. По нажатию правой кнопки мыши из игрока вылетает пуля и летит туда, где стоял курсор. Если по пути пуля встречает другого игрока – он теряет одну жизнь. Все пули синхронизированы, даже если игрок подключился после их спавна.Скрипт Player.cs
using Mirror;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Player : NetworkBehaviour //даем системе понять, что это сетевой объект
{
[SyncVar(hook = nameof(SyncHealth))] //задаем метод, который будет выполняться при синхронизации переменной
int _SyncHealth;
public int Health;
public GameObject[] HealthGos;
SyncList<Vector3> _SyncVector3Vars = new SyncList<Vector3>(); //В случае SyncList не нужно ставить SyncVar и задавать метод, это делается иначе
public List<Vector3> Vector3Vars;
public GameObject PointPrefab; //сюда вешаем ранее созданные префаб Point
public LineRenderer LineRenderer; //сюда кидаем наш же компонент
int pointsCount;
public GameObject BulletPrefab; //сюда вешаем префаб пули
void Update()
{
if (hasAuthority) //проверяем, есть ли у нас права изменять этот объект
{
float h = Input.GetAxis("Horizontal");
float v = Input.GetAxis("Vertical");
float speed = 5f * Time.deltaTime;
transform.Translate(new Vector2(h * speed, v * speed)); //делаем простейшее движение
if (Input.GetKeyDown(KeyCode.H)) //отнимаем у себя жизнь по нажатию клавиши H
{
if (isServer) //если мы являемся сервером, то переходим к непосредственному изменению переменной
ChangeHealthValue(Health - 1);
else
CmdChangeHealth(Health - 1); //в противном случае делаем на сервер запрос об изменении переменной
}
if (Input.GetKeyDown(KeyCode.P))
{
if (isServer)
ChangeVector3Vars(transform.position);
else
CmdChangeVector3Vars(transform.position);
}
if (Input.GetKeyDown(KeyCode.Mouse1))
{
Vector3 pos = Input.mousePosition;
pos.z = 10f;
pos = Camera.main.ScreenToWorldPoint(pos);
if (isServer)
SpawnBullet(netId, pos);
else
CmdSpawnBullet(netId, pos);
}
}
for (int i = 0; i < HealthGos.Length; i++)
{
HealthGos[i].SetActive(!(Health - 1 < i));
}
for (int i = pointsCount; i < Vector3Vars.Count; i++)
{
Instantiate(PointPrefab, Vector3Vars[i], Quaternion.identity);
pointsCount++;
LineRenderer.positionCount = Vector3Vars.Count;
LineRenderer.SetPositions(Vector3Vars.ToArray());
}
}
void SyncHealth(int oldValue, int newValue) //обязательно делаем два значения - старое и новое.
{
Health = newValue;
}
[Server] //обозначаем, что этот метод будет вызываться и выполняться только на сервере
public void ChangeHealthValue(int newValue)
{
_SyncHealth = newValue;
if (_SyncHealth <= 0)
{
NetworkServer.Destroy(gameObject);
}
}
[Command] //обозначаем, что этот метод должен будет выполняться на сервере по запросу клиента
public void CmdChangeHealth(int newValue) //обязательно ставим Cmd в начале названия метода
{
ChangeHealthValue(newValue); //переходим к непосредственному изменению переменной
}
[Server]
void ChangeVector3Vars(Vector3 newValue)
{
_SyncVector3Vars.Add(newValue);
}
[Command]
public void CmdChangeVector3Vars(Vector3 newValue)
{
ChangeVector3Vars(newValue);
}
void SyncVector3Vars(SyncList<Vector3>.Operation op, int index, Vector3 oldItem, Vector3 newItem)
{
switch (op)
{
case SyncList<Vector3>.Operation.OP_ADD:
{
Vector3Vars.Add(newItem);
break;
}
case SyncList<Vector3>.Operation.OP_CLEAR:
{
break;
}
case SyncList<Vector3>.Operation.OP_INSERT:
{
break;
}
case SyncList<Vector3>.Operation.OP_REMOVEAT:
{
break;
}
case SyncList<Vector3>.Operation.OP_SET:
{
break;
}
}
}
public override void OnStartClient()
{
base.OnStartClient();
_SyncVector3Vars.Callback += SyncVector3Vars; //вместо hook для SyncList используем подписку на Callback
Vector3Vars = new List<Vector3>(_SyncVector3Vars.Count); //так как Callback действует только на изменение массива,
for (int i = 0; i < _SyncVector3Vars.Count; i++) //а у нас на момент подключения уже могут быть какие-то данные в массиве, нам нужно эти данные внести в локальный массив
{
Vector3Vars.Add(_SyncVector3Vars[i]);
}
}
[Server]
public void SpawnBullet(uint owner, Vector3 target)
{
GameObject bulletGo = Instantiate(BulletPrefab, transform.position, Quaternion.identity); //Создаем локальный объект пули на сервере
NetworkServer.Spawn(bulletGo); //отправляем информацию о сетевом объекте всем игрокам.
bulletGo.GetComponent<Bullet>().Init(owner, target); //инифиализируем поведение пули
}
[Command]
public void CmdSpawnBullet(uint owner, Vector3 target)
{
SpawnBullet(owner, target);
}
}
ЗаключениеНадеюсь эти примеры помогут разобраться с азами работы с сетью в Unity. Знатоков этой темы призываю к обсуждению недочетов (про производительность и GC сейчас речь не идет). Полный проект можно скачать на гитхабе по этой ссылке.
===========
Источник:
habr.com
===========
Похожие новости:
- [Python, Обработка изображений, Машинное обучение] И снова про капчу
- [.NET, C#] Уменьшить размер консольного .NET 5.0 приложения
- [Информационная безопасность] Palo Alto Networks. Учимся думать, как firewall. Сессии и состояния
- [Информационная безопасность, IT-инфраструктура, Сетевые технологии] 2020 Network Security and Availability Report
- [Разработка веб-сайтов, .NET, Компиляторы, C#, WebAssembly] Ahead-of-Time компиляция и Blazor
- [Разработка под iOS, Swift] Работа с сложными JSON-объектами в Swift (Codable)
- [Unity, Unreal Engine, Компьютерная анимация, Дизайн игр, Игры и игровые приставки] Игровые локации, или как сюжет раскрывается через окружение в современных играх
- [.NET, Машинное обучение] Посмотрим на девочек? Или ml.net в работе
- [Венчурные инвестиции, AR и VR, Игры и игровые приставки, Криптовалюты, Будущее здесь] Earth2: виртуальная реальность, новая криптовалюта, или мир NFTs?
- [Настройка Linux] Чиним резолвинг адресов в VPN-локалке (openconnect) для docker и systemd-resolved
Теги для поиска: #_c#, #_unity, #_unity, #_mirror, #_synclist, #_syncvar, #_network, #_c#, #_unity
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 15:45
Часовой пояс: UTC + 5
Автор | Сообщение |
---|---|
news_bot ®
Стаж: 6 лет 9 месяцев |
|
Хочу поделиться опытом с теми, кто хочет попробовать себя в написании сетевой игры, но не знает с чего начать. Так как информации по этой теме в интернете много, но полезную и актуальную было найти тяжело (а в русскоязычном сегменте и подавно), я решил собрать и структурировать то, что удалось найти.Итак, для написания сетевой игры на Unity сейчас есть несколько вариантов:
Мой выбор пал на Mirror, как на ближайший потомок UNet, использующий большинство принципов уже знакомого UNet. На примере простого проекта мы посмотрим основы Mirror, а именно:
using Mirror;
using System.Collections; using System.Collections.Generic; using UnityEngine; public class Player : NetworkBehaviour //даем системе понять, что это сетевой объект { void Update() { if (hasAuthority) //проверяем, есть ли у нас права изменять этот объект { float h = Input.GetAxis("Horizontal"); float v = Input.GetAxis("Vertical"); float speed = 5f * Time.deltaTime; transform.Translate(new Vector2(h * speed, v * speed)); //делаем простейшее движение } } }
Далее создаем скрипт NetMan.cs, создаем пустой GameObject (назовем его NetMan) и вешаем на него скрипт. Это будет наш скрипт, который отвечает за старт сервера и подключение игроков.Пока просто наследуем класс от NetworkManager, на этом этапе этого будет достаточно. using Mirror;
using System.Collections; using System.Collections.Generic; using UnityEngine; public class NetMan : NetworkManager { }
Первый этап готов. Выставляем выполнение в неполном экране (чтобы несколько экземпляров помещалось), делаем сборку, запускаем 2 экземпляра и проверяем. Один экземпляр стартуем как сервер, второй как клиент. На wasd двигаем своего персонажа, он успешно синхронизируется с другим экземпляром. 2. NetworkMessage и spawn игрока в выбранной точкеНа примере спавна в выбранной точке мы научимся отправлять сообщения на сервер.В настройках NetMan убираем галочку AutoCreatePlayer, дальше мы будем контролировать спавн игрока сами. Для этого мы изменим скрипт NetMan.cs. Начнем с создания struct с данными о позиции: public struct PosMessage : NetworkMessage //наследуемся от интерфейса NetworkMessage, чтобы система поняла какие данные упаковывать
{ public Vector2 vector2; //нельзя использовать Property } public void OnCreateCharacter(NetworkConnection conn, PosMessage message)
{ GameObject go = Instantiate(playerPrefab, message.vector2, Quaternion.identity); //локально на сервере создаем gameObject NetworkServer.AddPlayerForConnection(conn, go); //присоеднияем gameObject к пулу сетевых объектов и отправляем информацию об этом остальным игрокам } public override void OnStartServer()
{ base.OnStartServer(); NetworkServer.RegisterHandler<PosMessage>(OnCreateCharacter); //указываем, какой struct должен прийти на сервер, чтобы выполнился свапн } bool playerSpawned;
public void ActivatePlayerSpawn() { Vector3 pos = Input.mousePosition; pos.z = 10f; pos = Camera.main.ScreenToWorldPoint(pos); PosMessage m = new PosMessage() { vector2 = pos }; //создаем struct определенного типа, чтобы сервер понял к чему эти данные относятся connection.Send(m); //отправка сообщения на сервер с координатами спавна playerSpawned = true; } NetworkConnection connection;
bool playerConnected; public override void OnClientConnect(NetworkConnection conn) { base.OnClientConnect(conn); connection = conn; playerConnected = true; } private void Update() { if (Input.GetKeyDown(KeyCode.Mouse0) && !playerSpawned && playerConnected) { ActivatePlayerSpawn(); } } using Mirror;
using System.Collections; using System.Collections.Generic; using UnityEngine; public class NetMan : NetworkManager { bool playerSpawned; NetworkConnection connection; bool playerConnected; public void OnCreateCharacter(NetworkConnection conn, PosMessage message) { GameObject go = Instantiate(playerPrefab, message.vector2, Quaternion.identity); //локально на сервере создаем gameObject NetworkServer.AddPlayerForConnection(conn, go); //присоеднияем gameObject к пулу сетевых объектов и отправляем информацию об этом остальным игрокам } public override void OnStartServer() { base.OnStartServer(); NetworkServer.RegisterHandler<PosMessage>(OnCreateCharacter); //указываем, какой struct должен прийти на сервер, чтобы выполнился свапн } public void ActivatePlayerSpawn() { Vector3 pos = Input.mousePosition; pos.z = 10f; pos = Camera.main.ScreenToWorldPoint(pos); PosMessage m = new PosMessage() { vector2 = pos }; //создаем struct определенного типа, чтобы сервер понял к чему эти данные относятся connection.Send(m); //отправка сообщения на сервер с координатами спавна playerSpawned = true; } public override void OnClientConnect(NetworkConnection conn) { base.OnClientConnect(conn); connection = conn; playerConnected = true; } private void Update() { if (Input.GetKeyDown(KeyCode.Mouse0) && !playerSpawned && playerConnected) { ActivatePlayerSpawn(); } } } public struct PosMessage : NetworkMessage //наследуемся от интерфейса NetworkMessage, чтобы система поняла какие данные упаковывать { public Vector2 vector2; //нельзя использовать Property } 3. Синхронизация переменных посредством SyncVarПереходим к очень интересной фиче – SyncVar. Она позволяет избежать ручной синхронизации данных. Главное правило – меняем переменную только на сервере и не используем ее как данные (только как временное хранилище для данных, которые нам нужно обработать).Для начала подготовим объекты, которые мы будем использовать для наглядной синхронизации. Например, здоровье в виде красных кружков. Открываем редактирование префаба Player и добавляем ему несколько объектов, представляющих собой жизнь (Knob + красный цвет). Располагаем их так, чтобы было хорошо видно.Редактируем скрипт Player.cs, добавляем переменные: public int Health;
public GameObject[] HealthGos; Добавляем в Update обновление объектов-жизней в соответствии с количеством жизней: void Update()
{ ... for (int i = 0; i < HealthGos.Length; i++) { HealthGos[i].SetActive(!(Health - 1 < i)); } } [SyncVar(hook = nameof(SyncHealth))] //задаем метод, который будет выполняться при синхронизации переменной
int _SyncHealth; void SyncHealth(int oldValue, int newValue) //обязательно делаем два значения - старое и новое. { Health = newValue; } [Server] //обозначаем, что этот метод будет вызываться и выполняться только на сервере
public void ChangeHealthValue(int newValue) { _SyncHealth = newValue; } [Command] //обозначаем, что этот метод должен будет выполняться на сервере по запросу клиента
public void CmdChangeHealth(int newValue) //обязательно ставим Cmd в начале названия метода { ChangeHealthValue(newValue); //переходим к непосредственному изменению переменной }
[ClientRpc] //обозначаем, что этот метод будет выполняться на клиенте по запросу сервера
public void RpcTest() //обязательно ставим Rpc в начале названия метода { Debug.Log("Сервер попросил меня это написать"); } void Update()
{ if (hasAuthority) //проверяем, есть ли у нас права изменять этот объект { ... if (Input.GetKeyDown(KeyCode.H)) //отнимаем у себя жизнь по нажатию клавиши H { if (isServer) //если мы являемся сервером, то переходим к непосредственному изменению переменной ChangeHealthValue(Health - 1); else CmdChangeHealth(Health - 1); //в противном случае делаем на сервер запрос об изменении переменной } } ... } 4. Синхронизация переменных посредством SyncListСинхронизировать одну переменную это конечно хорошо, но для серьезных проектов нам понадобится инструмент посерьезнее. SyncList позволяет синхронизировать массивы данных. Разберемся с ним на примере сохранения пройденного пути по нажатию кнопки (просто для наглядности). Редактируем скрипт Player.cs по аналогии с SyncVar.Изменение массива на сервере: SyncList<Vector3> _SyncVector3Vars = new SyncList<Vector3>(); //В случае SyncList не нужно ставить SyncVar и задавать метод, это делается иначе
[Server] void ChangeVector3Vars(Vector3 newValue) { _SyncVector3Vars.Add(newValue); } [Command]
public void CmdChangeVector3Vars(Vector3 newValue) { ChangeVector3Vars(newValue); } public List<Vector3> Vector3Vars;
void SyncVector3Vars(SyncList<Vector3>.Operation op, int index, Vector3 oldItem, Vector3 newItem) { switch (op) { case SyncList<Vector3>.Operation.OP_ADD: { Vector3Vars.Add(newItem); break; } case SyncList<Vector3>.Operation.OP_CLEAR: { break; } case SyncList<Vector3>.Operation.OP_INSERT: { break; } case SyncList<Vector3>.Operation.OP_REMOVEAT: { break; } case SyncList<Vector3>.Operation.OP_SET: { break; } } } public override void OnStartClient()
{ base.OnStartClient(); _SyncVector3Vars.Callback += SyncVector3Vars; //вместо hook, для SyncList используем подписку на Callback Vector3Vars = new List<Vector3>(_SyncVector3Vars.Count); //так как Callback действует только на изменение массива, for (int i = 0; i < _SyncVector3Vars.Count; i++) //а у нас на момент подключения уже могут быть какие-то данные в массиве, нам нужно эти данные внести в локальный массив { Vector3Vars.Add(_SyncVector3Vars[i]); } } public GameObject PointPrefab; //сюда вешаем ранее созданные префаб Point
public LineRenderer LineRenderer; //сюда кидаем наш же компонент int pointsCount; void Update() { if (hasAuthority) //проверяем, есть ли у нас права изменять этот объект { ... if (Input.GetKeyDown(KeyCode.P)) { if (isServer) ChangeVector3Vars(transform.position); else CmdChangeVector3Vars(transform.position); } } ... for (int i = pointsCount; i < Vector3Vars.Count; i++) { Instantiate(PointPrefab, Vector3Vars[i], Quaternion.identity); pointsCount++; LineRenderer.positionCount = Vector3Vars.Count; LineRenderer.SetPositions(Vector3Vars.ToArray()); } } Скрипт Player.cs using Mirror;
using System.Collections; using System.Collections.Generic; using UnityEngine; public class Player : NetworkBehaviour //даем системе понять, что это сетевой объект { [SyncVar(hook = nameof(SyncHealth))] //задаем метод, который будет выполняться при синхронизации переменной int _SyncHealth; public int Health; public GameObject[] HealthGos; SyncList<Vector3> _SyncVector3Vars = new SyncList<Vector3>(); //В случае SyncList не нужно ставить SyncVar и задавать метод, это делается иначе public List<Vector3> Vector3Vars; public GameObject PointPrefab; //сюда вешаем ранее созданные префаб Point public LineRenderer LineRenderer; //сюда кидаем наш же компонент int pointsCount; void Update() { if (hasAuthority) //проверяем, есть ли у нас права изменять этот объект { float h = Input.GetAxis("Horizontal"); float v = Input.GetAxis("Vertical"); float speed = 5f * Time.deltaTime; transform.Translate(new Vector2(h * speed, v * speed)); //делаем простейшее движение if (Input.GetKeyDown(KeyCode.H)) //отнимаем у себя жизнь по нажатию клавиши H { if (isServer) //если мы являемся сервером, то переходим к непосредственному изменению переменной ChangeHealthValue(Health - 1); else CmdChangeHealth(Health - 1); //в противном случае делаем на сервер запрос об изменении переменной } if (Input.GetKeyDown(KeyCode.P)) { if (isServer) ChangeVector3Vars(transform.position); else CmdChangeVector3Vars(transform.position); } } for (int i = 0; i < HealthGos.Length; i++) { HealthGos[i].SetActive(!(Health - 1 < i)); } for (int i = pointsCount; i < Vector3Vars.Count; i++) { Instantiate(PointPrefab, Vector3Vars[i], Quaternion.identity); pointsCount++; LineRenderer.positionCount = Vector3Vars.Count; LineRenderer.SetPositions(Vector3Vars.ToArray()); } } void SyncHealth(int oldValue, int newValue) //обязательно делаем два значения - старое и новое. { Health = newValue; } [Server] //обозначаем, что этот метод будет вызываться и выполняться только на сервере public void ChangeHealthValue(int newValue) { _SyncHealth = newValue; } [Command] //обозначаем, что этот метод должен будет выполняться на сервере по запросу клиента public void CmdChangeHealth(int newValue) //обязательно ставим Cmd в начале названия метода { ChangeHealthValue(newValue); //переходим к непосредственному изменению переменной } [Server] void ChangeVector3Vars(Vector3 newValue) { _SyncVector3Vars.Add(newValue); } [Command] public void CmdChangeVector3Vars(Vector3 newValue) { ChangeVector3Vars(newValue); } void SyncVector3Vars(SyncList<Vector3>.Operation op, int index, Vector3 oldItem, Vector3 newItem) { switch (op) { case SyncList<Vector3>.Operation.OP_ADD: { Vector3Vars.Add(newItem); break; } case SyncList<Vector3>.Operation.OP_CLEAR: { break; } case SyncList<Vector3>.Operation.OP_INSERT: { break; } case SyncList<Vector3>.Operation.OP_REMOVEAT: { break; } case SyncList<Vector3>.Operation.OP_SET: { break; } } } public override void OnStartClient() { base.OnStartClient(); _SyncVector3Vars.Callback += SyncVector3Vars; //вместо hook для SyncList используем подписку на Callback Vector3Vars = new List<Vector3>(_SyncVector3Vars.Count); //так как Callback действует только на изменение массива, for (int i = 0; i < _SyncVector3Vars.Count; i++) //а у нас на момент подключения уже могут быть какие-то данные в массиве, нам нужно эти данные внести в локальный массив { Vector3Vars.Add(_SyncVector3Vars[i]); } } } 5. Spawn предмета и взаимодействие с нимНа последнем этапе мы посмотрим как спавнить предметы и взаимодействовать с ними. Добавим нашему игроку возможность сделать пулями.Создадим новый скрипт Bullet.cs: using Mirror;
using System.Collections; using System.Collections.Generic; using UnityEngine; public class Bullet : NetworkBehaviour { uint owner; bool inited; Vector3 target; [Server] public void Init(uint owner, Vector3 target) { this.owner = owner; //кто сделал выстрел this.target = target; //куда должна лететь пуля inited = true; } void Update() { if (inited && isServer) { transform.Translate((target - transform.position).normalized * 0.04f); foreach (var item in Physics2D.OverlapCircleAll(transform.position, 0.5f)) { Player player = item.GetComponent<Player>(); if (player) { if (player.netId != owner) { player.ChangeHealthValue(player.Health - 1); //отнимаем одну жизнь по аналогии с примером SyncVar NetworkServer.Destroy(gameObject); //уничтожаем пулю } } } if (Vector3.Distance(transform.position, target) < 0.1f) //пуля достигла конечной точки { NetworkServer.Destroy(gameObject); //значит ее можно уничтожить } } } } [Server]
public void SpawnBullet(uint owner, Vector3 target) { GameObject bulletGo = Instantiate(BulletPrefab, transform.position, Quaternion.identity); //Создаем локальный объект пули на сервере NetworkServer.Spawn(bulletGo); //отправляем информацию о сетевом объекте всем игрокам. bulletGo.GetComponent<Bullet>().Init(owner, target); //инициализируем поведение пули } [Command]
public void CmdSpawnBullet(uint owner, Vector3 target) { SpawnBullet(owner, target); } void Update()
{ if (hasAuthority) //проверяем, есть ли у нас права изменять этот объект { ... if (Input.GetKeyDown(KeyCode.Mouse1)) { Vector3 pos = Input.mousePosition; pos.z = 10f; pos = Camera.main.ScreenToWorldPoint(pos); if (isServer) SpawnBullet(netId, pos); else CmdSpawnBullet(netId, pos); } } ... } [Server] //обозначаем, что этот метод будет вызываться и выполняться только на сервере
public void ChangeHealthValue(int newValue) { _SyncHealth = newValue; if (_SyncHealth <= 0) { NetworkServer.Destroy(gameObject); } } Не забываем выставить префаб Bullet в переменную BulletPrefab префаба Player. Напоследок добавляем на префаб Player компонент CircleCollider2D и ставим галочку IsTrigger, чтобы пуля могла отловить попадание.Последний этап завершен. Проверяем. По нажатию правой кнопки мыши из игрока вылетает пуля и летит туда, где стоял курсор. Если по пути пуля встречает другого игрока – он теряет одну жизнь. Все пули синхронизированы, даже если игрок подключился после их спавна.Скрипт Player.cs using Mirror;
using System.Collections; using System.Collections.Generic; using UnityEngine; public class Player : NetworkBehaviour //даем системе понять, что это сетевой объект { [SyncVar(hook = nameof(SyncHealth))] //задаем метод, который будет выполняться при синхронизации переменной int _SyncHealth; public int Health; public GameObject[] HealthGos; SyncList<Vector3> _SyncVector3Vars = new SyncList<Vector3>(); //В случае SyncList не нужно ставить SyncVar и задавать метод, это делается иначе public List<Vector3> Vector3Vars; public GameObject PointPrefab; //сюда вешаем ранее созданные префаб Point public LineRenderer LineRenderer; //сюда кидаем наш же компонент int pointsCount; public GameObject BulletPrefab; //сюда вешаем префаб пули void Update() { if (hasAuthority) //проверяем, есть ли у нас права изменять этот объект { float h = Input.GetAxis("Horizontal"); float v = Input.GetAxis("Vertical"); float speed = 5f * Time.deltaTime; transform.Translate(new Vector2(h * speed, v * speed)); //делаем простейшее движение if (Input.GetKeyDown(KeyCode.H)) //отнимаем у себя жизнь по нажатию клавиши H { if (isServer) //если мы являемся сервером, то переходим к непосредственному изменению переменной ChangeHealthValue(Health - 1); else CmdChangeHealth(Health - 1); //в противном случае делаем на сервер запрос об изменении переменной } if (Input.GetKeyDown(KeyCode.P)) { if (isServer) ChangeVector3Vars(transform.position); else CmdChangeVector3Vars(transform.position); } if (Input.GetKeyDown(KeyCode.Mouse1)) { Vector3 pos = Input.mousePosition; pos.z = 10f; pos = Camera.main.ScreenToWorldPoint(pos); if (isServer) SpawnBullet(netId, pos); else CmdSpawnBullet(netId, pos); } } for (int i = 0; i < HealthGos.Length; i++) { HealthGos[i].SetActive(!(Health - 1 < i)); } for (int i = pointsCount; i < Vector3Vars.Count; i++) { Instantiate(PointPrefab, Vector3Vars[i], Quaternion.identity); pointsCount++; LineRenderer.positionCount = Vector3Vars.Count; LineRenderer.SetPositions(Vector3Vars.ToArray()); } } void SyncHealth(int oldValue, int newValue) //обязательно делаем два значения - старое и новое. { Health = newValue; } [Server] //обозначаем, что этот метод будет вызываться и выполняться только на сервере public void ChangeHealthValue(int newValue) { _SyncHealth = newValue; if (_SyncHealth <= 0) { NetworkServer.Destroy(gameObject); } } [Command] //обозначаем, что этот метод должен будет выполняться на сервере по запросу клиента public void CmdChangeHealth(int newValue) //обязательно ставим Cmd в начале названия метода { ChangeHealthValue(newValue); //переходим к непосредственному изменению переменной } [Server] void ChangeVector3Vars(Vector3 newValue) { _SyncVector3Vars.Add(newValue); } [Command] public void CmdChangeVector3Vars(Vector3 newValue) { ChangeVector3Vars(newValue); } void SyncVector3Vars(SyncList<Vector3>.Operation op, int index, Vector3 oldItem, Vector3 newItem) { switch (op) { case SyncList<Vector3>.Operation.OP_ADD: { Vector3Vars.Add(newItem); break; } case SyncList<Vector3>.Operation.OP_CLEAR: { break; } case SyncList<Vector3>.Operation.OP_INSERT: { break; } case SyncList<Vector3>.Operation.OP_REMOVEAT: { break; } case SyncList<Vector3>.Operation.OP_SET: { break; } } } public override void OnStartClient() { base.OnStartClient(); _SyncVector3Vars.Callback += SyncVector3Vars; //вместо hook для SyncList используем подписку на Callback Vector3Vars = new List<Vector3>(_SyncVector3Vars.Count); //так как Callback действует только на изменение массива, for (int i = 0; i < _SyncVector3Vars.Count; i++) //а у нас на момент подключения уже могут быть какие-то данные в массиве, нам нужно эти данные внести в локальный массив { Vector3Vars.Add(_SyncVector3Vars[i]); } } [Server] public void SpawnBullet(uint owner, Vector3 target) { GameObject bulletGo = Instantiate(BulletPrefab, transform.position, Quaternion.identity); //Создаем локальный объект пули на сервере NetworkServer.Spawn(bulletGo); //отправляем информацию о сетевом объекте всем игрокам. bulletGo.GetComponent<Bullet>().Init(owner, target); //инифиализируем поведение пули } [Command] public void CmdSpawnBullet(uint owner, Vector3 target) { SpawnBullet(owner, target); } } ЗаключениеНадеюсь эти примеры помогут разобраться с азами работы с сетью в Unity. Знатоков этой темы призываю к обсуждению недочетов (про производительность и GC сейчас речь не идет). Полный проект можно скачать на гитхабе по этой ссылке. =========== Источник: habr.com =========== Похожие новости:
|
|
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 15:45
Часовой пояс: UTC + 5