Комментарии 51
А при реальной разработке игр (на Unity и вообще) тоже используется такой подход, когда логика пишется отдельным, абстрактным модулем и потом подключается к игровому движку, или это просто пример для статьи?
Нет. Так работает только какой-нибудь очень далекий от движка объект, например UI или дизайнерские задвики по AI. Вообще желательно всё писать поближе к движку, чтобы потом не оказаться с 1.5 fps. Иначе графику, физику, анимацию предеться очень оптимизировать и упрощать. Вообще даже бывает, что следят за ms, которые тратятся на обработку скриптов. И операции с матрицами и векторами в скрипты стараются не выносить.
Нет. Так работает только...
А вам не кажется, что это очень проектозависимо?
Ничто не мешает, к примеру, биндить физику Unity3d на модель, апдейтить модель, биндить модель к физике Unity3d — я почти так и делал, вполне удобно и на FPS влияние минимальное.
Но моя модель про анимации ничего не знала, не придумал как подружить их грамотно. Как у SpaceLab с этим дела обстоят? Есть ли необходимость в физике и анимациях на уровне проекта с игровой логикой?
core.Publish(new CrewLegBrokenEvent( crewMember ));
А в коде вьюшки что-то вроде:
public void On(CrewLegBrokenEvent ev) {
LaunchAnimation( ev.crewMember )
}
Таким образом у меня модель ничего не знает про рендер.
Это не подход вообще это очень низкий ентри левел. Писать код чтобы просто заработало.
Из минусов — некоторое усложнение серверной архитектуры, т.к. поднять инстанс game сервера и заставить его корректно общаться с клиентами и другими серверами — это не то же самое, что запустить логику игры из dll в отдельном потоке.
В своем обучающем pet project на unity3d тоже думаю о выносе логики в отдельную сборку, благо четко отделены неймспейсы Core и UnityLogic (по сути — модель и представление, как бы странно в контексте юнити это не звучало) и в Core от юнити только вектора да Assert'ы используются.
Вот только редактор имеет обыкновение пересоздавать файлы .sln и .csproj, поэтому добавить в решение отдельную сборку довольно сложно (хотя, вроде если сильно исхитриться, то можно), а значит скорее всего придется делать их вообще разными solution'ами, что слегка неудобно — одновременно редактировать и ядро и юнити логику не получится, возможны проблемы с подключением какого-нибудь Vector2 от юнити в этот внешний проект (не проверял, т.ч. может быть тут проблем нет).
Кроме того, не очень понятно, как не устроить в VCS спам обновлениями какого-нибудь Plugins/Core.dll, да и вручную постоянным копированием перекомпилированного Core заниматься не очень хочется.
Т.ч. пока активных действий не предпринимаю и Core внутри основного .csproj. Чтобы жить было веселее, встроил известный плагин/хак, позволяющий использовать сахар из C# 6, а для тестов воспользовался Unity Test Tools.
Но как-то костыльно это все( Все таки хочется выделить ядро в отдельный "чистый" проект, чтобы еще меньше зависеть от движка/удобнее тестировать и т.д. А у юнити проекта создать свои тесты и свои правила.
Но нужно, по крайней мере, автоматизировать сборку Plugins/Core.dll, причем кроссплатформенно. Хотя, вероятно это очень просто решить банальным output directory в проект юнити.
Вот только редактор имеет обыкновение пересоздавать файлы .sln и .csproj, поэтому добавить в решение отдельную сборку довольно сложно (хотя, вроде если сильно исхитриться, то можно), а значит скорее всего придется делать их вообще разными solution'ами, что слегка неудобно — одновременно редактировать и ядро и юнити логику не получится,
Я недавно придумал использовать для этого символические ссылки.
Создаёшь отдельный солюшен (CoreSolution) и проект (CoreProject) в Студии. Внутри CoreProject делаешь папочку Core и весь код хранишь в ней. А в проекте Unity в папку Assets кидаешь символическую ссылку на папку Core. В результате, писать и редактировать код можно и в CoreSolution, и в солюшене Unity, и даже одновременно.
Косяков такого подхода пока не заметил. Единственная неприятность ― если забыться и создать новый cs-файл в Unity внутри папки Core , то в CoreProject его потом надо будет добавлять вручную через Add -> Existing Item... Этот момент несколько раздражает.
Для удобной работы со ссылками есть расширение к Проводнику ― Shell Link Extension.
возможны проблемы с подключением какого-нибудь Vector2 от юнити в этот внешний проект (не проверял, т.ч. может быть тут проблем нет).
В этом плане всё нормально. UnityEngine.dll можно подключать к сторонним проектам и пользоваться всем его управляемым кодом. Правда, его там не особо много, в основном вектора и Mathf. Если нарвёшься на метод, который реализован в нативном коде, в худшем случае получишь исключение сразу в момент его вызова.
Вот такая конструкция добавит все файлы из поддерева в проект:
<ItemGroup>
<Compile Include="Core\**\*.cs" />
</ItemGroup>
Необязательно, кстати, симолические ссылки использовать — в проект можно и какой-нибудь ..\..\Core\**\*.cs
добавить.
У меня изначальная задумка была не столько для отделения «core-кода» от «не-core-кода», а скорее для того, чтобы вынести код, который я использую в каждом проекте Unity, куда-нибудь в одно место. Чтобы не хранить в десяти Unity-проектах десять копий одних и тех же, но чуть-чуть отличающихся исходников; потому что со временем перестаёшь понимать, где лежит более свежая версия, где более старая, и чем они отличаются.
Пробовал заводить для такого кода отдельный проект и билдить его dll в папку Assets текущего проекта Unity, но это неудобно. Оказалось проще кидать в Unity-проект линк на общую папку. В неё же можно положить какие-нибудь общие часто используемые ассеты, типа текстур в сеточку, в шахматную клеточку, в сеточку с пронумерованными ячейками для отладки uv-координат у геометрии.
Тут конечно появляется вероятность сломать что-нибудь в старом проекте, внеся изменения в общий код. Но пока в Unity не появится родная поддержка чего-нибудь типа NuGet, я думаю, это будет наименьшим злом.
Кстати, насколько я знаю, для подобных целей git submodule обычно используют. А чтобы не привязываться к структуре каталогов подмодуля можно объединиит подходы — в каком-нибудь ThirdParty хранить подмодуль и на него делать симлинк (или что-то другое, не знаю точно через что относительные ссылки делаются) в проект. По крайней мере, после клонирования на другую машину не потребуется доп. действий и вообще думать о зависимости.
Однако спасибо за ответы, оригинальный подход. Получается, в итоговый билд все компилируется Unity3d, но разработка и тестирование ядра ведется снаружи.
Надо будет попробовать, спасибо. Предполагаю, под линухом оба варианта должны работать абсолютно аналогично.
Чтобы жить было веселее, встроил известный плагин/хак, позволяющий использовать сахар из C# 6
Решарпер не так давно начал понимать туплы. Теперь можно жить ещё веселее и потихоньку начинать применять C# 7. Правда всерьёз только на Windows.
Это удобно, модульно, масштабируемо и конфигируруемо.
В противном же случае мы получим настоящий ад зависимостей и хрупки классы. Начнем что-нибудь исправлять и получим по лбу. В Unity3D собственно старая-новая система анимации (Меканим что ли она называется) вот прям ИМХО яркий пример того как не надо делать: если все делать как по учебнику, то анимация и игровая логика будут прибиты друг к другу гвоздями, фиг потом отдерешь.
Вся проблема только в том, как только продумать такую архитектуру.
Assert.IsInstanceOfType(new Core(), typeof(Core));
А делать Assert.IsEqual(2*2, 4)
вы не пробовали? Или там Assert.IsNotEqual(true, false)
?
Всегда начинаю писать тесты с assert true
.
Это же стандартная практика — проверить, что сам тестировочный фреймворк работает, проект корректно настроен и нужные классы импортируются.
Только для начальной проверки или тест отсается?
Если первое, то можно и сразу тест на логику писать. если прошел — все работает, все подгружено.
Если второе, то в каком классе лежит такой тест и один ли он там?
А если третье, то это тесты самого фреймвека и они должны лежать в проекте с ним и скорее всего они там и так есть.
Проверки типа Assert.IsInstanceOfType(new Core(), typeof(Core)) не несут никакой ценности, разве что проверить что CLR умеет правильно ассоциировать тип с экземпляром класса (думаю, это бы за 15 лет уже заметили).
Кто-нибудь знает наподобие игры, помимо Fallout Shelter?
В вашем случае это возможно, так как легко обходитесь без компонентов движка.
Есть парочка советов:
1) Словарь, где ключом является enum — это плохо. Лучше заменить на int, по необходимости этот enum приводить к int, это исключит ненужный боксинг/анбоксинг.
2) Не знаю, как часто вы используете LINQ, но от таких штук лучше отказаться. Лишние аллокации.
3) foreach для List лучше не использовать, опять же, лишние аллокации. Если редко вызывается, то ещё ладно, если +- каждый кадр, то плохо.
2) как ещё вы предлагаете узнать, содержится ли в коллекции интересующий нас элемент, кроме перебора всей коллекции?
3) как по-другому проходить по элементам коллекции? :) Под «лишними аллокациями» понимается создание енумератора? Да, он весит больше, чем интовый индекс, но не настолько, чтобы вызывать панику и бежать переписывать все циклы на for
ЕМНИП, там какой-то баг в рантайме, который давным-давно исправили в Mono, но никак не сольют в Unity.
2) Именно for.
3) Ещё раз, мы говорим про C#, про старую баженую Mono версию. Если енумератор создаётся +- каждый кадр, то это лишние аллокации, лишние нагрузки на gc, зачем?
Поправка: в Unity под капотом старый Mono. В новом используется реализация из referencesource и такой проблемы там нет.
Я так понимаю, в среднесрочной перспективе будут существовать три платформы: .Net Framework для Windows, .Net Core для трёх основных платформ и Mono для всего.
Фишка .Net Framework ― богатство функционала.
Фишка Mono ― встраиваемость и работа на всём, что движется.
Фишка .Net Core ― автономность, платформа идёт вместе с приложением.
В краткосрочной перспективе Mono переходит со своего компилятора на Roslyn, и все три ветви унифицируют свои библиотеки под флагом .Net Standard.
Примерно так я себе это представляю, но могу ошибаться.
http://www.hanselman.com/blog/WhatNETDevelopersOughtToKnowToStartIn2017.aspx
2) За LINQ-сахаром будет скрыт тот же for на самом деле: иного пути в поиске просто нет — надо перебирать элементы коллекции, разве что можно извратиться и сделать, например, бинарный поиск (сложность которого логарифмическая, а не линейная, т.е. работать он будет быстрее) или еще чего.
Да и вообще LINQ-инструкции сложно-отслеживаемые в дебаггере и имеют вредную особенность подталкивать к говнокоду (вот уже на многих проектах такое замечал), да и вообще, в какой-то момент времени код становится трудно-сопровождаемым. Так что да — берем и пишем процедуру поиска элемента в коллекции, хуже (по производительности) работать не будет, это точно.
3) for Добавлю еще, что foreach не совсем таки безопасный, я бы сказал, что он совсем не безопасный. Enumerator не отслеживает состояние родительской коллекции после своего создания и может вернуть (при перечислении) объекты, которых там уже нет. О чем собственно упоминается в msdn. Исключения вылетающие на foreach — наверное самые частые которые мне приходилось разгребать в чужом коде и заменять foreach на for в таких ситуациях.
Ну и да, применение foreach подразумевает, что:
- создается новый объект — итератор;
- на каждой итерации идет вызов метода MoveNext;
- на каждой итерации идет обращение к свойству Current, что равносильно вызову метода;
Конечно же родной JIT компиляторе в родной среде исполнения вумный и попытается соптимизировать. Но получается не всегда, а уж Mono так вообще такое не гарантирует (хотя последние версии пытаются), а древний Mono (вроде бы 2-ой) работающий под капотом Unity3D так вообще даже не попытается этого сделать. Хотя буквально пару месяцев назад вот наталкивался на ситуацию, когда foreach натурально сводил сума GC, который начинал молотить как сумашедший и всем становилось хорошо. И это было в .NET 4.6 под родимой виндой
В то время как глупый for просто обращается по индексу и все. Никаких премудростей и работать он будет везде одинаково, независимо от платформы, версии .NET, положения звезд, фазы Луны и т.д.
4) Я еще добавлю про var. Во-первых, опять таки это делает код трудно читаемым
натыкаешься на:
var a = Petja.GetUnknowVar();
И сидишь и думаешь, а это что вообще такое? Строка, циферка или какой-то класс, а может структура? Лезешь внутрь метода, а там снова Var на Var'e и Var'ом погоняет. Так и сидишь, ковыряешься в чужих потрохах.
Реально, var оправдан только тогда, когда нужно работать со всякими анонимнымными типами и прочих лямбда-ситуациях, т.е. тогда, когда частота порождения говнокода выше чем обычно. Вес остальное от лени, а поощрение лени плохо сочетается с самодисциплиной => говнокод. Я вот var обычно по пятницам использую, потом в понедельник что-нибудь ломается…
Хорошо шо хоть var проверяется еще на этапе компиляции. Правда тоже на умность компилятор надеется не стоит, напишите var i = 5 — и компилятор подставит int32, а на переполнения из-за Var я уже тоже натыкался. А вот dynamic… а применение динамических типов ОТКРЫВАЕТ ДВЕРИ АДА. То есть, оно говорит компилятору, что это динамический ВЫЗОВ АДСКОГО СОТОНЫ, и надо перенести все проверки типа (и ошибки соответственно) на время выполнения(с). Правда к Unity3D это вроде бы еще пока отношение не имеет
Не для того Хейлсберг создал строго-типизируемый язык программирования, что бы потом на нем пытаться писать «как буд-то это ЖабаСкрипт».
В общем когда сахара много, наступает момент когда код и его исполнение становится трудно предсказуемым
4) Я еще добавлю про var. Во-первых, опять таки это делает код трудно читаемым
>И сидишь и думаешь, а это что вообще такое?
Мышку навести, студия и покажет, что это. Магии же здесь нет, тип всегда точно определён.
Использовать var или нет — дело программиста. Но уж в явных местах лучше использовать, именно для улучшения чтения:
SomeClass a = new SomeClass();
List<KeyValuePair<int, SomeClass>> b = new List<KeyValuePair<int, SomeClass>>();
OtherSomeClass c = new OtherSomeClass();
// vs
var a = new SomeClass();
var b = new List<KeyValuePair<int, SomeClass>>();
var c = new OtherSomeClass();
Не согласен с вами. Каждому случаю свои ограничения.
Не использование Linq почти всегда раздувает код и снижает читаемость.
В данном случае код вставлен в команду покупки, которая вызывается один раз за транзакцию. Никакого смысла терять читаемость ради нескольких тактов на аллокацию в таком месте нет.
Вот если бы он Link вызывал на Update по длинным сложно организованным коллекциям, тогда да. А в таком случае использование Linq абсолютно оправдано.
Единственное ограничение что на iOS несколько функций Linq не работают. Но всё остальное обсолютно приемлемо если котер понимает что он делает.
З.Ы. Так же поставил вам лайк в гринлайте за эту статью, надеюсь игру пропустят.
public abstract class Command
{
public Core Core { get; private set; }
public bool IsValid { get; private set; }
public Command Execute (Core core)
{
Core = core;
IsValid = Run();
if (isValid) core.Publish(this); /// HERE
return this;
}
protected abstract bool Run ();
}
Потом я подписываюсь на эти события и перерисовываю то, что могло измениться
Пару примеров из реального кода:
public void On (MemberWakeup ev)
{
UpdateRowByMember(ev.member);
}
public void On (MemberReject ev)
{
UpdateRowByMember(ev.member);
}
public void On (MemberAssign ev)
{
UpdateRowByMember(ev.member);
}
public void On (ModuleEnable ev)
{
if (ev.module == module) Render();
}
public void On (ModuleDisable ev)
{
if (ev.module == module) Render();
}
public void On (ResourceChangeEvent ev)
{
if (activeTreeLeaf != null) {
activeTreeLeaf.RenderPanel();
}
}
Главное не сорваться и не начать менять компонент напрямую в методе. Просто то, что должно измениться оповещаем, что оно должно перерисоваться, а оно уже в свою очередь берет освеженные данные из модели и перерисовывается. Если хотите более оптимизированно — просто бьем на методы поменьше и вызываем только тот метод, который может измениться. Например:
public class ModuleOption
{
public void On (ResourceChangeEvent ev)
{
UpdateCost();
}
Когда открыто окно постройки модуля при изменении цены перерисуются только цены (под перерисовкой я понимаю не реально стирание и новое рисование, а указание новых значений, а уже юнити потом думает — рисовать заново или что там ему делать)
мне до сих пор не понятно как вы в дальнейшем подключаете свою логику к проекту в unity?
компилите отдельное DLL и просто его добавляете в проект? или переносите весь код в папку Assets? а потом что? если я правильно понимаю, то чтобы у вас все работало, необходимо создать экземпляр класса Core, который и будет точкой входа основной логики. на что вы его вешаете? на некий пустой объект, который будет у вас присутствовать в каждой сцене?
а как тогда будут работать остальные объекты в сцене, ведь у них не будет ссылки на экземпляр Core и связанных с ним объектов…
вопрос номер два: наткнулся в доке по Unity на Test Runner, который в последней версии даже встроен в сам движок. https://docs.unity3d.com/Manual/testing-editortestsrunner.html есть ли смысл тесты сразу под него писать?
P.S. Извиняюсь за свои тупые вопросы.
P.S.S. Поздравляю с успешным прохождением Green Light.
Да, у меня есть некий пустой объект, который является точкой входа и создает Engine.Core когда игрок начинает «Начать игру» или «Загрузить игру».
Если хотите быстро, дешево и неправильно — можно использовать статический метод, который возвращает текущий инстанс игры. Или даже статическое свойство. Запущенная игра, которую видит игрок то у нас всегда одна будет (если не одна, то такой трюк не пройдет)
class App : GameObject
{
public Engine.Core Game { get; private set; }
public void StartNewGame () {
Game = new Engine.Core();
}
}
class Foo : GameObject
, IListener<ResourceChangeEvent>
{
private BlaBla blabla;
public void Awake () {
App.Game.Emitter.Subscribe(this);
}
public void On (ResourceChangeEvent ev)
{
if (blabla != null) {
blabla.Do();
}
}
}
Если хотите поразбираться и написать так, чтобы не стыдно на собеседовании рассказать — погуглите DI контейнеры для Unity.
Unity тестами, к сожалению, не пользовался
Пишем игровую логику на C#. Часть 1/2