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

时间:2016-08-20 01:12:46

标签: events design-patterns game-engine software-design game-loop

我正在试图找出用于管理两个交互对象之间“竞争”的最佳设计模式。例如,如果我想要一个Fox类通过一个简单的环境追逐Rabbit类。我想让他们“竞争”并找出哪一个获胜。最终它将成为学生可以用来试验继承和其他OO编程技能的教学工具。

此用例是否有既定的设计模式?

这是我能想到的最好的:一个类来表示托管其他对象的环境。我保持它非常简单,并假设动物只是直线运行,如果他足够接近兔子,狐狸抓住了兔子。这是一些代码,展示了我所描述的内容。我使用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() ... run(),直到它第一次抓住兔子为止。

  3. 我可能还没有注意到其他问题。我怀疑上述(1)和(2)的答案都是某种事件系统,允许一只动物采取行动从另一只动物触发行动,反之亦然。我也认为可能需要对每个动作的时间进行一些表示,但我并不完全确定。

7 个答案:

答案 0 :(得分:3)

因此,您的任务是在设计模式下安装类似游戏的东西,最初只为企业类软件创建。根据定义,游戏不是企业软件,这也是许多人在设计游戏时避免考虑设计模式的原因。但这并不意味着它不可行。

我的建议:

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

所以,如果你把上面两个结合起来(我希望第二个不在那里),那么我就是这样设计的(如果我的建议提醒我,我会提到设计模式):

  1. 标记当前时间点。
  2. 环境开始游戏循环。
  3. 对于每个循环步骤,计算自上一个时间点以来经过的时间。这将为您提供某些单位的时间跨度(例如,通过N毫秒)。
  4. 给定时间跨度,您需要让每个对象更新其状态(从概念上讲,问他们 - 如果N毫秒已经过去,您现在在哪里?)。这让我想起了访客模式。
  5. 在所有对象更新状态后,环境会在屏幕上显示结果(在真实游戏中,这意味着绘制游戏的当前状态 - 每个对象都会在屏幕上重绘;对于简单的应用程序,您可以检查Fox是否据报道它抓住了兔子。)
  6. 显然,在循环步骤中,您需要继续标记当前时间,以便可以在每一步计算时间跨度差异。
  7. 步骤#4涉及一些复杂性,特别是如果准确性至关重要的话。例如如果时间跨度大约是一秒钟,并且在那一秒内(在中间的某个地方),狐狸会抓住兔子,但最终还是有距离?如果狐狸和兔子的速度是时间函数(有时它们减速,有时它们加速)会发生这种情况[顺便说一下,这听起来像一个策略模式 - 用于计算当前速度的变化 - 例如线性与时间函数)。显然,如果狐狸和兔子都只是在时间跨度结束时报告他们的位置,那么将错过捕捉时刻,这是不可取的。

    以下是我将如何解决它:对于给定的时间跨度,如果超过一毫秒(假设毫秒是最短的可接受原子时间以获得足够高的准确度),则将其拆分进入每个毫秒长度的时间跨度,并且对于每毫秒,要求每个对象更新其状态。毕竟,如果对象可以根据时间跨度更新其状态,我们可以根据需要多次调用它,缩短时间跨度。显然,有不可避免的副作用 - 你需要按某种顺序更新状态,但鉴于毫秒太短的时间,这样做应该没问题。

    伪代码看起来像这样:

    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
            }
        }
    
    }
    

答案 1 :(得分:2)

在游戏循环中,通常会更新两个对象的速度(此处在您的反应函数中),然后更新对象的位置。因此同时移动。

while(!gameOver) {
 rabbit->react(fox);
 fox->react(rabbit);
 rabbit->updatePosition();
 fox->updatePosition();
}

为了限制每转/每帧的活动,你必须想到一些聪明的东西。例如,您可以制作一组可以执行的操作,并且每个操作都有能源成本。每回合你都会获得一定的能量。你必须有多个run()动作才能使它变得有趣:)。

答案 2 :(得分:2)

  • 我会建议Mediator pattern。它通常促进松散 一组对象之间的直接耦合(在你的情况下是狐狸和 兔子)。您需要引入另一个捕获的对象 由于兔子和兔子的行为导致的系统状态 狐狸(例如称之为"追逐")。
  • 中介对象将处理所有人之间的交互 对象。我评估兔子和狐狸要求的行动, 然后确定这些行动的实际结果 (一切都通过调解员!)并更新"追逐" 因此。通过这种方式,您可以控制上述问题1和2, 或其他问题。
  • 我之前已经为HMI接口实现了这种模式, 用户可以通过键盘和系统与系统进行交互。屏幕, 并根据选择/系统状态/以前的选择, 等需要发生适当的状态转换。

答案 3 :(得分:1)

总体而言,我的方法是:

  1. 每个主题(狐狸,兔子等)都有一个状态(在你的情况下是速度,位置和方向)。
  2. 环境(容器)的状态是主体的状态和其他约束的组合(如果需要)(不可穿透的区域,破坏的地形等)。
  3. 每个主题都有一个最小化的成本函数(狐狸有主体之间的距离,兔子是这种距离的倒数)
  4. 每个主题必须有一些约束(例如max_distance_per_turn,max_direction_changes_per_turn等)以防止第一个主题在时间1获胜。
  5. 环境状态可能会影响主题行为(例如狐狸跑步挖出一个无法通过兔子的洞),因此每个动作都必须了解全局当前状态。
  6. 每个主题仅从起始环境+主题状态开始修改其状态;狐狸在0时相应地移动到兔子的位置(兔子做同样的事情)。请注意,这与fox.react(兔子)不同; rabbit.react(FOX);因为兔子知道狐狸在时间1的位置(移动后)。
  7. 如果需要的话,也应检查整个交易:如果在时间0时兔子不能咬人,而且在时间1它也不能咬人,但在过渡期间它达到了可咬的地方,狐狸应该赢。为了避免这一点,目标是尽可能使“转弯”成为原子,以便忽略这些交易。或者,您可以在每个回合后的环境中添加事务检查。
  8. 在更一般的视图中,您可以将该环境视为具有追溯的封闭系统:每个操作都会修改将影响新操作的整个状态。在这种情况下,每个构造仍然是顺序的,但每个“转向”确实是从一个状态到下一个状态的封闭事务,其中所有主体同时执行。

答案 4 :(得分:1)

阐明中介模式建议,为了增加同时性的错觉,可以将游戏状态提取到单独的对象(普通旧数据),并在所有对象做出决定后更新。例如(在java-ish语言中)

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类,增加健康水平,力量等。

这种方法解决了你的问题,因为每个玩家(狐狸和兔子)都不知道对方在同一回合做了什么,所以兔子可以逃避狐狸而狐狸不能只是{{1它是受害者(因为它不知道兔子会在哪里移动)。有趣的事实 - 权力的游戏棋盘游戏使用相同的技术创造一种与其他玩家同时向你的军队下达命令的错觉。

答案 5 :(得分: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;
}

}

这是Carnivore和Omnivore Classes

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();

}

这是兔子和狐狸类。 Rabbit和fox类在它们的构造函数中有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 ;
        }
    }
}

} 下面的init方法

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();
}

如果你想让这个更复杂,比如合作或其他什么 你需要使用可观察的模式。 它可以用于通过抽象工厂模式创建动物,森林。 很抱歉因为我没有太多时间而乱码。 我希望这会对你有所帮助。

答案 6 :(得分:1)

兔子,狐狸和可能的其他动物之间的竞争可以使用discrete event simulation建模,可以将其视为设计模式本身(模拟时钟,事件队列......)。 / p>

对象可以实现Strategy pattern。在这种情况下,execute方法可以命名为decideAction - 它将获得世界的旧状态(只读)并产生决策(操作描述)。

模拟将计划由决策产生的事件。处理事件时,模拟将改变世界的状态。因此,模拟可以被认为是Mediator pattern的一个实例,因为它将代理与直接交互隔离开来 - 他们只看到世界的状态并产生决策,同时生成新的世界状态和评估规则(如如速度和成功咬合或逃逸的检测)留待模拟。

为了使所有代理人(动物)同时做出决定,计划所有事件同时发生(在模拟时间内)并且仅在处理了在相同模拟时间发生的所有事件之后更新世界状态(决定已经制造)。

然后,您将不需要事件队列和模拟时钟。只需一个循环来收集所有决策并最终在每次迭代中更新世界状态就足够了。

但这可能不是您想要的,例如,因为最初,兔子可能需要一些时间来注意到狐狸正在接近。如果动物的事件之间的超时(反应时间)随着状态(警报,睡眠等)而变化,则可能更有趣。

当动物可以直接改变世界状态并且使用几乎任意的代码实现时,每个“转弯”的活动量不能被限制。如果动物只描述了它的动作,它的类型和参数可以通过模拟验证,并可能被拒绝。