Помощь в реализации того, как существа и предметы взаимодействуют в компьютерной ролевой игре
Я программирую простую ролевую игру (учиться и развлекаться), и я нахожусь в точке, где я пытаюсь найти способ взаимодействия игровых объектов друг с другом. Я стараюсь избегать двух вещей.
- Создание гигантского игрового объекта, который может быть чем угодно и делать все
- Сложность - поэтому я избегаю разработки на основе компонентов, например, см. здесь
Итак, учитывая эти параметры, мне нужен совет, чтобы игровые объекты могли выполнять действия друг с другом.
Например
- Существа (персонажи, монстры, NPC) могут выполнять действия над существами или предметами (оружие, зелья, ловушки, двери).
- Элементы могут выполнять действия над существами или предметами. Примером может быть ловушка, когда персонаж пытается открыть сундук.
То, что я придумал, - это метод PerformAction
, который может принимать Существа или Элементы как параметры. Как этот
PerformAction(Creature sourceC, Item sourceI, Creature targetC, Item targetI)
// this will usually end up with 2 null params since
// only 1 source and 1 target will be valid
Или я должен делать это вместо этого?
PerformAction(Object source, Object target)
// cast to correct types and continue
Или есть совершенно другой способ, о котором я должен думать об этом?
Ответы
Ответ 1
Это проблема с двойной отправкой. При регулярном программировании OO вы "отправляете" вызов виртуального метода конкретному типу класса, реализующего экземпляр объекта, который вы вызываете. Клиенту не нужно знать фактический тип реализации, он просто делает вызов метода против абстрактного описания типа. Это "единственная отправка".
Большинство языков OO не реализуют ничего, кроме отдельной отправки. Двойная отправка - это когда операция, которую нужно вызвать, зависит от двух разных объектов. Стандартным механизмом реализации двойной отправки на языках OO без прямой поддержки с двойной отправкой является " Visitor" шаблон дизайна. См. Ссылку для использования этого шаблона.
Ответ 2
Это похоже на случай полиморфизма. Вместо того, чтобы использовать Item или Creature в качестве аргумента, выведите оба из них (или реализуйте) из ActionTarget или ActionSource. Пусть реализация Существа или Элемента определяет, какой путь оттуда.
Вы очень редко хотите оставить его таким открытым, просто взяв Object. Даже небольшая информация лучше, чем никто.
Ответ 3
Вы можете попробовать смешать шаблон Command с некоторым умным использованием интерфейсов, чтобы решить эту проблему:
// everything in the game (creature, item, hero, etc.) derives from this
public class Entity {}
// every action that can be performed derives from this
public abstract class Command
{
public abstract void Perform(Entity source, Entity target);
}
// these are the capabilities an entity may have. these are how the Commands
// interact with entities:
public interface IDamageable
{
void TakeDamage(int amount);
}
public interface IOpenable
{
void Open();
}
public interface IMoveable
{
void Move(int x, int y);
}
Затем производная команда опустошает, чтобы увидеть, может ли она сделать то, что ей нужно для цели:
public class FireBallCommand : Command
{
public override void Perform(Entity source, Entity target)
{
// a fireball hurts the target and blows it back
var damageTarget = target as IDamageable;
if (damageTarget != null)
{
damageTarget.TakeDamage(234);
}
var moveTarget = target as IMoveable;
if (moveTarget != null)
{
moveTarget.Move(1, 1);
}
}
}
Обратите внимание, что:
-
Выведенная сущность должна реализовать только те возможности, которые ей подходят.
-
Базовый класс Entity не имеет кода для каких-либо возможностей. Это хорошо и просто.
-
Команды могут изящно ничего не делать, если объект не затронут им.
Ответ 4
Я думаю, что вы рассматриваете слишком маленькую часть проблемы; как вы вообще определяете аргументы функции PerformAction? Что-то вне функции PerformAction уже знает (или каким-то образом должно выяснить), требует ли действие, которое он хочет вызвать, цели или нет, и сколько целей и какой элемент или символ он работает. Существенно, что часть кода должна решить, какая операция выполняется. Вы опустили это из сообщения, но я думаю, что это абсолютный самый важный аспект, потому что это действие, которое определяет необходимые аргументы. И как только вы знаете эти аргументы, вы знаете форму функции или метода для вызова.
Скажите, что персонаж открыл сундук, и ловушка уходит. Вероятно, у вас уже есть код, который является обработчиком событий для открывания сундука, и вы можете легко передать персонажу, который это сделал. Вы также предположительно уже выяснили, что объект был захваченным сундуком. Итак, у вас уже есть необходимая информация:
// pseudocode
function on_opened(Character opener)
{
this.triggerTrap(opener)
}
Если у вас есть один класс Item, базовая реализация triggerTrap
будет пустой, и вам нужно будет вставить какие-то проверки, например. is_chest
и is_trapped
. Если у вас есть производный класс Chest, вам, вероятно, просто понадобится is_trapped
. Но на самом деле это так сложно, как вы это делаете.
То же самое касается открытия сундука в первую очередь: ваш код ввода будет знать, кто действует (например, текущий игрок или текущий персонаж AI), может определить, что такое цель (путем поиска элемента под мышкой, или в командной строке), и может определять требуемое действие на основе ввода. Затем он просто становится примером поиска правильных объектов и вызова правильного метода с этими аргументами.
item = get_object_under_cursor()
if item is not None:
if currently_held_item is not None:
player_use_item_on_other_item(currently_held_item, item)
else
player.use_item(item)
return
character = get_character_under_cursor()
if character is not None:
if character.is_friendly_to(player):
player.talk_to(character)
else
player.attack(character)
return
Держите его простым.:)
Ответ 5
в модели Zork, каждое действие, которое можно сделать для объекта, выражается как метод этого объекта, например.
door.Open()
monster.Attack()
что-то общее, как PerformAction, станет большим шаром грязи...
Ответ 6
Как насчет того, чтобы на ваших Актерах (существах, предметах) был метод, который выполнял действие по цели (-ам). Таким образом, каждый элемент может действовать по-разному, и у вас не будет одного крупного массивного метода борьбы со всеми отдельными предметами/существами.
Пример:
public abstract bool PerformAction(Object target); //returns if object is a valid target and action was performed
Ответ 7
У меня была схожая ситуация с этим, хотя мои не были ролевыми играми, но устройствами, которые иногда имели схожие характеристики с другими устройствами, но также и с некоторыми уникальными характеристиками. Ключ состоит в том, чтобы использовать интерфейсы для определения класса действий, например ICanAttack
, а затем реализовать конкретный метод для объектов. Если вам нужен общий код для обработки этого по нескольким объектам, и нет четкого способа получения одного из другого, вы просто используете класс утилиты со статическим методом для реализации:
public interface ICanAttack { void Attack(Character attackee); }
public class Character { ... }
public class Warrior : Character, ICanAttack
{
public void Attack(Character attackee) { CharacterUtils.Attack(this, attackee); }
}
public static class CharacterUtils
{
public static void Attack(Character attacker, Character attackee) { ... }
}
Затем, если у вас есть код, который должен определить, может ли символ или не может что-то сделать:
public void Process(Character myCharacter)
{
...
ICanAttack attacker = null;
if ((attacker = (myCharacter as ICanAttack)) != null) attacker.Attack(anotherCharacter);
}
Таким образом, вы явно знаете, какие возможности имеет какой-либо конкретный тип персонажа, вы получаете хорошее повторное использование кода, а код относительно самодокументирован. Основным недостатком этого является то, что в конечном итоге легко получить объекты, которые реализуют множество интерфейсов, в зависимости от сложности вашей игры.
Ответ 8
Это может быть не то, о чем многие согласятся, но я не команда, и это работает для меня (в большинстве случаев).
Вместо того, чтобы думать о каждом объекте как о наборе вещей, подумайте об этом как о наборе ссылок на материал. В принципе, вместо одного огромного списка из многих
Object
- Position
- Legs
- [..n]
У вас было бы что-то вроде этого (со значениями разделились, оставив только отношения):
![Table showing relationships between different values]()
Всякий раз, когда ваш игрок (или существо или [..n]) хочет открыть поле, просто вызовите
Player.Open(Something Target); //or
Creature.Open(Something Target); //or
[..n].Open(Something Target);
Где "Что-то" может быть набором правил или просто целым числом, которое идентифицирует цель (или даже лучше самого объекта), если цель существует и действительно может быть открыта, откройте ее.
Все это можно (вполне) легко реализовать через ряд интерфейсов, например:
interface IDraggable
{
void DragTo(
int X,
int Y
);
}
interface IDamageable
{
void Damage(
int A
);
}
С умным использованием этих интерфейсов вы, возможно, даже закончите использование таких вещей, как делегаты, чтобы сделать абстракцию между верхним уровнем
IDamageable
и подуровня
IBurnable
Надеюсь, что это помогло:)
РЕДАКТИРОВАТЬ: Это было смущение, но, похоже, я захватил @потрясающий ответ! Извините, @потрясающе! Во всяком случае, посмотрите на его пример, если вы хотите использовать фактический пример вместо объяснения того, как работает концепция.
РЕДАКТИРОВАТЬ 2: О, дерьмо. Я только что увидел, что вы четко заявили, что не хотите, чтобы какой-либо материал, содержащийся в статье, которую вы связали, совершенно точно так же, как я написал здесь! Не обращайте внимания на этот ответ, если вам нравится и жаль!