Вступление
Наверняка многие игроделы хотели написаю свою
Хочу отдельно выделить серию видео туториалов Hack & Slash RPG — A Unity3D Game Engine Tutorial которая несет в себе огромное количество информации для новичков о работе с Unity, скриптинге под него и о тонкостях создания rpg.
Сервер
Первоначально выбор стоял между Photon, Electroserver и Smartfox.
Photon не понравился тем что работает он исключительно под Windows.
У Electroserver в этом плане лучше — есть версии под Windows 64\32, Unix и Mac. API оказалось вполне удачным и удобным, но из-за того что версия 5 вышла совсем недавно — документации, туториалов и примеров по нему минимум, все относится к версии 4. Еще оттолкнуло почти полное отсутствие комьюнити, хотя возможно просто плохо искал(на официальном форуме полное затишье). Но зато доступно написание расширений не только на java, но и на AS1 — возможно для кого то это станет ключевым моментом.
В итоге остался лишь Smartfox, а именно SmartFoxServer, 2X Community Edition. Он, как и ES, доступен для Windows, Unix, Mac и у каждой версии есть 32 и 64 битные редакции. Бесплатно доступно до 100 соединений к серверу, потом относительно гибкая ценовая политика. Есть апи для Flash, Unity 3D и iPhone, документации по клиентскому и серверному апи, примеры простых соединений, чата и полноценного 3д шутера, который и стал основой для этой статьи.
Начало работ
Скачать Smartfox можно тут. После установки доступна панель управления по адресу localhost:8080, там же ссылка на админпанель и несколько примеров кода для Flash и Unity. Я устанавливал сервер на Windows как службу, при таком типе установки для перезапуска сервера нужно лишь перезапустить sfs2s-service службу (перезапуск необходим для подключения расширений и обновления настроек сервера).
Разбирать устройство сервера я не буду, это достойно отдельно статьи, да и все хорошо написано в документации.
Клиентская сторона
Задачу клиента для удобства можно разбить на 2 фазы — авторизация на сервере и сама игра. Начнем с авторизации. Создайте новый проект, из пакетов необходимо выбрать Terrain Assets — для создания площади по которой будем бегать.
В окне проекта создайте папки Auth, Fonts, Scenes, Plugins. Сохраним нашу сцену в папку Scenes, назовем например Authorization. Далее нужно подключить серверный плагин — для этого идем в папку установки сервера, далее открываем Client\Unity\API, и перетаскиваем фаил SmartFox2.dll в папку Plugins нашего проекта. Для поддержки русского языка нужно подключить любой шрифт с русским языком — для этого просто перетаскиваем шрифт в папку Fonts, Unity сама отформатирует его. В папке Auth создаем папку Scripts, и в ней 2 новых c# скрипта — назовем их Authorization и SmartFoxConnection. Должна получиться такая структура:

Далее переходим к редактированию скрипта, желательно использовать Visual Studio или MonoDevelop. Сохранять файлы скриптов нужно в utf8 (для русского языка).
Далее приведу листинг скрипта SmartFoxConnection, он стандартный и служит для управления соединением с сервером. Скрипт создает объект, и в объекте хранится ссылка на соединение. Так же реализовано отключение при закрытии:
using UnityEngine;
using Sfs2X;
// Statics for holding the connection to the SFS server end
// Can then be queried from the entire game to get the connection
public class SmartFoxConnection : MonoBehaviour {
private static SmartFoxConnection mInstance;
private static SmartFox smartFox;
public static SmartFox Connection {
get {
if (mInstance == null) {
mInstance = new GameObject("SmartFoxConnection").AddComponent(typeof(SmartFoxConnection)) as SmartFoxConnection;
}
return smartFox;
}
set {
if (mInstance == null) {
mInstance = new GameObject("SmartFoxConnection").AddComponent(typeof(SmartFoxConnection)) as SmartFoxConnection;
}
smartFox = value;
}
}
public static bool IsInitialized {
get {
return (smartFox != null);
}
}
// Handle disconnection automagically
// ** Important for Windows users - can cause crashes otherwise
void OnApplicationQuit() {
if (smartFox.IsConnected) {
smartFox.Disconnect();
}
}
}
* This source code was highlighted with Source Code Highlighter.
Теперь перейдем к гораздо более интересному — классу авторизации пользователя
Для начала подключим все необходимые классы и зададим переменные:
using UnityEngine;
using System;
using System.Collections;
using System.Collections.Generic;
using Sfs2X;
using Sfs2X.Core;
using Sfs2X.Entities;
using Sfs2X.Requests;
public class Authorization : MonoBehaviour {
public Font myFont; //для русского языка
private SmartFox smartFox; //наш коннектор
private string serverName = "127.0.0.1"; //адрес сервера
private string serverPort = "9933"; //порт сервера
private string zone = "SimpleChat"; //название зоны
private string username = "User_"; //имя пользователя по умолчанию
private string password = "somePass"; //пароль по умолчанию
private string authErrorMessage = ""; //сообщение об ошибке
* This source code was highlighted with Source Code Highlighter.
Пароль мы пока использовать не будем, так как нет обработчика на стороне сервера, но зададим переменную для него и будем спрашивать пароль на GUI.
Теперь нужно написать функцию Start, в ней мы будем получать smartfox коннектор от SmartFoxConnection, и добавим к имени пользователя случайный набор символов — для быстрого теста нескольких окон:
void Start() {
if (SmartFoxConnection.IsInitialized) {
smartFox = SmartFoxConnection.Connection;
}
else {
smartFox = new SmartFox();
}
username += UnityEngine.Random.Range(100, 1000).ToString();
}
* This source code was highlighted with Source Code Highlighter.
Теперь пришла пора нарисовать GUI, за его отрисовку отвечает функция Unity OnGUI. Прежде чем начать -вернемся в юнити. Перетащим скрипт Authorization на нашу Main Camera, что бы он выполнялся при запуске, и перетащим на переменную My Font наш шрифт с русским языком. Должно получиться следущее:

Вернемся к скрипту. Добавляем функцию OnGUI:
void OnGUI() {
GUI.skin.font = myFont; //задаем шрифт
GUILayout.BeginArea(new Rect(Screen.width / 2 - 150,
Screen.height / 2 - 150, 300, 300), "", "box"); //рисуем область отображения
GUILayout.BeginVertical(); //начинаем вертикальную группу
GUILayout.Label("Адрес сервера"); //далее идут пары Label+TextField для считывания данных
serverName = GUILayout.TextField(serverName);
GUILayout.Label("Порт сервера");
serverPort = GUILayout.TextField(serverPort);
GUILayout.Label("Имя пользователя");
username = GUILayout.TextField(username, 24);
GUILayout.Label("Пароль пользователя");
password = GUILayout.PasswordField(password, "*"[0], 24);
//проверяем заполненность всех данных, если true - отображаем кнопку соединения
if (serverName != "" && serverPort != "" && username != "" && password != "") {
if (GUILayout.Button("Соединиться")) {
}
}
GUILayout.Label(authErrorMessage);
GUILayout.EndVertical();
GUILayout.EndArea();
}
* This source code was highlighted with Source Code Highlighter.
Наш GUI готов. Если сейчас запустить юнити, мы увидим следующую картину:

Теперь реализуем подключение к серверу
Для начала добавим обработчик нажатия кнопки «Соединиться»:
if (GUILayout.Button("Соединиться")) {
AddEventListeners();
try {
smartFox.Connect(serverName, int.Parse(serverPort));
}
catch (Exception e) {
authErrorMessage = e.ToString();
}
}
* This source code was highlighted with Source Code Highlighter.
Функция AddEventListeners отвечает за прием и перенаправление всех эвентов что приходят от сервера на наши обработчики. Добавим ее, и функцию которая удаляет все существующие listeners:
private void AddEventListeners() {
smartFox.RemoveAllEventListeners();
smartFox.AddEventListener(SFSEvent.CONNECTION, OnConnection);
//присоединяемся к серверу
smartFox.AddEventListener(SFSEvent.CONNECTION_LOST, OnConnectionLost);
//потеря соединения с сервером
smartFox.AddEventListener(SFSEvent.LOGIN, OnLogin);
//авторизовались на сервере
smartFox.AddEventListener(SFSEvent.LOGIN_ERROR, OnLoginError);
//ошибка авторизации с сервером
smartFox.AddEventListener(SFSEvent.LOGOUT, OnLogout);
//пришел запрос что произведен выход
smartFox.AddEventListener(SFSEvent.UDP_INIT, OnUdpInit);
//udp инициализирован
smartFox.AddEventListener(SFSEvent.ROOM_JOIN, OnJoinRoom);
//присоединились к комнате
smartFox.AddEventListener(SFSEvent.ROOM_CREATION_ERROR, OnCreateRoomError);
//ошибка создания комнаты
smartFox.AddEventListener(SFSEvent.ROOM_JOIN_ERROR, OnJoinRoomError);
//ошибка присоединения к комнате
}
private void UnregisterSFSSceneCallbacks() {
smartFox.RemoveAllEventListeners();
}
* This source code was highlighted with Source Code Highlighter.
Теперь необходимо лишь добавить обработчики для каждого события. Нам нужны только события авторизации и создания\входа в комнату, остальных сейчас касаться не будем
///EVENTS
public void OnConnection(BaseEvent evt) {
bool success = (bool)evt.Params["success"];
if (success) {
SmartFoxConnection.Connection = smartFox;
//отправляем наши имя, пустой пароль и название зоны для присоединения
smartFox.Send(new LoginRequest(username, "", zone));
}
else authErrorMessage = "On Connection callback got: " + success + " (error : <" + (string)evt.Params["errorMessage"] + ">)";
}
public void OnConnectionLost(BaseEvent evt) {
authErrorMessage = "OnConnectionLost";
UnregisterSFSSceneCallbacks();
}
public void OnLogin(BaseEvent evt) {
try {
if (evt.Params.ContainsKey("success") && !(bool)evt.Params["success"]) {
authErrorMessage = (string)evt.Params["errorMessage"];
}
else {
smartFox.InitUDP(serverName, int.Parse(serverPort));
}
}
catch (Exception ex) {
authErrorMessage = "Exception handling login request: " + ex.Message + " " + ex.StackTrace;
}
}
public void OnLoginError(BaseEvent evt) {
authErrorMessage = "Login error: " + (string)evt.Params["errorMessage"];
}
void OnLogout(BaseEvent evt) {
authErrorMessage = "Logout";
smartFox.Disconnect();
}
public void OnUdpInit(BaseEvent evt) {
if (evt.Params.ContainsKey("success") && !(bool)evt.Params["success"]) {
authErrorMessage = (string)evt.Params["errorMessage"];
}
else {
SetupRoomList();//берем список комнат
}
}
public void OnJoinRoom(BaseEvent evt) {
Room room = (Room)evt.Params["room"];
if (room.IsGame) {
UnregisterSFSSceneCallbacks();
authErrorMessage = "Joined game room " + room.Name;
}
}
public void OnCreateRoomError(BaseEvent evt) {
authErrorMessage = "Room creation error; the following error occurred: " + (string)evt.Params["errorMessage"];
}
public void OnJoinRoomError(BaseEvent evt) {
string error = (string)evt.Params["errorMessage"];
authErrorMessage = "Room join error; the following error occurred: " + error;
}
* This source code was highlighted with Source Code Highlighter.
К моменту выполнения функции SetupRoomList у нас есть рабочее соединение, произошел обмен «рукопожатиями» с сервером, мы авторизовались на нем и получили служебную информацию (все лежит в переменной smartFox), в том числе список комнат.
Теперь реализуем следующую логику: если игровых комнат на сервере нет-создаем новую и заходим в нее. Если же есть хотя бы 1 игровая комната (а по сути в нашей зоне может быть только игровая комната созданная нашим приложением) то присоединяемся к ней. За все это отвечает функция SetupRoomList:
private void SetupRoomList() {
//переменная хранит имя будущей комнаты
String roomName = "";
//получаем от smartfox список всех комнат и перебираем их
List allRooms = smartFox.RoomManager.GetRoomList();
foreach (Room room in allRooms) {
//нам нужны только игровые комнаты
if (!room.IsGame)
continue;
//если игровая комната все же есть - сохраняем ее имя и выходим из цикла
roomName = room.Name;
break;
}
//если игровых комнат не найдено - создадим свою
if (roomName=="") {
//задаем имя комнаты как имя авторизации + game
roomName = smartFox.MySelf.Name + " game";
//максимальное число пользователей - если кто то больше него
//попробует зайти-получит ошибку переполнения комнаты
int numMaxUsers = 100;
//теперь записываем все настройки нашей новой комнаты
RoomSettings settings = new RoomSettings(roomName);
settings.GroupId = "game";
settings.IsGame = true;
settings.MaxUsers = (short)numMaxUsers;
settings.MaxSpectators = 0;
//settings.Extension = new RoomExtension("sfsMmo", "mmo.MmoExtension");
smartFox.Send(new CreateRoomRequest(settings, true, smartFox.LastJoinedRoom));
}
else {//комната есть - заходим в нее
Debug.Log("Joining " + roomName);
smartFox.Send(new JoinRoomRequest(roomName));
}
}
* This source code was highlighted with Source Code Highlighter.
И наконец что бы все работало — добавим функцию направления всех событий на соответствующие эвенты, выполнять ее будем в FixedUpdate:
void FixedUpdate() {
smartFox.ProcessEvents();
}
* This source code was highlighted with Source Code Highlighter.
Если сейчас запустить скрипт должна появиться следующая надпись:
Если скомпилировать наше приложение (в Unity File->Build & Run) и запустить в 2 окна, то второе окно присоединится к комнате первого (после Joined game room будет одно и то же название).
Для нашей сцены авторизации остался последний момент — использование расширения на сервере. В любой онлайн игре нужны постоянные проверки на сервере действий игроков — движение, атака, взаимодействие с миром и предметами — все должно проверяться. Естественно что «из коробки» сервер не может делать такие проверки, да и не должен. Делать их мы будем сами, с помощью расширений. Расширения хранятся по пути %папка установки сервера%\SFS2X\extensions\имя расширения\jar фаил расширения. После добавления\обновления\удаления расширения лучше перезапускать сервер.
Обновим функцию SetupRoomList, заменим
settings.MaxSpectators = 0;
smartFox.Send(new CreateRoomRequest(settings, true, smartFox.LastJoinedRoom));
* This source code was highlighted with Source Code Highlighter.
на
settings.MaxSpectators = 0;
settings.Extension = new RoomExtension("sfsMmo", "mmo.MmoExtension");
smartFox.Send(new CreateRoomRequest(settings, true, smartFox.LastJoinedRoom));
* This source code was highlighted with Source Code Highlighter.
В функции new RoomExtension мы указываем название папки с расширением и путь до класса который нужно запускать первым.
Теперь сохраняем скрипт, возвращаемся в Unity и запускаем сцену. Если нажать «Соединиться» то произойдет соединение с сервером и появится подтверждающее сообщение. Но если мы откроем %папка установки сервера%\SFS2X\logs\smartfox.log то там можно найти запись:
Exception: com.smartfoxserver.v2.exceptions.SFSExtensionException
Message: Extension boot error. The provided path is not a directory: extensions/sfsMmo
Description: Failure while creating room extension.
Possible Causes: If the CreateRoom request was sent from client make sure that the extension name matches the name of an existing extension
* This source code was highlighted with Source Code Highlighter.
Серверная сторона
Для разработки на Java я использую Netbeans, но думаю в других средах не должно быть принципиальных отличий.
Для начала решим что должен уметь наш сервер.
1. Отдавать новому игроку transform точки появления
2. Принимать от игрока transform его движения, сверять с тем что был, производить проверки движения, рассылать всем игрокам новые координаты или же откатывать игрока на старые
3. Отправлять свое время для синхронизаций, пинга и т.п.
В этот раз рассмотрю только создание пустого расширения для сервера.
Создадим новый проект, назовем его MmoExtension. В нем сделаем mmo, в пакете класс MmoExtension. Названия естественно можно использовать свои, но тогда нужно в скрипте Authorization править метод new RoomExtension под себя.
Подключим к проекту апи Smartfox сервера. Нам нужны библиотеки sfs2x и sfs2x-core, лежат они в папке %папка установки сервера%\SFS2X\lib.
Меняем код класса MmoExtension на «пустышку»:
package mmo;
import com.smartfoxserver.v2.core.SFSEventType;
import com.smartfoxserver.v2.entities.Room;
import com.smartfoxserver.v2.entities.User;
import com.smartfoxserver.v2.entities.data.ISFSObject;
import com.smartfoxserver.v2.entities.data.SFSObject;
import com.smartfoxserver.v2.extensions.SFSExtension;
public class MmoExtension extends SFSExtension{
@Override
public void init() {
trace("MmoExtension Init");
}
}
* This source code was highlighted with Source Code Highlighter.
Сохраним класс, скомпилируем расширение через Clean & Build. Полученный файл MmoExtension.jar положим в папку SFS2X\extensions\sfsMmo. После перезапуска в логе сервера должна быть строка «MmoExtension Init», так же при запуске нашего приложения в юнити комната создается без вызывания исключения на сервере.
Вместо заключения
Для достижения задачи осталось достаточно много пунктов — менеджер сетевого общения, сетевых трансформаций, времени, движения, базовые классы игрока и «врага», а так же обработчики на сервере. Притом все это достаточно целостно и взаимосвязано, и делить код на части весьма затруднительно. Так что если тема заинтересует — выложу продолжение, полноценный серверный обработчик и клиентский менеждер отправки-приема сообщений.