Серия уроков Создание онлайн игр / Уроки по 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:
Готово.