Серия уроков Создание онлайн игр / Уроки по Unreal Engine 4
#1 - создание listen сервера игры и главного меню
- https://youtu.be/uJ2ggQfMJq8
- Рассказ о различиях listen серверов от dedicated
- Создание проекта с шаблоном от 3 лица на версии движка 4.24 по следующему тз
- Для создания игры (хоста) в главном меню, при нажатии на кнопку "создать игру" вызывается функция "Open Level" c названием игровой карты и опцией "listen"
- Для подключения к серверу в главном меню есть поле для ввода IP-адреса сервера и кнопка "подключиться". При нажатии на кнопку "подключиться", вызывается функция "Open Level" куда в качестве названия карты подается IP-адрес.
#2 - создание выделенного (dedicated) сервера
- Учимся работать с аргументами приложения. Пишем простейшую консольную программу на си шарпе и выводим на экран ее аргументы.
static void Main(string[] args) { foreach (var item in args) { Console.WriteLine(item); } Console.ReadKey(); }
- Модифицируем проект игры. Убираем из главного меню кнопку "создать игру". Устанавливаем в настройках Server Default Map.
- Создаем два .bat файла для запуска игры и выделенного сервера
StartGame.bat
"C:\UE4_4.24.3_S\Engine\Binaries\Win64\UE4Editor.exe" "I:\OnlineGame3\OnlineGame.uproject" -game
StartServer.bat
"C:\UE4_4.24.3_S\Engine\Binaries\Win64\UE4Editor.exe" "I:\OnlineGame3\OnlineGame.uproject" -server -log -port=7778
- Собираем выделенный сервер
1. Меняем версию движка на движок собранный с исходников.
2. Если нужно, создаем любой с++ класс в проекте.
3. Создаем файл «[названиеПроекта]ServerTarget.cs» в папке "Source" проекта
using UnrealBuildTool; using System.Collections.Generic; public class [названиеПроекта]ServerTarget : TargetRules { public [названиеПроекта]ServerTarget(TargetInfo Target) : base(Target) { Type = TargetType.Server; DefaultBuildSettings = BuildSettingsVersion.V2; ExtraModuleNames.AddRange( new string[] { "[названиеПроекта]" } ); } }
4. Нажимаем «Generate Visual Studio project files».
5. Собираем игру
6. Собираем сервер
7. Копируем файлы сервера в игру
projectNameServer.exe, projectNameServer.exp, projectNameServer.lib
из
projectFolder/Binaries/Win64
в
compiledGameFolder/projectName/Binaries/Win64
#3 - Пересоздание проекта на новой версии движка 4.26, с поддержкой с++ ахитектуры и упорядоченой структурой папок
- О важности соответствия требуемой версии Visual Studio и рабочих нагрузок
- Переработка старого или создание нового 4.26 проекта
- Создание блупринт класса через промежуточный с++ класс
- Переопределение родительского класса у блупринта
- Пересоздание предыдущего проекта с с++ архитектурой и упорядоченой структурой папок
#4 - Git
#5 - Обзор плана на курс, что предстоит создать. Необходимые умения и список используемых технологий.
- План курса, фичи которые нужно воплотить:
- Веб сайт с регистрацией, авторизацией и ссылкой на скачивание клиента игры
- Вход игру под созданной учетной записью
- Создание и кастомизация персонажа (раса, пол)
- Получение списка персонажей
- Невозможность создания двух персонажей с одним и тем же ником
- Удаление персонажа
- Вход персонажем в игру
- Невозможность войти в игру одним и тем же персонажем/аккаунтом два раза одновременно с разных окон
- Отображение ника персонажа (по той же системе можно передать все остальное, квесты, инвентарь, разные 3д модели для разных рас и т. д.)
- Сохранение координат (по той же системе сохраняется все остальное) персонажа. При повторном заходе на сервер персонаж появится там же где он был на момент выхода из игры
- Игровой чат для всех игроков на сервере
- Схема входа в игру
- Список всех API методов логин сервера
Client safe methods:
Log In ( Send: login, pass, GameClientVersion. Receive: Status, UserId, UserToken. )
Log Out ( Send: UserId, UserToken. Recieve: Status. )
Read Slots ( Send: UserId, UserToken. Recieve: Status, List of characters. )
Create Character ( Send: UserId, UserToken, NickName, Race, Gender. Recieve: Status. )
Delete Chatacter ( Send: UserId, UserToken, CharacterId. Recieve: Status. )
Start Game ( Send: UserId, UserToken. Recieve: Status, ServerIp. )
Server only methods:
Read Character ( Send: ServerLogin, ServerPassword, CharacterId. Recieve: Status, Character. )
GlobalServerSync ( Send: ServerLogin, ServerPassword, SyncData. Recieve: Status, SyncData. )
Log Out And Save ( Send: ServerLogin, ServerPassword, Character. Recieve: Status. )
SyncData
{
List<Characters> Characters;
List<String> MustBeLogOutedUserIds;
}
MustBeLogOutedUserIds
{
Game Server to Login Server:
Users which was no safe disconnected, without LogOut method calls.
They are currently offline, but they cannot logIn anymore, cause in database they are still isOnline = true.
Method must set isOnline = false for all this users in database.
Login Server to Game Server:
Users which try to logIn with currently online accounts.
Game server must forced kick this users from game server and put them to MustBeLogOutedUserIds queue like non safe disconnected;
}
- Требуемые навыки:
- Unreal Engine - средний.
- C# language - средний.
- Mictosoft Asp.Net - базовый.
- Список используемых технологий:
- Unreal engine
- VaRest Unreal Engine plugin
- Asp.Net Core mvc + web api
- Asp.Net Core Entity Framework
- Asp.Net Core Identity
- Microsoft SQL Server
#6 - Устанавливаем Microsoft SQL Server и утилиту SQL Server Management Studio. Создаем пустой ASP.NET Core проект.
- Устанавливаем MSSQL Server и SQL Server Management Studio.
SQL Server установлен, теперь нужно установить SQL Server Management Studio
MSSQL Server и SQL Server Management Studio успешно установлены
- Создаем пустой ASP.NET Core проект.
Пустой ASP.NET Core проект создан.
#7 - Установка необходимых NuGet пакетов и требуемых client side библиотек.
- Установка необходимых NuGet пакетов
Небоходимые нам NuGet пакеты:
- Microsoft.EntityFrameworkCore.SqlServer - Entity Framework - ORM система, для работы с базой данных MSSQL Server которую мы установили на предыдущем уроке;
- Microsoft.EntityFrameworkCore.Tools - Дополнение к Entity Framework для автоматического создания базы данных на основании созданных нами классов, подход Code First;
- Microsoft.AspNetCore.Identity.EntityFrameworkCore - Общепринятая библиотека с функционалом для регистрации, авторизации, работы с правами пользователей;
- Microsoft.AspNetCore.Mvc.NewtonsoftJson - Библиотека для возможности создавать сложные JSON форматы;
- Swashbuckle.AspNetCore - Swagger. Замечательная библиотека для наглядного тестирования и документации WEB API.
Теперь нам нужно установить все остальные требуемые NuGet пакеты
- Установка необходимых client side библиотек из "cdnjs"
Требуемые библиотеки:
- twitter-bootstrap - для базового дизайна сайта
- jquery - просто так
Теперь точно так же нужно добавить вторую и последнюю библиотеку JQuery
libman.json:
{ "version": "1.0", "defaultProvider": "cdnjs", "libraries": [ { "library": "twitter-bootstrap@4.6.0", "destination": "wwwroot/lib/bootstrap/" }, { "library": "jquery@3.6.0", "destination": "wwwroot/lib/jquery/" } ] }
Готово.
#8 - Настройка файла appsettings.json, создания структуры папок и моделей сущностей
- Настройка appsettings.json
Файл appsettings.json в ASP.NET Core это конфигурационный файл, куда можно выносить необходимые переменные вместо того что бы их хадкодить. По-умолчанию данный файл выглядит так:
{ "Logging": { "LogLevel": { "Default": "Information", "Microsoft": "Warning", "Microsoft.Hosting.Lifetime": "Information" } }, "AllowedHosts": "*" }
Но нам нужно поместить внутри него:
- Строку подключения к базе данных
- IP-адрес игрового сервера Unreal Engine
- Требуемую версию клиента игры
Сейчас наш appsettings.json файл выглядит следующим образом:
{ "ConnectionStrings": { "DefaultConnection": "Server=localhost\\SQLEXPRESS;Database=testDB;Trusted_Connection=True;" }, "GameServerIPs": { "DefaultServer": "127.0.0.1:7777" }, "GameClienVersions": { "Current": "1" }, "Logging": { "LogLevel": { "Default": "Information", "Microsoft": "Warning", "Microsoft.Hosting.Lifetime": "Information" } }, "AllowedHosts": "*" }
- Создание структуры папок
Сейчас наш проект выглядит слеющим образом:
Нам же сейчас необходимо создать следующие папки:
- Создание моделей сущностей
Нам наобходимо создать два класа сущностей PlayerUser.cs и Character.cs
Давайте сначала создадим файл PlayerUser.cs в папке Models/Entities:
PlayerUser.cs:
using Microsoft.AspNetCore.Identity; namespace TutorialLoginServerV2.Models.Entities { public class PlayerUser : IdentityUser { public string AuthToken { get; set; } public bool IsOnline { get; set; } = false; public bool MustBeLogOuted { get; set; } = false; public bool IsBanned { get; set; } = false; } }
Теперь нам нужно создать второй и последний класс модели Character.cs в папке Models/Entities
Character.cs:
namespace TutorialLoginServerV2.Models.Entities { public class Character { public int Id { get; set; } public string OwnerId { get; set; } public string Nickname { get; set; } public int Race { get; set; } // 0 - Human, 1 - Elf, 2 - Orc public int Gender { get; set; } // 0 - Male, 1 - Female public int Experiance { get; set; } = 0; public float LocationX { get; set; } = 0.0f; public float LocationY { get; set; } = 0.0f; public float LocationZ { get; set; } = 0.0f; public float RotationX { get; set; } = 1.0f; public float RotationY { get; set; } = 1.0f; public float RotationZ { get; set; } = 1.0f; } }
Готово.
#9 - Создание пустого класса EngineManager и класса MainDbContext
- Создание пустого класса EngineManager
В предыдущем эпизоде мы создавали папку "Services" для модуля EngineManager.
Модуль EngineManager не является обязательным в нашем проекте, мы его создаем лишь для удобства, дабы инкапсулировать часть логики из контроллеров, что сделает код более чистым и удобочитаемым.
Модуль EngineManager будет подключен в механизм внедления зависимостей как синглтон.
Сегодня бы создадим пустую болванку класса EngineManager внутри папки "Services".
using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; namespace TutorialLoginServerV2.Services { public class EngineManager { } }
- Создание класса MainDbContext
Database Context Class (сокращенно DbContext) это часть добавленной нами в проект ранее Entity Framework Core ORM системы.
Этот класс является объектно-оринтированным представлением базы данных, как обычный c# объект.
Внутри него мы поместим классы сущностей, такие как PlayerUser.cs и Character.cs в качестве массивов. Точнее не обычных массивов, а специфических коллекций DbSet, параметризированных типами наших сущностей.
В будущем мы выполнем специфическую процедуру под названием "миграция" впоследствии чего внутри нашей базы данных Microsoft SQL Server будут автоматически созданы таблицы на основании наших сущностей.
Переменная DbSet<PlayerUser> создаст таблицу PlayerUser и переменная DbSet<Character> создаст таблицу Characters.
Когда мы закончим настройку приложения и базы, у нас появится возможность выполнять операции Создания, Чтения, Обновления и Удаления из базы с помощью обычных методов объекта Database Context. Когда-то нам бы пришлось вручную писать SQL-запросы.
using TutorialLoginServerV2.Models.Entities; using Microsoft.AspNetCore.Identity.EntityFrameworkCore; using Microsoft.EntityFrameworkCore; namespace TutorialLoginServerV2.Models { public class MainDbContext : IdentityDbContext { public MainDbContext(DbContextOptions<MainDbContext> options) : base(options) { } public DbSet<PlayerUser> PlayerUsers { get; set; } public DbSet<Character> Characters { get; set; } } }
Обратите внимание, что наш класс MainDbContext наследуется не от обычного DbContext, а от IdentityDbContext.
Так происходит потому что мы работаем не с чистым Entity Framework, а с Entity Framework в связке с системой Identity.
Если в двух словах, то созданный нами класс "MainDbContext" наследуется от класса "IdentityDbContext", а класс "IdentityDbContext" в свою очередь наследуется от класса "DbContext"
Так же как и EngineManager, MainDbContext класс будет подключен в механизм внедрения зависимостей.
Готово.
#10 - Создание класса Startup и инициализация базы данных
- Создание класса Startup
Класс Startup.cs является одним из важнейших классов в приложении asp.net core. Этот класс отрабатывает каждый раз при запуске приложения и настраивает его.
Внутри данного класса мы подгружаем файл настроек, добавляем сервисы, конфигурируем их, регистрируем их в механизм внедрения зависимостей, что бы их могли использовать другие классы, настраиваем шаблоны MVC и многое другое.
Сейчас ваш класс Startup.cs должен выглядить примерно следующим образом:
Но нам сегодня предстоит выполнить следующие действия в нем:
- Создадим конструктор в котором мы загрузим в переменную Configuration все настройки которые мы помещали в файл appsettings.json в 8 уроке. В этом нам поможет встроенная система внедрения зависимостей.
- Зарегистрируем созданный нами в предыдущем уроке EngineManager, в качестве сервиса, в механизме внедрения зависимостей.
- Зарегистрируем созданный нами в предыдущем уроке MainDbContext, в качестве сервиса, в механизме внедрения зависимостей. Собственно это является так же добавлением EntityFramework.
- Зарегистрируем добавленный в качестве NuGet пакета в 7 епизоде Identity, в качестве сервиса, в механизме внедрения зависимостей.
- Настроим систему Identity.
- Зарегистрируем систему Model View Controller, по факту сделаем наше приложение MVC.
- Зарегистрируем добавленный в 7 эпизоде, в качестве NuGet пакета Swagger.
- Активируем Swagger.
- Активируем Swagger UI.
- Активируем Developer Exception Page. По факту включим более подробные сообщения об ошибках, для более удобной отладки приложения.
- Откроем доступ для папки со статическими файлами "wwwroot".
- Активируем возможность аутентификации.
- Активируем возможность авторизации.
- Настроим маршрутизацию контроллеров MVC. Для примера если мы откроем сайт http://example.com/account/register тогда в нашем приложении запустится метод Register' внутри контроллера Account.
- Создадим специальный метод, которым мы добавим в нашу базу данных первого пользователя Админа, а так же создадим для него роль администратора.
Краткое содержание класса Startup.cs:
Полный класс Startup.cs:
using TutorialLoginServerV2.Models; using TutorialLoginServerV2.Models.Entities; using TutorialLoginServerV2.Services; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Identity; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.OpenApi.Models; using System; using System.Threading.Tasks; namespace TutorialLoginServerV2 { public class Startup { public IConfiguration Configuration { get; } public Startup(IConfiguration configuration) { Configuration = configuration; } public void ConfigureServices(IServiceCollection services) { services.AddTransient<EngineManager>(); services.AddDbContextPool<MainDbContext>(options => options.UseSqlServer(Configuration.GetSection("ConnectionStrings")["DefaultConnection"])); services.AddIdentity<PlayerUser, IdentityRole>().AddEntityFrameworkStores<MainDbContext>(); services.Configure<IdentityOptions>(options => { options.Password.RequiredLength = 6; options.Password.RequiredUniqueChars = 0; options.Password.RequireNonAlphanumeric = false; options.Password.RequireLowercase = false; options.Password.RequireUppercase = false; options.Password.RequireDigit = false; options.User.RequireUniqueEmail = true; options.SignIn.RequireConfirmedEmail = false; }); services.AddMvc(); services.AddSwaggerGen(c => { c.SwaggerDoc("v1", new OpenApiInfo { Title = "Login Server", Version = "v2" }); }); services.AddControllers().AddNewtonsoftJson(options => options.SerializerSettings.ReferenceLoopHandling = Newtonsoft.Json.ReferenceLoopHandling.Ignore); } public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IServiceProvider services) { app.UseSwagger(); app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "LoginServer v2")); app.UseDeveloperExceptionPage(); app.UseStaticFiles(); app.UseRouting(); app.UseAuthentication(); app.UseAuthorization(); app.UseEndpoints(endpoints => { endpoints.MapControllerRoute( name: "default", pattern: "{controller=Home}/{action=Index}"); }); //CreateDefaultAdminRoleAndAdminUser(services).Wait(); } private async Task CreateDefaultAdminRoleAndAdminUser(IServiceProvider serviceProvider) { RoleManager<IdentityRole> RoleManager = serviceProvider.GetRequiredService<RoleManager<IdentityRole>>(); UserManager<PlayerUser> UserManager = serviceProvider.GetRequiredService<UserManager<PlayerUser>>(); string loginForNewAdmin = "Administrator"; string emailForNewAdmin = "admin@mail.ru"; string passForNewAdmin = "grefdsrthdfgrbd45"; bool roleCheck = await RoleManager.RoleExistsAsync("Admin"); if (!roleCheck) { await RoleManager.CreateAsync(new IdentityRole("Admin")); } PlayerUser userCheck = await UserManager.FindByEmailAsync(emailForNewAdmin); if (userCheck == null) { PlayerUser newAdminUser = new PlayerUser { UserName = loginForNewAdmin, Email = emailForNewAdmin }; await UserManager.CreateAsync(newAdminUser, passForNewAdmin); await UserManager.AddToRoleAsync(newAdminUser, "Admin"); } } } }
Маршрут (route): "{controller=Home}/{action=Index}"); работает следующим образом, что бы открыть страницу по-умолчанию на нашем сайте, приложение попытается найти контроллер "Home" и запустить в нем метод "Index".
У нас пока еще не создан контроллер "Home" и потому если мы сейчас попытаемся запустить наше приложение, то мы получим ошибку 404 страница не найдена. Это норма, мы создадим данные классы в следующих видео.
- Инициализация базы данных
На данный момент мы уже создали модели сущностей, такие как PlayerUser.cs и Character.cs.
Мы создали модель контекста базы данных MainDbContext.cs внутри которой мы установили желаемые таблицы в базе данных на основании наших сущностей.
Мы поместили строку подключения к базе данных в файл appsettings.json, откуда в классе Startup.cs мы ее прочитали и записали в переменную Configuration.
Мы так же зарегистрировали наш класс контекста базы данных MainDbContext.cs внутри метода ConfigureServices внутри класса Startup.
services.AddDbContextPool<MainDbContext>(options => options.UseSqlServer(Configuration.GetSection("ConnectionStrings")["DefaultConnection"]));
Но тем не менее если мы зайдем в SQL Server Management Studio, то мы увидим, что никакой новой базы данных все еще не было создано.
И если мы попытаемся выполнить любой метод предполагающий обращение к базе данных, то мы увидим сообщение об ошибке.
Давайте разкомментируем следующую строку (что бы попытаться добавить пользователя Админа в базу данных):
//CreateDefaultAdminRoleAndAdminUser(services).Wait();
После чего запустим наше приложение..
Как вы можете помнить, в предыдущих уроках я говорил вам, что для автоматического создания базы данных на основании созданных c-sharp классов, мы должны выполнить некую магическу операцию под странным названием "миграция".
Что бы ее выпонить нужно сначала открыть "Package Manager Console":
После чего нам нужно выполнить нашу первую "миграцию":
Теперь мы можем открыть SQL Server Management Studio и проверить была ли создана наша база данных:
Давайте посмотрим на таблицу PlayerUsers:
Давайте еще раз разкомментируем следующую строку:
//CreateDefaultAdminRoleAndAdminUser(services).Wait();
После чего запустим приложение еще раз:
Теперь все работает!! Да, мы все еще видим ошибку "404 Error", но так и должно быть. Больше не выскакивают исключения, а значит наше приложение заработало с базой данных!
Давайте еще раз заглянем в таблицу PlayerUsers:
Готово.