Помощь в реализации взаимодействия существ и предметов в компьютерной ролевой игре - PullRequest
13 голосов
/ 01 февраля 2010

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

  1. Создание гигантского игрового объекта, который может быть чем угодно и делать все, что угодно
  2. Сложность - поэтому я держусь подальше от компонентного дизайна, как вы см. Здесь

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

Например

  • Существа (Персонажи, Монстры, 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

Или я должен думать об этом совсем по-другому?

Ответы [ 8 ]

4 голосов
/ 01 февраля 2010

Это проблема "двойной отправки". В обычном ОО-программировании вы «отправляете» операцию вызова виртуального метода на конкретный тип класса, реализующего экземпляр объекта, против которого вы вызываете. Клиенту не нужно знать фактический тип реализации, он просто вызывает метод против абстрактного описания типа. Это «разовая отправка».

Большинство языков OO не реализуют ничего, кроме единой диспетчеризации. Двойная диспетчеризация - это когда операция, которую нужно вызвать, зависит от двух разных объектов. Стандартным механизмом реализации двойной диспетчеризации на языках OO без прямой поддержки двойной диспетчеризации является шаблон проектирования « Visitor ». См. Ссылку для использования этого шаблона.

3 голосов
/ 05 февраля 2010

Вы можете попробовать смешать шаблон 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);
        }
    }
}

Обратите внимание, что:

  1. Производный объект должен реализовывать только те возможности, которые ему подходят.

  2. Базовый класс Entity не имеет кода для любой возможности. Это красиво и просто.

  3. Команды могут изящно ничего не делать, если они не затрагивают сущность.

3 голосов
/ 01 февраля 2010

Это звучит как случай полиморфизма. Вместо того, чтобы принимать Предмет или Существо в качестве аргумента, заставьте их обоих наследовать (или реализовывать) из ActionTarget или ActionSource. Пусть реализация Существа или Предмета определит, куда идти дальше.

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

2 голосов
/ 02 февраля 2010

Я думаю, вы рассматриваете слишком маленькую часть проблемы; Как вы вообще определяете аргументы функции 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

Будьте проще. :)

1 голос
/ 01 февраля 2010

в модели Zork каждое действие, которое можно выполнить с объектом, выражается как метод этого объекта, например,

door.Open()
monster.Attack()

что-то общее, например, PerformAction, в конечном итоге станет большим шариком грязи ...

0 голосов
/ 16 сентября 2011

С этим многие могут не согласиться, но я не команда, и это работает для меня (в большинстве случаев).

Вместо того, чтобы думать о каждом Объекте как о коллекции вещи , думайте о ней как о коллекции ссылок на вещи . По сути, вместо одного огромного списка из множества

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

Надеюсь, это помогло:)

РЕДАКТИРОВАТЬ: Это было неловко, но, кажется, я угнал ответ @ munificent! Я извиняюсь @muntiful! В любом случае, посмотрите на его пример, если вам нужен фактический пример вместо объяснения того, как работает концепция.

РЕДАКТИРОВАТЬ 2: О дерьмо. Я только что увидел, что вы четко заявили, что не хотите ничего из того, что содержалось в статье, на которую вы ссылались, , что совершенно точно соответствует тому, что я написал здесь ! Не обращайте внимания на этот ответ, если вам нравится, и извините за это!

0 голосов
/ 01 февраля 2010

У меня была похожая ситуация с этим, хотя у меня была не ролевая игра, а устройства, которые иногда имели характеристики, аналогичные другим устройствам, но также имели некоторые уникальные характеристики. Ключ заключается в том, чтобы использовать интерфейсы для определения класса действий, например 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);
}

Таким образом, вы явно знаете, какими возможностями обладает какой-либо конкретный тип символов, вы получаете хорошее повторное использование кода, и код относительно самодокументируется. Основным недостатком этого является то, что легко получить объекты, которые реализуют МНОГО интерфейсов, в зависимости от сложности вашей игры.

0 голосов
/ 01 февраля 2010

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

пример:

public abstract bool PerformAction(Object target);  //returns if object is a valid target and action was performed
...