Как стать автором
Поиск
Написать публикацию
Обновить

Создание онлайн игр в Unity 3d с помощью Smartfox, часть 1

Вступление


Наверняка многие игроделы хотели написаю свою cms online игру, которая будет круче, ну, или, как минимум, ничуть не хуже всех существующих. В этой статье я рассмотрю создание основы: подсоединение игроков к серверу, использование UDP, подсоединение к комнате и создание комнаты + написание расширения для сервера. Так же хотел затронуть совместное перемещение по миру и синхронизация действий (в данном случае движения) между игроками, но объем статьи не позволяет. Все скрипты написаны на C#, за основу взят официальный пример Unity3D FPS Demo.

Хочу отдельно выделить серию видео туториалов 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. Должна получиться такая структура:
image
Далее переходим к редактированию скрипта, желательно использовать 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 наш шрифт с русским языком. Должно получиться следущее:
image

Вернемся к скрипту. Добавляем функцию 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 готов. Если сейчас запустить юнити, мы увидим следующую картину:
image

Теперь реализуем подключение к серверу

Для начала добавим обработчик нажатия кнопки «Соединиться»:

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», так же при запуске нашего приложения в юнити комната создается без вызывания исключения на сервере.

Вместо заключения


Для достижения задачи осталось достаточно много пунктов — менеджер сетевого общения, сетевых трансформаций, времени, движения, базовые классы игрока и «врага», а так же обработчики на сервере. Притом все это достаточно целостно и взаимосвязано, и делить код на части весьма затруднительно. Так что если тема заинтересует — выложу продолжение, полноценный серверный обработчик и клиентский менеждер отправки-приема сообщений.
Теги:
Хабы:
Данная статья не подлежит комментированию, поскольку её автор ещё не является полноправным участником сообщества. Вы сможете связаться с автором только после того, как он получит приглашение от кого-либо из участников сообщества. До этого момента его username будет скрыт псевдонимом.