Pull to refresh

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

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


Для достижения задачи осталось достаточно много пунктов — менеджер сетевого общения, сетевых трансформаций, времени, движения, базовые классы игрока и «врага», а так же обработчики на сервере. Притом все это достаточно целостно и взаимосвязано, и делить код на части весьма затруднительно. Так что если тема заинтересует — выложу продолжение, полноценный серверный обработчик и клиентский менеждер отправки-приема сообщений.
Tags:
Hubs:
You can’t comment this publication because its author is not yet a full member of the community. You will be able to contact the author only after he or she has been invited by someone in the community. Until then, author’s username will be hidden by an alias.