建模两个竞争对象的设计模式

7
我正在尝试找出最佳的设计模式来管理两个相互作用的对象之间的“竞争”。例如,如果我想要一个追逐Rabbit类的Fox类通过一个简单的环境。我想让它们“竞争”,并找出谁赢了。最终它将成为一个教学工具,学生可以用来实验继承和其他面向对象编程技巧。
这种情况下是否有已经建立的设计模式?
这是我能想到的最好的方法:一个类来代表承载这两个对象的环境。我保持它非常简单,并假设动物只会直线奔跑,如果狐狸靠近兔子咬兔子,狐狸就会抓到兔子。以下是一些演示我所描述的内容的代码。我使用PHP是因为我可以快速地编写它,但我不想关注语言的具体细节。我的问题实际上是关于设计模式/架构的。
class Forrest() {
        public $fox;
        public $rabbit;
        public $width = 100; //meters?
        public $length = 100;

        __construct() {
                $this->fox = new Fox();
                $this->rabbit = new Rabbit();
                $this->theChase();
        }

        public function theChase() {
                 while (!$this->rabbit->isBitten) {
                         $this->rabbit->react($fox);
                         $this->fox->react($rabbit);
                 }
                 log('The fox got the rabbit!');
        }
}

abstract class Animal() {
        public $speed;
        public $hasTeeth = false;
        public $position;
        public $direction;
        public $isBitten = false;
        public function run($distance) {
                // update coordinates based on direction and speed
        }

        public function bite($someone) {
                 if (isCloseEnough( $someone ) && $this->hasTeeth) {
                          $someone->isBitten = true;
                          log(get_class($this) . ' bit the ' . get_class($someone)); //the Fox bit the Rabbit
                 }
        }

        public abstract function react($someone);
}

class Rabbit extends Animal {
         __construct() {
                  $this->speed = 30;
                  $this->position = [0,0];
                  $this->direction = 'north';
                  log(get_class($this) . ' starts at 0,0');
         }

         public react($fox) {
                //avoid the fox
         }
}

class Fox extends Animal {
          __construct() {
                  $this->speed = 20;
                  $this->position = [100,100];
                  $this->direction = 'south';
                  log (get_class($this) . ' starts at 100,100');
          }

          public react($rabbit) {
                  //try to catch the rabbit
          }
}

我发现这种方法存在两个即时问题:
  1. 这种架构会导致顺序交替动作。换句话说,首先兔子做某事,然后狐狸做某事,然后兔子再做某事……这更像是一种纸牌游戏,每个玩家轮流移动。如果两个对象都能同时做出反应,那将会更有趣。
  2. 目前还没有限制每个“回合”活动量的系统。需要对单个“回合”中发生的事情进行某种限制,以保持趣味性。否则,狐狸可以简单地 run() run() run() ...... 直到在第一回合就抓住了兔子。
可能还存在其他我尚未注意到的问题。我怀疑解决上述问题(1)和(2)的答案是某种事件系统,允许一个动物的行动触发另一个动物的行动,反之亦然。我也认为每个行动可能需要与时间相关联,但我不太确定。
7个回答

3

因此,您的任务是将类似游戏的东西纳入设计模式中,这些模式最初只用于企业类型的软件。游戏从定义上来说不是企业软件,这就是为什么许多人在设计游戏时避免考虑设计模式的原因。但这并不意味着无法做到。

我的建议:

  • 先考虑模型:按照您设想的方式设计问题。
  • 记住,它仍然是一个游戏,因此需要遵循游戏开发模式:实际上只有一个——游戏循环。

因此,如果将上述两者结合起来(我希望第二个不存在),那么我会这样设计它(如果我的建议提醒我某种设计模式,我会提到它):

  1. 标记当前时间点。
  2. 环境启动游戏循环。
  3. 对于每个循环步骤,计算自上次时间点以来经过的时间。这将给您一些单位的时间跨度(例如,经过了N毫秒)。
  4. 给定时间跨度,您需要询问每个对象更新其状态(在概念上,询问它们——如果经过了N毫秒,你现在会在哪里?)。这有点像访问者设计模式。
  5. 在所有对象更新其状态之后,环境将结果显示在屏幕上(在真实的游戏中,这意味着绘制游戏的当前状态——每个对象都将在屏幕上重新绘制;对于您的简单应用程序,您可以检查Fox是否报告说它已经抓住了兔子)。
  6. 显然,在循环步骤中,您需要不断标记当前时间,以便可以在每个步骤中计算时间跨度差异。

第4步涉及一些复杂性,特别是如果准确性很重要。例如,如果时间跨度大约为1秒,在其中某个位置内狐狸就会抓住兔子,但最终仍存在距离怎么办?如果狐狸和兔子的速度是时间的函数(有时会减慢,有时会加快),则可能会发生这种情况[顺便说一句,这听起来像是策略模式 - 计算当前速度的变化 - 例如线性与时间函数的变化)。显然,如果狐狸和兔子只在时间段结束时报告其位置,则会错过捕捉时刻,这是不可取的。

这里是我的解决方案:对于给定的时间跨度,如果它超过1毫秒(假设毫秒是足够好的准确性的最短可接受原子时间),则将其分成每个毫秒长度的时间段,并为每个毫秒询问每个对象更新其状态。毕竟,如果对象可以根据时间跨度更新其状态,那么我们可以无限次数地用较短的时间跨度调用它。显然,存在不可避免的副作用-您需要以某种顺序更新状态,但是鉴于毫秒是太短的时间段,这应该没问题。

伪代码如下:

var foxAndRabbitGame = new FoxAndRabbitGame();
foxAndRabbitGame.RunGame(screen); //visitor
/* when this line is reached, game is over. Do something with it. */

class FoxAndRabbitGame
{
    private fox = new Fox(Speed.Linear()); //strategy
    private rabbit = new Rabbit(Speed.Linear()); //strategy


    void RunGame(screen)
    {
        var currentTime = NOW;
        while (true)
        {
            var timePassed = NOW - currentTime;
            currentTime = NOW;

            foreach (millisecond in timePassed)
            {
                fox.UpdateState ( millisecond , rabbit );
                rabbit.UpdateState ( millisecond, fox );

                if (fox.TryBite(rabbit))
                {
                    //game over.
                    return;
                }
            }

            //usually, drawing is much slower than calculating state,
            //so we do it once, after all calculations.
            screen.Draw(this); //visitor
            screen.Draw(Fox); //visitor
            screen.Draw(rabbit); //visitor
        }
    }

}

然而有一个小问题 - 兔子和狐狸对象没有被正确地封装,这并没有解决OP所述的第二个问题。此外,这是一个非常技术性的答案,虽然这并不一定是错误的,但我认为我们正在讨论一个抽象的设计问题,而不是如何实现游戏引擎。 - Dunno
我不认同你关于设计模式只适用于企业软件的观点。实际上,你在回答中使用了其中两个设计模式,这已经证明了它们并非如此。许多人认为游戏开发不需要良好的架构设计、面向对象思维和优秀的编程能力,这是游戏开发行业(我正好在从事该行业)面临的真正问题。 - Dunno
@Dunno,我认为我通过建议引入最小原子时间跨度(在我的答案中为1毫秒)来回答了OP的第二个问题;我不保证我的问题解释是唯一正确的。至于设计模式和游戏,我指的是对于游戏而言,帧率比代码质量更重要。因此,许多模式不适用于游戏;例如,其中许多要求更丰富的内存分配以获得更清晰的代码,而游戏永远不会这样做,而企业则喜欢它们的可维护性效果。在游戏中,帧率是王者。在企业中,模式是。 - Tengiz
帧率受到每帧进行昂贵计算或渲染的影响。除非你在做超级复杂的 RTS 并且不做任何愚蠢的事情(比如每帧迭代大量对象),否则后者会引起更多问题。设计模式在游戏开发中与任何其他编程工作一样重要,只是游戏开发往往吸引了那些只想玩乐和制作游戏的糟糕程序员。 - Dunno
@Dunno,我很高兴听到来自游戏开发者(假设这是你的职业)的消息。我一直犹豫是否进入该行业,只因为似乎他们不关注设计模式和代码质量问题。现在,我感觉问题不在于我,而是他们 :-) 谢谢! - Tengiz

2
我建议使用中介者模式。它通常促进了一组对象(在你的情况下是狐狸和兔子)之间的松散直接耦合。您需要引入另一个对象,它捕获系统状态作为兔子和狐狸行动的结果(例如将其称为“追逐”)。
中介者对象将处理所有对象之间的交互。即评估兔子和狐狸请求的操作,然后确定这些操作的实际结果是什么(所有内容都通过中介者!),并相应地更新“追逐”。这样,您可以控制上述问题1和2或其他问题。
我以前已经为HMI界面实现过此模式,其中用户可以通过键盘和屏幕与系统交互,并根据选择/系统状态/先前选择等进行适当的状态转换。

2
在游戏循环中,通常会更新两个对象的速度(在这里是您的react函数),然后更新对象的位置。因此同时移动。
while(!gameOver) {
 rabbit->react(fox);
 fox->react(rabbit);
 rabbit->updatePosition();
 fox->updatePosition();
}

如果要限制每轮/帧的活动,您需要想出一些聪明的方法。例如,您可以创建一组特定的操作并为每个操作设定能量消耗。每轮将获得一定数量的能量可供使用。但您需要有多个run()操作才能使其更加有趣 :).


我想我明白了。通过分离react()和update(),它允许每个对象同时移动。 - emersonthis
在实际版本中,肯定会有几个不同的操作。因此结果不太可预测。我只是试图让这个例子非常简单。 - emersonthis

1
一般而言,我的方法是:
  1. 每个主体(狐狸、兔子等)都有一个状态(在您的情况下是速度、位置和方向)。
  2. 环境(容器)具有状态,该状态是主体状态和其他约束条件(如不可穿透区域、毁坏地形等)的组合,如果需要的话。
  3. 每个主体都有一个要最小化的成本函数(狐狸有主体之间的距离,兔子则是这种距离的倒数)
  4. 每个主体必须有一些限制(例如每回合的最大距离、每回合的最大方向变化次数等),以防止第一个主体在时间1获胜。
  5. 环境状态可以影响主体行动(例如狐狸奔跑时挖了一个兔子无法通过的洞),因此每个行动都必须知道全局当前状态。
  6. 每个主体仅从起始环境+主体状态开始修改其状态;狐狸根据兔子在时间0的位置移动(兔子也是如此)。请注意,这与狐狸.react(兔子);兔子.react(狐狸)不同;因为兔子知道狐狸在时间1(移动后)的位置。
  7. 如果需要,还应检查整个事务:如果在时间0兔子不能被咬,在时间1它仍然无法被咬,但在转换过程中它到达了一个可以被咬的点,则狐狸应获胜。为了避免这种情况,目标是尽可能使“回合”原子化,以忽略那些交易。或者您可以在每个回合后向环境添加交易检查。
在更一般的观点中,您可以将该环境视为具有反馈闭合系统:每个操作都会修改整个状态,从而影响新的操作。在这种情况下,每个指令仍然是顺序的,但每个“回合”实际上是从一个状态到下一个状态的封闭事务,在其中所有主题同时执行。

1
兔子、狐狸以及其他动物之间的竞争可以使用离散事件模拟进行建模,这可以被认为是一种设计模式(模拟时钟、事件队列等)。
对象可以实现策略模式。在这种情况下,execute方法可以被命名为decideAction——它将获取旧的世界状态(只读),并产生一个决策(行动描述)。
然后,模拟将计划由该决策产生的事件。当事件被处理时,模拟将改变世界的状态。因此,模拟可以被认为是中介者模式的一个实例,因为它将代理从直接交互中隔离出来——它们只看到世界的状态并做出决策,而新状态的生成和规则的评估(例如速度和成功咬或逃脱的检测)留给了模拟。
为了使所有代理(动物)的决策同时进行,计划所有事件在模拟时间内同时发生,并且只有在处理同一模拟时间内发生的所有事件(做出决策)之后才更新世界状态。那么你就不需要事件队列和模拟时钟了,只需要一个循环来收集所有的决策,最后在每次迭代中更新世界状态即可。
当然,这可能不是你想要的,例如因为兔子要花费一些时间才能注意到狐狸正在靠近。如果动物的超时时间(反应时间)与它的状态(警觉、睡眠等)有所不同,那么可能会更有趣。
每个“回合”的活动量是无法限制的,当动物可以直接改变世界状态并使用几乎任意的代码实现时。如果动物只是描述其行为,则模拟可以验证其类型和参数,并可能被拒绝。

这是一个很好的答案。但我不理解最后两句话。为什么活动量不能被限制?代码为什么是任意的? - emersonthis
通过任意代码我指的是由其他人编写的代码,该人在编写时没有任何限制,除了技术方面的限制。这个代码在技术上可能做任何事情,实际上也可能做到。当您将世界状态暴露给此代码并且允许代码直接写入它时,您必须做一些非常疯狂的事情来限制活动量-例如保留原始状态的副本,并从比较两个状态获取操作描述,然后才进行验证。最好直接获取操作描述。 - Palec
顺便提一下,您可能不仅想限制动作,还要限制计算动作时花费的处理器时间。否则,玩家可能会花费疯狂的时间来计算最佳策略。然而,这种约束在技术上更具有挑战性,并且如果您的学生只是玩模拟而不是为动物编写良好的AI,则是不必要的。 - Palec
通过以声明方式描述行动来限制行动似乎真的是标准做法。有趣的是,可以看看[CodeGolf.SE]上的Survival Game - Create Your Wolf。特别是EmoWolf非常棒。 :-) - Palec

1
详细说明中介者模式的建议,为了增加同时性的幻觉,游戏状态可以被提取到一个单独的对象(纯数据)中,并在所有对象做出决策后进行更新。例如(在类似Java的语言中)。
public class OpponentData {
    private Position theirPosition; // + public get

    // constructor with theirPosition param, keeping the class immutable
}

public interface Animal {
    // returns data containing their updated data
    OpponentData React(OpponentData data);
    Position GetPosition();
}

public class Fox implements Animal {
    public OpponentData React(OpponentData data) {
        if (this.position == data.GetPosition())
            // this method can be a little tricky to write, depending on your chosen technology, current architecture etc
            // Fox can either have a reference to GameController to inform it about victory, or fire an event
            // or maybe even do it itself, depending if you need to destroy the rabbit object in game controller
            EatTheRabbit();
        else {
            // since the game state won't be updated immediately, I can't just run until I reach the rabbit
            // I can use a bunch of strategies: always go 1 meter forward, or use a random value or something more complicated
            ChaseTheRabbit();
        }
        return new OpponentData(this.position);
    }
}

public class Rabbit implements Animal {
        public OpponentData React(OpponentData data) {
            KeepRunningForYourLife();
            // maybe you can add something more for the rabbit to think about
            // for example, bushes it can run to and hide in
            return new OpponentData(this.position);
        }
}

public class GameController {
    private Fox fox;
    private Rabbit rabbit;
    private OpponentData foxData;
    private OpponentData rabbitData;

    private void Init() {
        fox = new Fox();
        rabbit = new Rabbit();
        foxData = new OpponentData(fox.GetPosition());
        rabbitData = new OpponentData(rabbit.GetPosition());
    }

    private void PerformActions() {
        var oldData = foxData;
        foxData = fox.React(rabbitData);
        // giving the old data to the rabbit so it doesn't know where the fox just moved
        rabbitData = rabbit.React(oldData);
    }
}

如果您希望游戏不仅依赖于位置,还可以轻松扩展OpponentData类,添加健康水平、力量等因素。这种方法解决了您的两个问题,因为每个玩家(狐狸和兔子)在同一回合中都不知道对方正在做什么,所以兔子可以躲避狐狸,而狐狸不能只是run() run() run()去追它的猎物(因为它不知道兔子要移动到哪里)。有趣的是,《权力的游戏》棋盘游戏使用相同的技术来制造给予你的军队与其他玩家同时下达指令的幻觉。

1
我认为与动物抽象类相关的应该有两个抽象类。(杂食类和肉食类,它们具有不同的属性)
这是动物抽象类:
public abstract class Animal implements Runnable{
private double speed = 0 ; // Default
private Point location = new Point(new Random().nextInt(50) + 1 , new Random().nextInt(50) + 1);

abstract void runAway(Animal animal);
abstract void chase(Animal animal);
abstract void search4Feed();
abstract void feed();

public synchronized Point getLocation() {
    return location;
}
public synchronized void setLocation(Point location) {
    this.location = location;
}

public double getSpeed() {
    return speed;
}

public void setSpeed(double speed) {
    this.speed = speed;
}

}

这是食肉动物和杂食动物类的代码。
public abstract class Carnivore extends Animal {
Animal targetAnimal ;

}

public abstract class Omnivore extends Animal {
Animal chasingAnimal;

}

对于森林类及其实现,可以通过不同的森林类来实现Iforest。同时需要保持其独立的动物生态系统。

public class Forest implements IForest {
private List<Animal> animalList = new ArrayList<Animal>();

public Forest() {

}

@Override
public void addAnimalToEcoSystem(Animal animal) {
    animalList.add(animal);
}

@Override
public void removeAnimalFromEcoSystem(Animal animal) {
    animalList.remove(animal);
}

@Override
public void init() {
    // to do:       
}

@Override
public List<Animal> getAnimals() {
    return this.animalList;
}

}

public interface IForest {
void removeAnimalFromEcoSystem(Animal animal);
void addAnimalToEcoSystem(Animal animal);
List<Animal> getAnimals();
void init();

}

这里是兔子和狐狸的类。 兔子和狐狸的类在它们的构造函数中有IForest类实例。 追捕任何动物或从任何动物逃跑都需要森林生态系统, 并且这些类必须通过IForest接口将它们的运动通知给Forest类。 我在这里使用了Runnable线程,因为这些类需要独立移动,而不是顺序移动。 在run方法中,您可以根据指定的条件定义猎人或猎物的规则。
public class Rabbit extends Omnivore {

private IForest forest = null ;

public Rabbit(IForest forest) {
    this.forest = forest;
    this.setSpeed(40);
}

@Override
public void runAway(Animal animal) {
    this.chasingAnimal = animal;
    this.run();
}

@Override
public void chase(Animal animal) {
    // same as fox's
}

@Override
void feed() {
    // todo:        
}

@Override
void search4Feed() {

}

@Override
public void run() {
    double distance = 10000; //default,
    this.chasingAnimal.runAway(this); // notify rabbit that it has to run away
    while(distance < 5){ // fox gives chasing up when distance is greater than 5
        distance = Math.hypot(this.getLocation().x - this.chasingAnimal.getLocation().x, 
                this.getLocation().y - this.chasingAnimal.getLocation().y);
        if(distance < 1) {
            break; // eaten
        }
        //here set  new rabbit's location according to rabbit's location
    }
}

}

public class Fox extends Carnivore {

private IForest forest = null ;

public Fox(IForest forest) {
    this.forest = forest;
    this.setSpeed(60);
}

@Override
public void chase(Animal animal) {
    this.targetAnimal = animal;
    this.run();
}

@Override
public void run() {
    double distance = 10000; //default,
    this.targetAnimal.runAway(this); // notify rabbit that it has to run away
    while(distance < 5){ // fox gives chasing up when distance is greater than 5
        distance = Math.hypot(this.getLocation().x - this.targetAnimal.getLocation().x, 
                this.getLocation().y - this.targetAnimal.getLocation().y);
        if(distance < 1) {
            feed();
            break;
        }
        //here set  new fox's location according to rabbit's location
    }
}

@Override
public void runAway(Animal animal) {
    // same as rabbit's
}

@Override
public void feed() {
    // remove from forest's animal list for the this.targetAnimal
}

@Override
void search4Feed() {
    // here fox searches for closest omnivore
    double distance = -1;
    Animal closestFeed = null;
    List<Animal> l = this.forest.getAnimals();
    for (Animal a : l) {
        double d = Math.hypot(this.getLocation().x - a.getLocation().x, this.getLocation().y - a.getLocation().y);
        if (distance != -1) {
            if(d < distance){
                this.chase(a);
            }
        }
        else{
            distance = d ;
        }
    }
}

下面是初始化方法:

}

public static void main(String[] args) {
    // you can use abstract factory pattern instead.
    IForest forest = new Forest();
    forest.addAnimalToEcoSystem(new Rabbit(forest));
    forest.addAnimalToEcoSystem(new Fox(forest));
    forest.init();
}

如果你想让这更加复杂,例如协作或其他事情,你需要使用观察者模式。它可以用于通过抽象工厂模式创建动物、森林等。由于时间不够,代码可能有些混乱,敬请谅解。希望这能对你有所帮助。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接