Как и раньше — внизу статьи вы можете найти полный код на ГитХаб и ссылку на бесплатное скачивание.
План работы
1.
2.
3.
4.
5.
6. Добавляем Constructible, строения теперь строятся некоторое время
7. Добавляем ресурсы, для постройки необходимы ресурсы
8. Добавляем цикл производства — модуль потребляет и выдает ресурсы
Добавляем Constructible
Давайте теперь что-то привяжем к течению времени. Пусть постройки и модули строятся не сразу, а несколько ходов (зависимо от конфигурации). Для начала во все настройки добавим пункт ConstructionTime. Если ConstructionTime равно нулю — структуру построить невозможно.
public class BuildingConfig { // ... public int ConstructionTime; }
public class ModuleConfig { // ... public int ConstructionTime; }
Не забываем добавить настройки в фабрику:
public class Factory { // ... Type = BuildingType.PowerPlant, ConstructionTime = 8, // ... Type = BuildingType.Smeltery, ConstructionTime = 10, // ... Type = BuildingType.Roboport, ConstructionTime = 12, // ... Type = ModuleType.Generator, ConstructionTime = 5 // ... Type = ModuleType.Furnace, ConstructionTime = 6 // ... Type = ModuleType.Digger, ConstructionTime = 7 // ... Type = ModuleType.Miner, ConstructionTime = 8 // ... }
Теперь создадим класс Progression, которым мы будем реализовывать любые прогрессии, которые текут во времени, например, строительство.
public class Progression { public readonly int Time; public int Progress { get; private set; } public bool IsFake { get { return Time == 0; } } public bool IsReady { get { return IsFake || Progress >= Time; } } public bool IsRunning { get { return !IsReady && Progress > 0; } } public Progression (int time) { Time = time; Progress = 0; } public void AddProgress () { if (!IsReady) Progress++; } public void Complete () { if (!IsReady) Progress = Time; } public void Reset () { Progress = 0; } }
Теперь добавим в наши комнаты и модули возможность постройки.
public class Building { // ... public readonly Progression Constructible; // ... public Building (BuildingConfig config) { // ... Constructible = new Progression(config.ConstructionTime); }
public class Module { // ... public readonly Progression Constructible; public Module (ModuleConfig config) { // ... Constructible = new Progression(config.ConstructionTime); }
И запретим постройку модулей в еще не построенной комнате:
public class ModuleConstruct : Command { // ... protected override bool Run () { // ... if (!Building.Constructible.IsReady) { return false; }
Само собой после этого упали тесты, потому мы добавим в тесты CorrectConstruction, IncorrectConstruction, CantConstructInWrongBuilding и ModulesLimits после успешного выполнения команды BuildingConstruct вызов метода Complete (да-да, специально для этого мы его и создали)
room.Building.Constructible.Complete()
А для проверки на невозможность построить в еще не законченной комнате напишем отдельный тест:
[TestMethod] public void CantConstructInUncompleteBuilding () { var core = new GameLogic.Core(); var room = core.Ship.GetRoom(0); new BuildingConstruct( room, core.Factory.ProduceBuilding(BuildingType.PowerPlant) ) .Execute(core); Assert.IsFalse( new ModuleConstruct( room.Building, core.Factory.ProduceModule(ModuleType.Generator), 2 ) .Execute(core) .IsValid ); }
Но теперь давайте сделаем, чтобы комната строилась не только по мановению руки богов мира нашей игры, но и просто со временем. Для этого создадим специальную команду и будем вызывать ее каждый ход:
public class NextTurn : Command { protected override bool Run () { new ConstructionProgress().Execute(Core); // .. } }
public class ConstructionProgress : Command { protected override bool Run () { foreach (var room in Core.Ship.Rooms) { BuildingProgress(room.Building); } return true; } private void BuildingProgress (Building building) { building.Constructible.AddProgress(); foreach (var module in building.Modules) { module.Constructible.AddProgress(); } } }
И сразу покроем тестами, которые покажут, что код работает прекрасно:
[TestMethod] public void Constructible () { const int smelteryTime = 10; const int furnaceTime = 6; var core = new GameLogic.Core(); var room = core.Ship.GetRoom(0); // Smeltery new BuildingConstruct( room, core.Factory.ProduceBuilding(BuildingType.Smeltery) ) .Execute(core); Assert.IsFalse( room.Building.Constructible.IsReady ); new NextTurnCount(smelteryTime - 1).Execute(core); Assert.IsFalse(room.Building.Constructible.IsReady); new NextTurn().Execute(core); Assert.IsTrue(room.Building.Constructible.IsReady); // Furnace new ModuleConstruct( room.Building, core.Factory.ProduceModule(ModuleType.Furnace), 2 ).Execute(core); var module = room.Building.GetModule(2); Assert.IsFalse( module.Constructible.IsReady ); new NextTurnCount(furnaceTime - 1).Execute(core); Assert.IsFalse(module.Constructible.IsReady); new NextTurn().Execute(core); Assert.IsTrue(module.Constructible.IsReady); }
Добавляем ресурсы
Для того, чтобы что-то создать сначала необходимо что-нибудь разрушить и собрать металлолом. Давайте реализуем ресурсы, чтобы игроку пришлось оплачивать свои постройки. Ресурсов будет три — Энергия, Руда и Металл.
public enum ResourceType { Energy, Ore, Metal }
Также создадим Банк, где игрок будет хранить и откуда забирать ресурсы.
public class Bank { private readonly Dictionary<ResourceType, int> resources = new Dictionary<ResourceType, int>(); public int Get (ResourceType type) { return resources.ContainsKey(type) ? resources[type] : 0; } public void Change (ResourceType type, int value) { var current = Get(type); if (current + value < 0) { throw new ArgumentOutOfRangeException("Not enought " + type + " in bank"); } resources[type] = current + value; } }
public class Core { // ... public readonly Bank Bank = new Bank(); }
Теперь добавляем цену производства в настройки модулей и строений:
public class BuildingConfig { // ... public Dictionary<ResourceType, int> ConstructionCost; }
public class ModuleConfig { // ... public Dictionary<ResourceType, int> ConstructionCost; }
public class Factory { // ... Type = BuildingType.PowerPlant, ConstructionCost = new Dictionary<ResourceType, int>() {{ ResourceType.Metal, 20 }}, // ... Type = BuildingType.Smeltery, ConstructionCost = new Dictionary<ResourceType, int>() {{ ResourceType.Metal, 20 }}, // ... Type = BuildingType.Roboport, ConstructionCost = new Dictionary<ResourceType, int>() {{ ResourceType.Metal, 20 }}, // ... // ... Type = ModuleType.Generator, ConstructionCost = new Dictionary<ResourceType, int>() {{ ResourceType.Metal, 10 }}, // ... Type = ModuleType.Furnace, ConstructionCost = new Dictionary<ResourceType, int>() {{ ResourceType.Metal, 10 }}, // ... Type = ModuleType.Digger, ConstructionCost = new Dictionary<ResourceType, int>() {{ ResourceType.Metal, 10 }}, // ... Type = ModuleType.Miner, ConstructionCost = new Dictionary<ResourceType, int>() {{ ResourceType.Metal, 40 }}, // ... }
Теперь добавим команду, которая позволяет платить ресурсы и сразу же попробуем ее в деле (в тестах):
public class Pay : Command { public readonly Dictionary<ResourceType, int> Cost; public Pay (Dictionary<ResourceType, int> cost) { Cost = cost; } protected override bool Run () { // Если хотя бы одного ресурса не хватаем - отменяем всю оплату и возвращаем ошибку if (Cost.Any(item => Core.Bank.Get(item.Key) < item.Value)) { return false; } // Если всех хватает - забираем из банка foreach (var item in Cost) { Core.Bank.Change(item.Key, -item.Value); } return true; } }
[TestClass] public class Player { [TestMethod] public void Payment () { var core = new Core(); core.Bank.Change(ResourceType.Metal, 100); core.Bank.Change(ResourceType.Ore, 150); Assert.IsFalse( new Pay(new Dictionary<ResourceType, int>{ { ResourceType.Metal, 100 }, { ResourceType.Ore, 2000 } }) .Execute(core) .IsValid ); Assert.AreEqual(100, core.Bank.Get(ResourceType.Metal)); Assert.AreEqual(150, core.Bank.Get(ResourceType.Ore)); Assert.IsTrue( new Pay(new Dictionary<ResourceType, int>{ { ResourceType.Metal, 100 }, { ResourceType.Ore, 30 } }) .Execute(core) .IsValid ); Assert.AreEqual(0, core.Bank.Get(ResourceType.Metal)); Assert.AreEqual(120, core.Bank.Get(ResourceType.Ore)); } }
Оплата работает корректно и начать платить за постройки и модули довольно просто — добавим вызов команды Pay в качестве последней валидации (она должна быть последней, если мы не хотим, чтобы после оплаты другая проверка не дала построить конструкцию):
public class BuildingConstruct : Command { // ... protected override bool Run () { // ... if (!new Pay(Building.Config.ConstructionCost).Execute(Core).IsValid) { return false; } Room.Building = Building; return true; } }
public class ModuleConstruct : Command { // ... protected override bool Run () { // ... if (!new Pay(Module.Config.ConstructionCost).Execute(Core).IsValid) { return false; } Building.SetModule(Position, module); return true; } }
К счастью, у нас снова отвалились тесты (к счастью, потому что это значит, что они отлично выполняют свою работу).
В старых тестах добавим игроку ресурсы и напишем новый тест, который в будущем будет проверять, что внезапно не появилась возможность бесплатно построить конструкцию. Добавляем во все сломанные тесты поближе к началу:
core.Bank.Change(ResourceType.Metal, 1000);
И пишем тест на постройку с недостачей ресурсов:
[TestMethod] public void CantBuiltCostly () { var core = new GameLogic.Core(); var room = core.Ship.GetRoom(0); core.Bank.Change(ResourceType.Metal, 3); Assert.IsFalse( new BuildingConstruct( room, core.Factory.ProduceBuilding(BuildingType.Smeltery) ) .Execute(core) .IsValid ); }
Добавляем цикл производства
Забирать ресурсы, конечно, приятно, но давать значительно приятнее. Давайде запрограммируем возможность запускать производственные цепочки. Каждый модуль сможет скушать определенное количество сырья и потом выдать готовый материал. Снова начинаем с конфигурации:
public class ModuleConfig { // ... public int CycleTime; // сколько времени модуль будет перетравливать сырье public Dictionary<ResourceType, int> CycleInput; // сколько сырья public Dictionary<ResourceType, int> CycleOutput; // какой выход готовой продукции }
public class Module { // ... public readonly Progression Cycle; public Module (ModuleConfig config) { // ... Cycle = new Progression(config.CycleTime); } }
public class Factory { // ... { ModuleType.Generator, new ModuleConfig() { // ... CycleTime = 12, CycleInput = null, // электростанция ничего не требует, только дает CycleOutput = new Dictionary<ResourceType, int>() { { ResourceType.Energy, 10 } }, }}, { ModuleType.Furnace , new ModuleConfig() { // ... CycleTime = 16, CycleInput = new Dictionary<ResourceType, int>() { { ResourceType.Energy, 6 }, { ResourceType.Ore, 4 }, }, CycleOutput = new Dictionary<ResourceType, int>() { { ResourceType.Metal, 5 } } }}, { ModuleType.Digger , new ModuleConfig() { // ... CycleTime = 18, CycleInput = new Dictionary<ResourceType, int>() { { ResourceType.Energy, 2 } }, CycleOutput = new Dictionary<ResourceType, int>() { { ResourceType.Ore, 7 } } }}, { ModuleType.Miner , new ModuleConfig() { // ... CycleTime = 32, CycleInput = new Dictionary<ResourceType, int>() { { ResourceType.Energy, 8 } }, CycleOutput = new Dictionary<ResourceType, int>() { { ResourceType.Ore, 40 } } }}
Теперь добавим в каждый ход прогресс по производству:
public class NextTurn : Command { protected override bool Run () { new CycleProgress().Execute(Core); // Добавьте его в начало, это будет важно в тестах // ... } }
public class CycleProgress : Command { protected override bool Run () { foreach (var room in Core.Ship.Rooms) { BuildingProgress(room.Building); } return true; } private void BuildingProgress (Building building) { if (!building.Constructible.IsReady) return; foreach (var module in building.Modules) { ModuleProgress(module); } } private void ModuleProgress (Module module) { if (!module.Constructible.IsReady || module.Cycle.IsFake) { return; } // Добавляем прогресс только если модуль уже запущен (ресурсы были заплачены) // Или если мы можем запустить его сейчас (заплатить ресурсы) if (module.Cycle.IsRunning || TryStartCycle(module)) { AddStep(module); } } private void AddStep (Module module) { module.Cycle.AddProgress(); // Если после добавления прогресса работа модуля завершена... if (module.Cycle.IsReady) { // ... отдаем игроку его ресурсы CycleOutput(module); // ... и обнуляем прогресс, следующий раз ему придется запускаться сначала module.Cycle.Reset(); } } private bool TryStartCycle (Module module) { if (module.Config.CycleInput == null) { return true; } // Пытаемся заплатить ресурсы и если удается - модуль запущен return new Pay(module.Config.CycleInput).Execute(Core).IsValid; } private void CycleOutput (Module module) { foreach (var item in module.Config.CycleOutput) { // Отдаем игроку каждый ресурс, который ему был нужен Core.Bank.Change(item.Key, item.Value); } } }
Класс получился крупноват, но мы всегда можем его отрефакторить, если сложность будет завысокая. Теперь пишем тест. Он будет довольно длинный, проверять и корректность производства, и незапуск в случае недостачи ресурсов. Также я специально для теста создал отдельные настройки для модуля и строения (вдруг ГД их поменяет и у меня тесты упадут). В идеале все тесты можно было бы поменять на специальные тестовые настройки:
public class Cycle { [TestMethod] public void CheckCycle () { var buildingConfig = new BuildingConfig() { Type = BuildingType.Smeltery, ModulesLimit = 1, AvailableModules = new [] { ModuleType.Furnace } }; var moduleConfig = new ModuleConfig() { Type = ModuleType.Furnace, ConstructionTime = 2, ConstructionCost = new Dictionary<ResourceType, int>() { { ResourceType.Metal, 10 } }, CycleTime = 4, CycleInput = new Dictionary<ResourceType, int>() { { ResourceType.Ore, 10 }, { ResourceType.Energy, 5 } }, CycleOutput = new Dictionary<ResourceType, int>() { { ResourceType.Metal, 1 } } }; var core = new Core(); core.Bank.Change(ResourceType.Metal, 10); core.Bank.Change(ResourceType.Ore, 80); core.Bank.Change(ResourceType.Energy, 10); var building = new Building(buildingConfig); core.Ship.GetRoom(0).Building = building; var module = new Module(moduleConfig); Assert.IsTrue( new ModuleConstruct(building, module, 0) .Execute(core) .IsValid ); new NextTurn().Execute(core); Assert.IsFalse(module.Cycle.IsRunning); new NextTurn().Execute(core); Assert.IsTrue(module.Constructible.IsReady); Assert.IsFalse(module.Cycle.IsRunning); new NextTurn().Execute(core); Assert.IsTrue(module.Cycle.IsRunning); Assert.AreEqual(1, module.Cycle.Progress); Assert.AreEqual(70, core.Bank.Get(ResourceType.Ore)); Assert.AreEqual(5, core.Bank.Get(ResourceType.Energy)); Assert.AreEqual(0, core.Bank.Get(ResourceType.Metal)); new NextTurnCount(3).Execute(core); Assert.IsFalse(module.Cycle.IsRunning); Assert.AreEqual(70, core.Bank.Get(ResourceType.Ore)); Assert.AreEqual(5, core.Bank.Get(ResourceType.Energy)); Assert.AreEqual(1, core.Bank.Get(ResourceType.Metal)); new NextTurn().Execute(core); Assert.IsTrue(module.Cycle.IsRunning); Assert.AreEqual(60, core.Bank.Get(ResourceType.Ore)); Assert.AreEqual(0, core.Bank.Get(ResourceType.Energy)); Assert.AreEqual(1, core.Bank.Get(ResourceType.Metal)); new NextTurnCount(3).Execute(core); Assert.IsFalse(module.Cycle.IsRunning); Assert.AreEqual(2, core.Bank.Get(ResourceType.Metal)); new NextTurn().Execute(core); // Cant launch because of Energy leak Assert.IsFalse(module.Cycle.IsRunning); Assert.AreEqual(60, core.Bank.Get(ResourceType.Ore)); Assert.AreEqual(0, core.Bank.Get(ResourceType.Energy)); } }
Конец
Итак, тесты запустились корректно и мы смогли сделать минимальную версию нашего продукта. Класс Factory получился раздутым, но если вынести настройки в JSON, то и он будет вполне ничего. Используя Json.NET нам необходимо написать что-то вроде этого:
var files = Directory.GetFiles(path + "/Items/Modules", "*.json", SearchOption.AllDirectories); var modules = new List<ModuleConfig>(); foreach (var file in modules) { var content = File.ReadAllText(file); modules.Add( JsonConvert.DeserializeObject<ModuleConfig>(content) ); }
{ "Type": "Generator", "ConstructionTime": 5, "ConstructionCost": { "Metal": 10 }, "CycleTime": 12, "CycleInput": { "Energy" 6, "Ore": 4, }, "CycleOutput": { "Energy": 10 } }
Для тех, кто просто любит код — есть отдельный репозиторий на ГитХаб
Кроме этого, если вас интересуют вопросы по разработке SpaceLab — задавайте, отвечу на них в комментариях или в отдельной статье
Скачать для Windows, Linux, Mac бесплатно и без СМС, а так же поддержать нас можно на странице SpaceLab на GreenLight
Комментарии (15)
DDDENISSS
25.02.2017 01:57В чем удобность команд?
Читал про StrangeIOC, там можно создавать цепочки из сигналов и команд. Но у вас такого не видел.
Еще, возможно, когда в команде много программистов, то удобнее, когда каждая операция в своем файле (проще мерж, лучше видно историю изменений).
Но не нравится мне, что надо создавать класс/файл на каждый чих.TheShock
25.02.2017 02:10+1Можете, пожалуйста, поуточнять? Что имеете ввиду под цепочками? У меня команда может вызывать другую команду — чем не цепочка. Для вас система недостаточно сложная? Просто для данной игры более сложная система не нужна. Вот когда делал Генералы — там было сложнее и команды были иерархическими, у команд были дети, сложная система событий, которая позволяла подписаться до команды, в середине команды, после команды, более сложная валидация. Каждому приложению ровно такая сложность, которая необходима, но не больше.
О да, я забыл, ведь создание файлов — это столь сложная операция. Гораздо удобнее каждый раз рыться в файле на тысячи строк. Поиском? Главное преимущество файлов — значительно легче искать то, что необходимо. Когда у тебя весь необходимый код вмещается на одном экране и другой экран просто не нужен) Один файл на 50-100 строк, файлы структурированы. Можно открыть несколько вкладок. Код, который сейчас не нужен — не путается под ногами. Вот мне необходима функциональность А и Г, а между ними — Б и В. Так я просто не открыл лишнее, а только два файла в разный вкладках — а так метаться постоянно видя лишний код.
И какие причины НЕ создавать файлы? Сплошные плюсы ведь)mayorovp
25.02.2017 09:49-1Ну, в двухста файлах в одной папке тоже ничего хорошего. А группировать их не всегда удобно, иногда важно чтобы классы оставались в одном пространстве имен, а после переноса файлов в другую папку IDE будет активно мешать этому.
Лично я предпочитаю создавать отдельный файл на класс если в этом классе более 7 строк кода.
DDDENISSS
25.02.2017 13:52Что имеете ввиду под цепочками?
commandBinder.Bind(GameEvent.HIT).To<DestroyEnemyCommand>().To<UpdateScoreCommand>(); commandBinder.Bind(GameEvent.HIT).InSequence() .To<CheckLevelClearedCommand>() .To<EndLevelCommand>() .To<GameOverCommand>(); injectionBinder.Bind<ICommandBinder>().To<SignalCommandBinder>().ToSingleton(); // сигналы - новые, строго типизированные события.
https://strangeioc.github.io/strangeioc/TheBigStrangeHowTo.html
Это должно проделываться в Composition Root. Вызов одной команды из другой это явно плохая практика.
Кстати, в StrangeIOC команды могут быть асинхронными, в этом случае они конечно упрощают жизнь. Хотя я для этого использовал Promise, которые очень популярные в JS.
файле на тысячи строк
Я не говорил, что надо вообще все в один файл писать. Но создавать файл для каждой функции, еще и в 1-5 строк… это странно для меня. Хотя конечно можно в одном файле написать несколько классов.
Но остается проблема с GC. У вас некоторые команды создавались в каждом update, рано или поздно это может внести свою лепту в понижение fps.
Я не представляю как выглядел мой проект, если бы каждый метод я выносил бы в отдельный класс. Где-то должна быть эта грань.DDDENISSS
25.02.2017 14:04Не знал, что время редактирования комментария столь ограничено. Немного изменил пример.
commandBinder.Bind(GameEvent.HIT).To<DestroyEnemyCommand>().To<UpdateScoreCommand>(); commandBinder.Bind(GameEvent.HIT).To<DestroyEnemyCommand>().Once(); commandBinder.Bind(GameEvent.HIT).InSequence() .To<CheckLevelClearedCommand>() .To<EndLevelCommand>() .To<GameOverCommand>(); commandBinder.Bind<ShipDestroyedSignal>().To<ShipDestroyedCommand>(); // сигналы - новые, строго типизированные события.
TheShock
25.02.2017 17:32Вызов одной команды из другой это явно плохая практика
Чем плохая?
Это должно проделываться в Composition Root.
Почему?
Как на меня в данной ситуации это лишнее усложнение, которое не несёт никакого позитива в данном случае.
commandBinder.Bind<ShipDestroyedSignal>().To<ShipDestroyedCommand>(); // сигналы - новые, строго типизированные события.
Я в игре могу подвесится прям на команды как на события. И тоже строготипизированно. Выглядит как-то так:
public void On(ShipDestroyedCommand ev) { }
Но создавать файл для каждой функции, еще и в 1-5 строк
Почему для каждой? Есть много классов, где несколько методов. Да и все классы очень быстро растут. Нету смысла сейчас экономить файлы, чтобы потом их все-равно выносить. И как тогда файлы, которые содержат два-три класса называть? Другой вопрос — нужно ли такое разбиение и может неоторые классы должны нести чуть больше ответственности?
Я не представляю как выглядел мой проект, если бы каждый метод я выносил бы в отдельный класс
Я не выношу каждый метод в отдельный класс, не понимаю о чем вы говорите? Есть и довольно большие классы. Каждый класс у меня отвечает за то что должен и сейчас это довольно мало кода, но дальше он растет и классы растут. Я не могу понять, что вы предлагаете? Ну вот на практике, код, который вы читали выше — какие два класса объеденить в один?
в StrangeIOC команды могут быть асинхронными, в этом случае они конечно упрощают жизнь
Чем упрощают? Что для вас «асинхронные» в данном контексте?)
DDDENISSS
26.02.2017 00:50Почему?
При использование Composition Root, все связи видны как на ладоне. Как минимум это должно упростить жизнь новым программистам на проекте.
Я сейчас читаю книгу «Dependency Injection in .NET». Там об этом написано. Хотя там написано про инициализацию зависимостей, но думаю к сигналам и командам это тоже относится.
Я в игре могу подвесится прям на команды как на события. И тоже строготипизированно. Выглядит как-то так:
В StrangeIoC есть два вида событий: старые событий и новые сигналы. Я это имел ввиду.
Чем упрощают? Что для вас «асинхронные» в данном контексте?)
Команда выполняется в течении нескольких кадров. Проще вызвать другую команду в конце асинхронной команды. Иначе нужен будет или callback или promise.
Чтобы просто не создавать классы размером в 1000 строк, можно использовать шаблон стратегия. Хотя я часто пишу статические классы — утилиты или хелперы.
Суть команд, мне кажется, больше, чем просто декомпозиция большого класса.mayorovp
26.02.2017 08:54Вы упускаете из виду тот факт, что прямые вызовы легко прослеживаются через IDE — а Composition Root в большом проекте иногда нужно еще и найти сначала...
nporaMep
25.02.2017 16:05вопрос по классу Command, bool Run и bool IsValid
как обрабатываются различные причины false на клиенте, когда например нужно отобразить разное сообщение пользователю почему та или иная команда не может быть выполнена?TheShock
25.02.2017 16:23Изначально у меня было что-то вроде такого:
public enum Status { Success, RoomNotEmpty, NotEnoughItems, CrewMemberIsFreezed } public abstract class Command { public Core Core { get; private set; } public bool IsValid { get; private set; } public Status Status { get; private set; } public Command Execute (Core core) { Core = core; Status = Run(); IsValid = (Status == Status.Success); return this; } protected abstract Status Run (); }
И тогда каждая команда при ошибке возвращает не false, а код ошибки. Как часть сервера работает прекрасно — можно и узнать все возможные ошибки и сериализуется легко.
protected override Status Run () { if (Room.Building.Type != BuildingType.Empty) { return Status.RoomNotEmpty; } if (Building.Type == BuildingType.Empty) { return Status.CantBuildEmpty; } if (!new Pay(Building.Config.ConstructionCost).Execute(Core).IsValid) { return Status.NotEnoughResources; } Room.Building = Building; return Status.Success; }
Сейчас же я понял, я никогда не проверяю статус, потому что вьюшка не позволяет запустить команду, которая может вернуть false. Правда, статусы самодокументируемые и отладка с ними легче, так что если бы нас в команде было хотя бы двое — обязательно бы их использовали.mayorovp
25.02.2017 18:33вьюшка не позволяет запустить команду, которая может вернуть false
Это означает, что у вас бизнес-логика дублируется… Нарушение принципа DRY.
TheShock
25.02.2017 18:40Вы правы.
Хотя если строго говорить, то валидация в командах у меня излишняя, а не дублируется (нарушением DRY было бы, если бы убрав эту валидацию, у меня перестало бы работать, а так — функционал игры совершенно не изменится). А сейчас эта валидация просто не нужна для корректной работы игры.
Я пишу эту валидацию скорее для более легкого покрытия тестами, допроверки глупых багов в юай и дополнительной декларативности. А еще потому что это что-то двух разных микросервисов, а там код тоже дублируется.
offi
Спасибо за статьи, было интересно почитать. Есть ли в планах статья про продвижение игры, успехи на GreenLight и т.п.?
TheShock
Спасибо) Пока такого в планах нету, потому что я не привык рассказывать о том, в чем плохо разбираюсь)
Прогресс на Гринлайт пока неплохой, но каждый голос важен.