Хабр Курсы для всех
РЕКЛАМА
Практикум, Хекслет, SkyPro, авторские курсы — собрали всех и попросили скидки. Осталось выбрать!
Спасибо за интересную публикацию!
Зачастую даже в умных книгах авторы не всегда утруждаются подачей вразумительных примеров.
У Вас в тексте просто по диагонали глазами пробежался, и вроде ничего нового… но то, что знал ранее, стало гораздо более понятным, как-то лучше в голове уложилось вместе с полезной рекомендацией ("is-a" / "has-a" для выбора между наследованием и композицией)...
P.S.:
Такой магии бы да побольше…
Пешыте исчо!
ведь невозможно унаследоваться и от FlyingObject и от SwimmingObject
Дальше не читал
if (spellAbility == null) {
throw new Error('NoSpellCastAbility');
}
execute () {
const spellAbility = this.source.getAbility(SpellCastAbility);
ensureNotNull(spellAbility, 'NoSpellCastAbility')execute () {
const spellAbility = this.source.requireAbility(SpellCastAbility);execute () {
const spellAbility = this.source.getAbility(SpellCastAbility);
if (spellAbility == null) {
return Status.NoSpellCastAbility;
}
this.addChildren(new PayManaCommand(this.source, this.spell.manaCost));
this.addChildren(this.spell.getCommands(this.source, this.target));
return Status.Success;
}
if (!cond) return error у нас на практике будет много совершенно разных. Все мы никак не сможем красиво заDRYить.Отлично написано! Отличные примеры — помогли лучше усвоить и запомнить! (а ещё они такие жизненные!)
var plusButton = (Button)GetNode("PlusButton");
plusButton.Connect("pressed", this, "ModifyValue", new object[] { 1 });var plusButton = GetNode<Button>("PlusButton");
plusButton.Connect("pressed", ModifyValue);var plusButton = GetNode<Button>("PlusButton");
plusButton.Connect("pressed", () => ModifyValue(-1));Такое и композируется лучше, и по иерархиям бегать потом не надо, чтобы понять что происходит со стейтом, да и юнит тесты писать проще.Начнем с того, что это обычная маркетологичная чушь, которой нас пичкают на всех афтепати. Нет, композируется точно так же, по иерархиям бегать надо точно так же, юнит тесты писать точно так же. По сути от замены класса на функцию для всех этих факторов меняется только форма записи. Почему вдруг должны упроститься эти вещи о которых вы говорите — я не знаю. Может вам не понравилось использование addChildren вместо прямого вызова метода класса? Но тут дело не в классах. На функциях тоже можно написать такой код через каррирование.
Вообще когда я вижу класс с единственным методом, я не очень понимаю, почему это не может быть функциейЖаль, что вы из тех, кто бездумно повторяет за своим кумиром. Видите ли, в топике было раскрыто, зачем нужно классы и почему их нельзя заменить, к примеру, на функции. А если точнее — вы, как и предыдущий автор, совершенно забыли про вьюшку. Да, для простых вещей, которые пишутся на Редакс подойдет подход «изменили всю модель — перерисовали всю вьюшку». Но проблема в том, что этот подход просто отвратительно анимируется. Вот представьте себе настолку и там правило: «когда самурай стает на клетку рядом с противниками — он атакует каждого из них, но контратаку получает только от первого». Любой геймдевелопер знает, что это совсем обычная абилка для пошаговых игр. Итак, у нас получится такая иерархия:
SAMURAY_ID = 100;
ENEMY_1_ID = 201;
ENEMY_2_ID = 202;
ENEMY_3_ID = 203;
Movement( SAMURAY_ID, 3, 5 )
Attack( SAMURAY_ID, ENEMY_1_ID )
DealDamage( ENEMY_1_ID, 3 )
CounterAttack( ENEMY_1_ID, SAMURAY_ID)
DealDamage( SAMURAY_ID, 1 )
Attack( SAMURAY_ID, ENEMY_2_ID )
DealDamage( ENEMY_2_ID, 3 )
Death( ENEMY_2_ID )
GiveMoney( PLAYER_1, 300 )
Attack( SAMURAY_ID, ENEMY_3_ID )
DealDamage( ENEMY_3_ID, 3 )
await animateMovement(SAMURAY_ID, 3, 5);
await animateAttack(SAMURAY_ID, ENEMY_1_ID);
await showDamage(ENEMY_1_ID, 3)
await animateAttack(SAMURAY_ID, ENEMY_1_ID);
У самурая новое положение и на 3 меньше хит-поинтов
У двох врагов на 3 меньше хит-поинтов
Один враг умер
Самурай имеет абилку телепортации и может ходить на 1 клетку.
Игрок решает сначала телепорироваться с клетки (3, 3) на клетку (3, 4).
А потом походить с клетки (3, 4) на (3, 5).
Художники, неожиданно, нарисовали разные анимации для телепортации и для ходьбы.
Для переноски данных есть рекордыНу давайте, напишите пример. Холиварить — не мешки ворочать. Я явно описал задачу и написал со своей стороны ее решение, но вы отказываетесь рассказать, как же, по вашему, ее нужно решать. Покажите класс. Научите нас, какой должна быть современная архитектура мечты.
Классы не переносят данные, а группируют данные и операции над ними. Для переноски данных есть рекордыА Рекорды не реализуются через классы в некоторых языках, не?
Ability( type: 'DoubleAttack', source: 3, target: 5 )
Attack( source: 3, target: 5 )
DealDamage( target: 5, amount: 2 )
Attack( source: 3, target: 5 )
DealDamage( target: 5, amount: 2 )
Death( target: 5 )
GiveMoney( player: 1, 100 )Ability ( source: 3, target: 5, type: 'DoubleAttack' )
Attack ( source: 3, target: 5 )
DealDamage( target: 5, amount: 2 )
Attack ( source: 3, target: 5 )
DealDamage( target: 5, amount: 2 )
Death ( target: 5 )
GiveMoney ( player: 1, 100 )drawAbility (ability: AbilityCommand) {
if (ability.type == 'DoubleAttack') {
var unit = this.unitsViews.findById(ability.source);
await unit.launchPentagramAnimation();
}
}drawAttack (attack: AttackCommand) {
var source = this.unitsViews.findById(attack.source);
var target = this.unitsViews.findById(attack.target);
var angle = source.getAngleTo(target);
await source.rotateAt( angle );
await source.launchSwordAnimation();
}drawDamage (damage: DealDamageCommand) {
var target = this.unitsViews.findById(attack.target);
this.canvas.createNewFloatingNumber( `-${attack.amount}`, { color: 'red' });
}А если таргет надо лечить, а не атаковать?Тогда вместо
Ability( type: 'DoubleAttack', source: 3, target: 5 ) будет, к примеру Heal( source: 3, target: 5 ). И, соответственно, внутри — все наоборот.Вьюшка должна тоже быть в курсе о всех возможных командах?О всех командах, которые вы хотите как-то отобразить) Если с сервера пришло, к примеру LaunchFireball, а вьюшка не представляет, что такое фаербол, то никакой анимации рисоваться не будет. Скорее всего эта команда пропустится и отрисуется только цифры использования маны и отлетающий демедж.
this.addChildren(new PayManaCommand(this.source, this.spell.manaCost));
this.addChildren(this.spell.getCommands(this.source, this.target));
PS: Наверное в VampireSpell опечатка, castTo должно быть getCommands.Да, вы правы, спасибо.
Я понимаю если бы это был какой-то CommandExecutor / Scheduler, в который мы просто кидаем в конец новые команды и они в свое время выполняются. Зачем нужно addChildren?Да, и тут вы правы. Конечно, у нас есть CommandExecutor. Но тут я ввел addChildren, который нужен, чтобы не делать дополнительный DI. Чтобы не было вопросов — а откуда у нас экзекьютор появился, а как правильно его передать в команду? И так далее.
Movement( SAMURAY_ID, 3, 5 ) Attack( SAMURAY_ID, ENEMY_1_ID ) DealDamage( ENEMY_1_ID, 3 ) CounterAttack( ENEMY_1_ID, SAMURAY_ID) DealDamage( SAMURAY_ID, 1 ) Attack( SAMURAY_ID, ENEMY_2_ID ) DealDamage( ENEMY_2_ID, 3 ) Death( ENEMY_2_ID ) GiveMoney( PLAYER_1, 300 ) Attack( SAMURAY_ID, ENEMY_3_ID ) DealDamage( ENEMY_3_ID, 3 )
Или они должны выполняться не по принципу FIFO очереди, а там какой-нибудь depth-first search обходом.Да, именно так. depth-first search — то, что нужно. Когда мы делаем ундо, то, конечно, нужно сделать ундо и всем детям.
class CommandExecutor {
RunTree (Command command) {
TriggerBeforeEvent(command);
command.Execute();
TriggerInsideEvent(command);
foreach (var child in command.children) RunTree (child);
TriggerAfterEvent(command);
}
}
Композиция против наследования, паттерн Команда и разработка игр в целом