Инди
Netless
3388

Инди за 0$ на Unity. Часть 1: Консоль

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

В закладки
Аудио

Базовая архитектура приложения

Каждый разработчик сталкивался с тем что ему нужно переключить сцену или сохранить какое-то значение, но не понятно где это сделать. На помощь разработчику приходит глобальный объект, который хранит в себе эти данные и вроде бы все счастливы. Назовём этот объект Manager. Со временем появляются ещё данные, потом ещё и ещё... После утомительной работы над очередной фичей в нашей игре мы смотрим в код Manager'а и ахаем от 1000+ строк кода и полной вакханалии. И хорошо если ещё всё работает, но факта проблемы это не отменяет. Мы сидим на часовой бомбе, которая может рвануть в любой момент.

Всем же знакома данная ситуация? Как незамысловатым движением руки наш класс превращается в God Object. Чтобы избежать эту проблему мы должны с любовью относится к нашему продукту и следовать принципам SOLID.

Ближе к делу!

Для реализации игры я спроектировал следующую архитектуру:
Всего у нас будет 4 синглтона, отвечающие за ключевые функции программы - Console, GameMaster(AppMaster), InputSystem, SaveLoadSystem. Каждый из этих синглтонов отвечает за свой функционал и является глобальным в контексте моего приложения.

Console - Класс-синглтон, отвечающий за отрисовку и обработку команд пользователя, которые сам пользователь вводит в консоль. Благодаря консоли мы сможем делает разные финты ушами, тестировать некоторый функционал, читать логи прямо из игры и просто читерить. На самом деле это один из самых важных классов, потом поймёте почему.

GameMaster aka AppMaster - Синглтон, отвечающий за загрузку сцен, общее внутри-игровое состояние и состояние приложения вообще. Сам по себе класс будет иметь мало функционала, но от этого менее важным он не становится. Тут будут храниться все конфиги пользователя, ачивки и прочее.

InputSystem - Прослойка между встроенными ивентами ввода и нашей игрой. Стандартная система ввода юнити не позволяет нам переопределять бинды в рантайме или изменять тот чувствительность мыши. Для этих целей мы реализуем свою прослойку. Если вы пишите кросплатформенную игру (например для пк, консолей и мобильников), то такая задача будет суицидом, но т.к. я пишу игру только для пк, то всё будет проще.
Ремарка: Юнити работает над новой системой ввода, но на данный момент она находится в состоянии preview и доступна в experimental пространстве имён. Инпут система это не визуал и словив баг можно нехило закопаться в дебаггинге по этому я избегаю такие превью пакеты до релиза.

SaveLoadSystem - Сериализатор\десериализатор динамических данных. В игре я собираюсь реализовать систему диалогов, инвентарь и прогресс пользователя. Всё это надо где-либо хранить и как то переносить между игровыми сессиями. Для таких целей мы реализуем эту систему и будем использовать в нашей игре.

С помощью этих модулей мы сможем держать код в чистоте, а ноги в тепле. В консоли мы сможем определять кастомные комманды, в гейммастере мы будем держать все важные данные, инпут система позволит нам определять экшены и подписываться на них, а система сериализации позволит нам определять свои сценарии для сериализации\десериализации любых объектов. У меня будет несколько сцен, в которых я уже буду реализовывать бизнес-логику игры, отделённую от основных модулей. Помимо этих модулей мне предстоит реализовать подсистему диалогов, подсистему инвентаря и т.д., но об этом пока нет смысла думать.

Структура проекта

Все свои ассеты я складываю в папку /Assets/Game. Внутри этой папки у меня стандартная структура (Scripts, Sprites, Scenes, etc).

В корне своей папки я создаю Assembly Definition, которую прямо так и называю: Game. В папке со скриптами у меня есть поддиректория Tests, в которой я храню все тесты своих скриптов.

Структура на момент написания статьи. В Shit улетаю ассеты, которые не имеют серьёзной значимости, мусор или неудачные эксперименты. С некоторой периодичностью я её чищу, а перед релизом сношу полностью.

Давайте приступим к реализации одного из самых важных модулей - Console.

Что такое консоль и как её реализовать?

Консоль - это коммандный интерфейс игры. В консоли можно изменить какие-либо значения без изменения исходного кода программы. Так же в ней можно выводить отладочную информацию, что тоже важно при разработке сложных программ.

Реализовывать простейшую консоль я буду средствами UnityUI. Для этого создадим канвас, накинем туда ScrollView вместе с простым Text, Image и InputField. Картинку я использую для заднего фона, scrollview для лога консоли и Inputfield для пользовательского ввода. Не забываем сразу поставить канвасу высокий порядок отрисовки чтобы консоль всегда была поверх остального интерфейса.
Так же добавляем Text элементу компонент ContentSizeFitter для автоматического ресайза элемента и выставляем вертикальное выравнивание текста на bottom.

Пытливый читатель может сказать "Отдельный канвас? Зачем? Я вот всё в один общий канвас сую и просто играюсь с SetActive методом у объектов внутри канваса". Это не плохой подход, но по Best Practices стоит разделять элементы на канвасы т.к. при юнити перерисовывает весь канвас при любом изменении внутри него, что может быть ресурсоёмко в некоторых случаях.

Подробнее про Canvas Best Practices: https://unity3d.com/how-to/unity-ui-optimization-tips

Итак, у нас должно получиться что-то в таком духе:

Внешний вид, шрифты и расположение объектов сугубо на усмотрение разработчика. Главное чтобы консоль была растянута по горизонтали, а положение по вертикали выбирать только вам.

Когда UI элементы будут готовы, наступает время кода. Я весь свой код всегда сую в пространство имён Game. Таким образом я всегда знаю какой код мой, какой не мой и избегаю любых конфликтов имён. Для консоли я использую подпространство Game.Console.

Создаём класс Console в пространстве имён Game.Console:

using System.Collections; using System.Collections.Generic; using UnityEngine; using System; namespace Game.Console { public struct Command { public string description; public Delegate function; } public class Console : MonoBehaviour { #region Static public static Console instance = null; static Dictionary<string, Command> cmds = new Dictionary<string, Command>(); public static void RegisterCommand(string name, string desc, Delegate func) { if (cmds.ContainsKey(name)) { Debug.LogError("Command '" + name + "' is already in use"); return; } Command cmd = new Command(); cmd.description = desc; cmd.function = func; cmds.Add(name, cmd); } public static void UnregisterCommand(string name) { if (!cmds.ContainsKey(name)) { Debug.LogError("Command '" + name + "' is not registered yet"); return; } cmds.Remove(name); } #endregion #region Helpers public void PrintHelp() { foreach (string cmdName in cmds.Keys) { Print(cmdName + "\t" + cmds[cmdName].description); } } public void Print(string line) { logTMP.text += line + "\n"; } public void Print(object obj) { Print(obj.ToString()); } public void PrintError(string msg) { Print("<color=red>Error:</color> " + msg); } #endregion string motd = "Welcome to debug console. Type \"help\" for nothing\n"; public TMPro.TextMeshProUGUI logTMP; public TMPro.TMP_InputField InputField; bool isActive = false; void Start() { if (instance != null) { throw new Exception("Console instance dup detected!"); } instance = this; DontDestroyOnLoad(gameObject); SetState(isActive); Application.logMessageReceived += HandleLog; RegisterCommand("help", string.Empty, new Action(PrintHelp)); RegisterCommand("echo", "print arg to console", new Action<object>(Print)); RegisterCommand("dtf", string.Empty, new Action(() => { Print("Whatsup DTF?"); })); logTMP.text = motd; } // Временное решение пока не создали InputSystem private void Update() { if (Input.GetKeyDown(KeyCode.Tilde) || Input.GetKeyDown(KeyCode.BackQuote)) { SetState(!isActive); } } // Парсим инпут пользователя и выполняем комманду public void ProcessInput() { if (Input.GetKeyDown(KeyCode.Tilde) || Input.GetKeyDown(KeyCode.BackQuote)) { InputField.text = InputField.text.Substring(0, InputField.text.Length - 1); return; } string rawString = InputField.text; if (rawString.Length < 1) { Focus(); return; } Print("> " + rawString); InputProcessor processor = new InputProcessor(rawString); processor.Run(); InputField.text = ""; ExecuteCommand(processor.command, processor.arguments); Focus(); } // Метод, который выполняет комманду и отлавливает ошибки public void ExecuteCommand(string cmdName, object[] args) { Command cmd; if (!cmds.TryGetValue(cmdName, out cmd)) { PrintError("Command not found."); return; } try { cmd.function.DynamicInvoke(args); } catch (System.Reflection.TargetParameterCountException ex) { PrintError("Incorrect number of arguments"); } } // Коллбек для перехвата сообщений от встроенного в юнити логгера public void HandleLog(string logString, string stackTrace, LogType type) { Print(logString); } public void SetState(bool state) { isActive = state; foreach (Transform child in transform) child.gameObject.SetActive(isActive); if (isActive) Focus(); } public void Focus() { InputField.ActivateInputField(); } } }

Класс Console имеет ссылку на себя в статической переменной instance. Есть статические методы ReginsterCommand\UnregisterCommand, позволяющие мне регистрировать\удалять команды из любой точки проекта.

Кидаем этот класс на наш канвас, присваиваем через инспектор ему инпут филд и лог. В инпутфилде указываем коллбек Console.ProcessInput на событие OnEndEdit для того чтобы перехватывать ввод пользователя.

Обработка ввода

Дальше нам нужно обрабатывать данные, которые пользователь вводит в консоль. По факту мы получаем String который нам нужно распарсить, вырвав оттуда название консольной команды и аргументы для её выполнения. Т.к. мы хотим иметь гибкость при объявлении команд, используя в аргументах комманды не только строки, но и другие объекты, нам нужно в рантайме определять тип аргумента и тайпкастить его строку в нужный тип.

Для этого мы напишем класс InputProcessor

using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Text.RegularExpressions; using System.Threading.Tasks; using UnityEngine; namespace Game.Console { public class InputProcessor { private string input = string.Empty; public string command; public object[] arguments { get { return parsedArguments.Select(arg => arg.value).ToArray(); } } private List<Argument> parsedArguments = new List<Argument>(); private bool valid = false; public InputProcessor(string rawInput) { input = rawInput; } public void Run() { string[] splittedInput = input.Split(new char[] { ' ' }, 2); command = splittedInput[0]; if (splittedInput.Length == 1) { // there is no additional args, only single command name valid = true; return; } string rawArgs = splittedInput[1]; parsedArguments = ParseArguments(rawArgs); valid = true; } private List<Argument> ParseArguments(string rawArgs) { List<Argument> result = new List<Argument>(); string args = rawArgs; while(args != string.Empty) { Argument arg = ExtractArgument(ref args); result.Add(arg); } return result; } Regex stringRegex = new Regex(@"^([""])((?=(\\?)).)*?\1"); //https://stackoverflow.com/questions/171480/regex-grabbing-values-between-quotation-marks Regex intRegex = new Regex(@"^([\d]+)"); Regex floatRegex = new Regex(@"^(\d*\.\d*)"); Regex boolRegex = new Regex(@"^(true|false)"); Regex cleanFirst = new Regex(@"^\s?"); public Argument ExtractArgument(ref string rawArgs) { string arg = null; rawArgs = cleanFirst.Replace(rawArgs, ""); Match match = stringRegex.Match(rawArgs); if (match.Success) { arg = match.Value.Substring(1, match.Value.Length -2); rawArgs = stringRegex.Replace(rawArgs, ""); return new Argument(arg, ArgumentType.String); } match = boolRegex.Match(rawArgs); if (match.Success) { arg = match.Value; rawArgs = boolRegex.Replace(rawArgs, ""); return new Argument(arg, ArgumentType.Boolean); } match = floatRegex.Match(rawArgs); if (match.Success) { arg = match.Groups[1].Value; rawArgs = floatRegex.Replace(rawArgs, ""); return new Argument(arg, ArgumentType.Float); } match = intRegex.Match(rawArgs); if (match.Success) { arg = match.Groups[1].Value; rawArgs = intRegex.Replace(rawArgs, ""); return new Argument(arg, ArgumentType.Integer); } throw new Exception("Failed to extract argument"); } public bool IsValid() { return valid; } } }

Этот класс принимает в конструкторе строку и при вызове метода Run парсит её. Если что-либо идёт не так, то мы получаем Exception и прерываем выполнение кода.

Т.к. данный класс довольно критический, я решил покрыть его тестами ещё на стадии написания. Благодаря тестам я всегда уверен, что он работает и это греет мне душу, а так же позволяет спать ночами.

Рекомендую сохранить эти тесты и запускать при любом апдейте процессора дабы спать спокойно как я.

using System.Collections; using System.Collections.Generic; using NUnit.Framework; using UnityEngine; using UnityEngine.TestTools; using Game.Console; namespace Tests { public class InputProcessorTest { string i_justCommand = "help"; string i_commandAndArgument = "echo 123"; [Test] public void InputProcessorTestSimplePasses() { Game.Console.InputProcessor processor = new InputProcessor(i_justCommand); processor.Run(); Assert.IsTrue(processor.IsValid()); Assert.AreEqual(0, processor.arguments.Length); } [Test] public void InputProcessorTestCommandWithArg() { Game.Console.InputProcessor processor = new InputProcessor(i_commandAndArgument); processor.Run(); Assert.IsTrue(processor.IsValid()); Assert.AreEqual(1, processor.arguments.Length); } [Test] public void TestExtractArgument_String() { Game.Console.InputProcessor processor = new InputProcessor(i_justCommand); string args = "\"test\" 123 1.23"; string argsNew = args; Argument arg = processor.ExtractArgument(ref args); Assert.AreEqual("test", arg.value); Assert.AreNotEqual(args, argsNew); } [Test] public void TestExtractArgument_CombinedString() { Game.Console.InputProcessor processor = new InputProcessor(i_justCommand); string args = "\"test 123 something here 3.14\" 123 1.23"; string argsNew = args; Argument arg = processor.ExtractArgument(ref args); Assert.AreEqual("test 123 something here 3.14", arg.value); Assert.AreNotEqual(args, argsNew); } [Test] public void TestExtractArgument_Int() { Game.Console.InputProcessor processor = new InputProcessor(i_justCommand); string args = "420 \"test\" 3.14"; string argsNew = args; Argument arg = processor.ExtractArgument(ref args); Assert.AreEqual(420, arg.value); Assert.AreNotEqual(args, argsNew); } [Test] public void TestExtractArgument_Float() { Game.Console.InputProcessor processor = new InputProcessor(i_justCommand); string args = "32.14 420 \"test\""; string argsNew = args; Argument arg = processor.ExtractArgument(ref args); //Assert.AreEqual(32.14, arg.value); float actualDelta = System.Math.Abs(32.14f - (float)arg.value); float minDelta = 0.01f; Assert.IsTrue(actualDelta <= minDelta); Assert.AreNotEqual(args, argsNew); } [Test] public void TestExtractArgument_Boolean() { Game.Console.InputProcessor processor = new InputProcessor(i_justCommand); string args = "true \"test 123 something here 3.14\" 123 1.23"; string argsNew = args; Argument arg = processor.ExtractArgument(ref args); Assert.AreEqual(true, arg.value); Assert.AreNotEqual(args, argsNew); args = "false \"test 123 something here 3.14\" 123 1.23"; argsNew = args; arg = processor.ExtractArgument(ref args); Assert.AreEqual(false, arg.value); Assert.AreNotEqual(args, argsNew); } } }

Этот тест файл я не рефакторил, рекомендую это сделать при расширении функционала процессора.

Итог

Мы разработали консоль, которая позволяет нам объявлять, удалять команды, парсит, тайпкастит аргументы комманд и перехватывает события встроенного в юнити логгера. Теперь мы можем в любом компоненте объявить комманды, которые будут доступны для вызова в консоли, например:

private void OnEnable() { Console.RegisterCommand("pl_ms", "set player movement speed", new System.Action<float>(SetSpeed)); } private void OnDisable() { Console.UnregisterCommand("pl_ms"); } public void SetSpeed(float val) { Debug.Log("Overriding mov speed. Was " + MovementSpeed + ", new " + val); MovementSpeed = val; }

Таким незамысловатым способом мы можем отлаживать\тестировать нужный нам функционал и это будет быстрее чем мы будем отрисовывать кастомные InspectorGUI элементы.

Домашнее задание

  • Объявить кастомный exception при ошибке парсинга аргументов в InputProcessor и отлавливать его в Console. Сейчас при фейле консоль ничего не говорит о том, что ей не удалось определить тайп аргумента. Нужно это исправить.
  • Стилизация лога. Все сообщения из встроенного в юнити логгера приходят прямо в консоль как есть. Было бы круто сделать стилизацию по типу лог мессейджа.
  • Тайпкастинг boolean сейчас case-sensitive. Нужно бы это исправить и дополнить тесты.

Что дальше?

В следующей главе мы создадим InputSystem для объявления кастомных экшенов и прослушивания событий по этим экшенам.

Материал опубликован пользователем.
Нажмите кнопку «Написать», чтобы поделиться мнением или рассказать о своём проекте.

Написать
{ "author_name": "Netless", "author_type": "self", "tags": ["region","endregion"], "comments": 69, "likes": 90, "favorites": 216, "is_advertisement": false, "subsite_label": "indie", "id": 65645, "is_wide": false, "is_ugc": true, "date": "Fri, 23 Aug 2019 18:24:45 +0300", "is_special": false }
0
{ "id": 65645, "author_id": 116280, "diff_limit": 1000, "urls": {"diff":"\/comments\/65645\/get","add":"\/comments\/65645\/add","edit":"\/comments\/edit","remove":"\/admin\/comments\/remove","pin":"\/admin\/comments\/pin","get4edit":"\/comments\/get4edit","complain":"\/comments\/complain","load_more":"\/comments\/loading\/65645"}, "attach_limit": 2, "max_comment_text_length": 5000, "subsite_id": 64960, "last_count_and_date": null }
69 комментариев
Популярные
По порядку
Написать комментарий...
12

Удивили. Статья о настоящем, голом геймдеве на DTF ^_^

Из быстрых советов при беглом просмотре могу посоветовать делать инстансы реальными синглтонами, которые при первом запросе создают себя сами и таким образом их дублирование невозможно (способ есть даже для моно-бехов) И вешать на сцену их не придется

Ответить
–8

Вы геймдев с программированием перепутали.

Ответить
1

Геймдев включает много всего, почему бы не написать и об этой его части?

Ответить
2

Ничего не мешает, да.
К примеру осветители - важный элемент кино. Но называть их "чистым кино" - это просто космос.

Чистый геймдев - это, по определению, геймдизайн, Карл. Люди такие - геймдизайнеры. А код пишут программисты, которым в 95% случаев вообще все равно кто им дал задачу - ГД или бизнес-аналитик.

Ответить
1

А, я думал, оспаривается целесообразность всей статьи, и только сейчас заметил, что это был ответ на комментарий "статья о настоящем, голом геймдеве". Тогда всё правильно! Поддерживаю!

Ответить
1

Так, я не понял. Почему так мало плюсов и комментариев? Это ж свежевыжатая статья о геймдеве на портале, посвящённом геймдеву.

Ответить
7

Потому что программисты приходят сюда чтобы морально деградировать в комментариях, а не продолжать думать о коде.

Ответить
1

Судя по комментам, потому как эта статья понятно только тем, кто и так это знает. А тем кто это не знает - статья совершенно бесполезна.
Я вот решил изучить статью, чтобы что-то почерпнуть для себя и возможно применить на UE4 (методологию например). Но ничего из этого не вышло: что такое солид не понятно (даже если изучить статью в вики, да), что такое синглтон - тоже. Т.е. по умолчанию я уже должен был разобраться, а если я с этим всем разобрался и всё это умею, то какая ценность этой статьи для меня?
Короче, да хотелось бы чтобы статьи в геймдеве были похожи на эту - т.е. было написано что конкретно делается, но хотелось бы ещё и понимать что происходит.

Ответить
0

Зато какая-никакая стартовая площадка для самообразования. Всё досконально разжёвывать тоже не гуд.

Ответить
1

Я согласен с тезисом, но только вне контекста. Просто, проблема большинства мануалов в том, что они либо совсем очевидные, либо слишком требовательны к опыту.

Ответить
1

На мой взгляд, начинать разработку инди игры (особенно когда ты работаешь один и крайне ограничен в человеко-часах) с создание консоли - несколько странно. Но это мое ИМХО конечно...

Ответить
0

Да ладно, на эту консоль уйдут день-два. По сравнению с тем, сколько времени уйдёт на остальную игру, это песчинка на просторах Вселенной :D

Ответить
0

Потому что не занёс как Playrix (¬‿¬ )

Ответить
0

Потому что даже в gamedev-подсайте читают в основном развлекательный контент:

Ответить
0

О, Gone Home.

Ответить
3

По мелочи. В методе UnregisterCommand нет смысла проверять с помощью ContainsKey - лишняя проверка. Remove же bool возвращает. Если элемента нет, то будет false.

Ответить
0

Тоже верно. Это просто дело привычки при работе с коллекциями явно проверять наличие элемента

Ответить
0

От плохих привычек нужно избавляться (:

Ответить
0

Не могу сказать, что это плохая привычка. Например в том же C, при работе с массивами это спасает жёпу от OutOfBound исключений

Ответить
1

Это плохая привычка, т.к. лишняя проверка. Delete в C# внутри и так это проверят. Если элемента нет, то оно вернёт false. А C лучше не упоминать всуе.

Ответить
0

точнее это спасает от того, что вы вываливаетесь за границы массива и начинаете творить вакханалию в памяти и т.д. и т.п. но суть я думаю вы уловили ;)

Ответить
3

Мне кажется это какое то грузилово, для обучения оно не подходит, если только для обучения продвинутых юзеров которые это всё и так знают.

Ответить
2

Console - Класс-синглтон

GameMaster aka AppMaster - Синглтон

А так хорошо статья начиналась (。•́︿•̀。)

Ответить
0

А в чём проблема?

Ответить
4

Это классический холливар на тему синглтон противоречит солиду. Я просто их уже наелся и не вижу смысла холиварить, но вы можете поучаствовать

Ответить
1

А почему не используете какую-нить DI/IoC либу?

Ответить
8

А зачем? Я пишу один и целью ставлю не гнаться за кол-вом строчек кода, библиотек, чего-либо ещё, а написать простую инди-игру без ассет флипов.

Инди не о качестве кода, не о программировании как таковом. Инди это поиск компромиссов между скоростью и качеством в масштабах людей с ограниченными возможностями (т.е. времени, ресурсами и т.д.)

Ответить
1

Хоть я за использование синглтонов там, где, они уместны (мы же игру хотим делать, а не соревноваться "кто сделает более SOLIDную архитектуру"), но зачем GameMaster делать синглтоном - не совсем понятно.
У меня это обычно просто сущность, которая инкапсулирует другие элементы, делает начальную инициализацию, возможно выступает посредником между другими игровыми сущностями. К нему при этом никто не обращается

Ответить
2

Чаще всего наличие в явном виде синглотонов говорит о плохой архитектуре. DI тогда уж заюзали бы. Тот же Zenject (хотя я его и недолюбливаю).

Ответить
1

Дело не в том, как мы получаем доступ к классу, а в том, как мы его используем.
Если гейм менеджер используется как синглтон, а ссылка на него достаётся из di ферймворка, то разницы с синглтоном по сути нет никакой.

Ответить
1

Не знаю как в геймдеве, но в вебе синглтон антипаттерн и очень редко бывает нужен (я его юзал только когда надо было коннект к БД хранить), возможно тут тоже юзается неуместно. Хз на самом деле.

Ответить
1

Ничего себе, даже тесты О_О
Это вообще законно? Жму руку и плюсую! Netless молодец, делайте как Netless!

Ответить
1

"Инди за 0$ на Unity" - а как же ваше время (которое походу стоит прилично) ?

Ответить
1

В прошлой главе уже поднимался этот вопрос. Речь в заголовке не о полной стоимости разработки игры, а о тратах на ассеты. Иными словами, цикл статей посвящен разработке игры без ассет флипов

Ответить
0

Есть вопрос, почему подсайт Инди, а не Gamedev?

Ответить
1

Ширяев сказал постить здесь, каджит делает как сказал шшширяев.

Ответить
0

Речь в заголовке [...] о тратах на ассеты. Иными словами, цикл статей посвящен разработке игры без ассет флипов

Эм, но ведь зачастую [без трат на ассеты] = [ассет флипы]. Не очень понимаю суть объяснения.

Ответить
1

Ого, человек, пишущий на Unity, который знает про SOLID ヽ(°〇°)ノ

Ответить
1

Да ладно, много таких. Меня про SOLID на каждом собеседовании спрашивают. Просто статьи никто не пишет)

Ответить
0

Ещё бы научиться разделать model и view.

Ответить
0

Спасибо за материал! Плюсую.
Дайте, пожалуйста, совет, как лучше войти в программирование на юнити? Книги, курсы.

Ответить
2

Виталий дал правильный совет. Я вообще придерживаюсь главного правила: просто писать. Не знаете как что-либо сделать - гуглите. Научитесь как художник "срисовывать" по готовому чтобы потом "создавать" с нуля.

Материалов много, но кач-во хромает. Но понять это сложно пока не прочтёшь\изучишь материал.

Читайте всё, что попадается, осмысливайте и "пишите в стол" т.е. пишите разные элементы геймплея чисто для себя.

Ответить
1

Если есть базовые знания C#, начни с официальных туториалов от Юнити. Если нет, то сперва лучше подучить язык

Ответить
1

Если совсем нет никаких знаний о программировании, то рекомендую начать с Дж. Бонда "Unity и C#. Геймдев от идеи до реализации". Там в основном про геймдизайн и прототипирование, но и C# на базовом уровне рассматривается, плюс в последней главе есть примеры кода из реальных прототипов игр.

Если интересно еще больше углубиться в C#, .NET и вообще понимать, как программирование работает, то тогда Э. Троелсен "Язык программирования C# 7 и платформы .NET и .NET Core".

Ну и как писали выше, практика, туториалы + документация от Юнити.

Ответить
0

Спасибо! Взял на заметку.

Ответить
1

Ожидал всё таки увидеть больше про то, что было заявлено в интро. Сейчас больше похоже на какой-то девлог. Возможно просто не сошлись мои ожидания и то, что в итоге сейчас в статье. Привык такое видеть на хабре, но для dtf обычно нужно чуть больше более земных объяснений: скрипты в виде ссылок/под катом, чуть больше скринов из юнити, гифку с тем, что в итоге получилось. Но всё равно спасибо за статью. Ещё было бы круто добавить ссылки на предыдущие статьи цикла т.к. всё же это 2-я статья, а не первая.
Отдельно не понимаю зачем "это" в разделе "Инди". Начинающие игроделы поймут из этого примерно ничего. Чуть больше знающие сами могут такое уже собрать.

Ответить
0

Не в тему, но меня одного передергивает от "Доброго времени суток"?

Ответить
2

Доброго времени суток. Хотите поговорить об этом?

Ответить
0

Вроде зашёл на dtf, но оказался на хабре

Ответить
2

НЛО прилетело и опубликовало эту надпись здесь

Ответить
0

Это что-то невообразимое на ДТФ. Реальные примеры и советы, а не посты в духе "Хочу делать игру, с чего начать?".

Ответить
0

Из статьи осталось не понятно, что из себя представляет используемый класс Argument (это явно не класс из System.Activities), но в целом статья топ.

Ответить
–2

Успел прочитать только начало, но уже хватило.

Ради всего святого, бросайте наконец использовать синглтоны. Мало того, что это потом совершенно невозможно автоматически тестировать, так и понять зависимости разных систем без чтения кода невозможно.

Ответить
1

Мало того, что это потом совершенно невозможно автоматически тестировать

Так вы мухи от котлет отделяйте и всё будет прекрасно покрываться тестами. На примере статьи вы можете ещё отделить выполнение команд в отдельную сущность и покрыть её тестами.

бросайте наконец использовать синглтоны

Ну предложите тогда другой вариант реализации сущности в единичном числе на всей, например, сцене. Или во всём приложении.

Ответить
1

В чем разница между сервис локатором и синглтонами? Если сам класс используется одинаково.

Ответить
0

Иными словами вы предлагаете переложить всю ответственность на какой-нибудь сервис локатор? Разверните свой ответ, а то получается, что вы вроде и пытаетесь что-либо ответить, но делаете это как-то не особо вразумительно...

Ответить
0

Ответственность за что?

За то, чтобы достать какую-то вещь - на сервис-локатор подходящего типа, который передаётся при создании или инициализации по ссылке каждому объекту, которому нужен тот или иной сервис.

За то, чтобы вещь была в единственном экземпляре - на тот код, который будет точкой входой в игру или тест.

Ответить
0

Сервис-локатор противоречит solid.

За то, чтобы вещь была в единственном экземпляре - на тот код, который будет точкой входой в игру или тест.

Это сработает только если он будет инстанцироваться в этой самой точке входа. Для сущностей, которые инстанцируются в рантайме такое решение ничем не поможет.

Ответить
0

Но для некоторых задач они хороши же. Например, загрузка, кэширование, выдача по запросу, изменение и сохраниение каких-либо данных

Ответить
0

Ни для чего из этого синглтон не нужен.

Ответить
3

Вот же ж нашли себе священную корову и талдычат друг за другом как заведенные

Складывается ощущение, что свидетели секты антисинглтонов искренне верят, что чем яростнее они будут бросаться говном, тем они лучше как программисты

В чем, разумеется, ошибаются

Ответить
0

Встречное предложение: бросайте наконец писать академический код. В системах где используются синглетоны никто в принципе не собирается делать тестирование какое либо и это нормально (YAGNI). Да, и никто не умрет если код не будет качества "Эпический", он в таком проекте не нужен.

Ответить
0

Сообщение удалено

Ответить
0

Ваше Благородие, рядовой Void нихуя не понял!

Ответить
0

Респектую!

Ответить

Прямой эфир

[ { "id": 1, "label": "100%×150_Branding_desktop", "provider": "adfox", "adaptive": [ "desktop" ], "adfox_method": "createAdaptive", "auto_reload": true, "adfox": { "ownerId": 228129, "params": { "pp": "g", "ps": "clmf", "p2": "ezfl" } } }, { "id": 2, "label": "1200х400", "provider": "adfox", "adaptive": [ "phone" ], "auto_reload": true, "adfox": { "ownerId": 228129, "params": { "pp": "g", "ps": "clmf", "p2": "ezfn" } } }, { "id": 3, "label": "240х200 _ТГБ_desktop", "provider": "adfox", "adaptive": [ "desktop" ], "adfox": { "ownerId": 228129, "params": { "pp": "g", "ps": "clmf", "p2": "fizc" } } }, { "id": 4, "label": "Article Branding", "provider": "adfox", "adaptive": [ "desktop" ], "adfox": { "ownerId": 228129, "params": { "p1": "cfovz", "p2": "glug" } } }, { "id": 5, "label": "300x500_desktop", "provider": "adfox", "adaptive": [ "desktop" ], "adfox": { "ownerId": 228129, "params": { "pp": "g", "ps": "clmf", "p2": "ezfk" } } }, { "id": 6, "label": "1180х250_Interpool_баннер над комментариями_Desktop", "provider": "adfox", "adaptive": [ "desktop" ], "adfox": { "ownerId": 228129, "params": { "pp": "h", "ps": "clmf", "p2": "ffyh" } } }, { "id": 7, "label": "Article Footer 100%_desktop_mobile", "provider": "adfox", "adaptive": [ "desktop", "tablet", "phone" ], "adfox": { "ownerId": 228129, "params": { "pp": "g", "ps": "clmf", "p2": "fjxb" } } }, { "id": 8, "label": "Fullscreen Desktop", "provider": "adfox", "adaptive": [ "desktop", "tablet" ], "auto_reload": true, "adfox": { "ownerId": 228129, "params": { "pp": "g", "ps": "clmf", "p2": "fjoh" } } }, { "id": 9, "label": "Fullscreen Mobile", "provider": "adfox", "adaptive": [ "phone" ], "auto_reload": true, "adfox": { "ownerId": 228129, "params": { "pp": "g", "ps": "clmf", "p2": "fjog" } } }, { "id": 10, "disable": true, "label": "Native Partner Desktop", "provider": "adfox", "adaptive": [ "desktop", "tablet" ], "adfox": { "ownerId": 228129, "params": { "pp": "g", "ps": "clmf", "p2": "fmyb" } } }, { "id": 11, "disable": true, "label": "Native Partner Mobile", "provider": "adfox", "adaptive": [ "phone" ], "adfox": { "ownerId": 228129, "params": { "pp": "g", "ps": "clmf", "p2": "fmyc" } } }, { "id": 12, "label": "Кнопка в шапке", "provider": "adfox", "adaptive": [ "desktop", "tablet" ], "adfox": { "ownerId": 228129, "params": { "pp": "g", "ps": "clmf", "p2": "fdhx" } } }, { "id": 13, "label": "DM InPage Video PartnerCode", "provider": "adfox", "adaptive": [ "desktop", "tablet", "phone" ], "adfox_method": "createAdaptive", "adfox": { "ownerId": 228129, "params": { "pp": "h", "ps": "clmf", "p2": "flvn" } } }, { "id": 14, "label": "Yandex context video banner", "provider": "yandex", "yandex": { "block_id": "VI-250597-0", "render_to": "inpage_VI-250597-0-1134314964", "adfox_url": "//ads.adfox.ru/228129/getCode?pp=h&ps=clmf&p2=fpjw&puid1=&puid2=&puid3=&puid4=&puid8=&puid9=&puid10=&puid21=&puid22=&puid31=&puid32=&puid33=&fmt=1&dl={REFERER}&pr=" } }, { "id": 15, "label": "Баннер в ленте на главной", "provider": "adfox", "adaptive": [ "desktop", "tablet", "phone" ], "adfox": { "ownerId": 228129, "params": { "p1": "byudo", "p2": "ftjf" } } }, { "id": 17, "label": "Stratum Desktop", "provider": "adfox", "adaptive": [ "desktop" ], "auto_reload": true, "adfox": { "ownerId": 228129, "params": { "pp": "g", "ps": "clmf", "p2": "fzvb" } } }, { "id": 18, "label": "Stratum Mobile", "provider": "adfox", "adaptive": [ "tablet", "phone" ], "auto_reload": true, "adfox": { "ownerId": 228129, "params": { "pp": "g", "ps": "clmf", "p2": "fzvc" } } } ]