9

我目前正在 Scala 中开发一款游戏,其中有许多实体(例如 GunBattery、Squadron、EnemyShip、EnemyFighter),它们都继承自 GameEntity 类。游戏实体通过事件/消息系统向游戏世界和彼此广播感兴趣的事物。有许多 EventMesssage(EntityDied、FireAtPosition、HullBreach)。

目前,每个实体对于它响应的每种消息类型(例如)都有一个receive(msg:EventMessage)以及更具体的接收方法。receive(msg:EntityDiedMessage)通用receive(msg:EventMessage)方法只是一个 switch 语句,它根据消息的类型调用适当的接收方法。

随着游戏的开发,实体和消息的列表(以及哪些实体将响应哪些消息)是流动的。理想情况下,如果我希望游戏实体能够接收新的消息类型,我只想能够编写响应的逻辑,而不是这样做并且必须在其他地方更新匹配语句。

我的一个想法是将接收方法从游戏实体层次结构中提取出来并具有一系列函数,例如def receive(e:EnemyShip,m:ExplosionMessage)和 defreceive(e:SpaceStation,m:ExplosionMessage)但这使问题更加复杂,因为现在我需要一个匹配语句来涵盖消息游戏实体类型。

这似乎与双重多重调度的概念以及访问者模式有关,但我在理解它时遇到了一些麻烦。我并不是在寻找 OOP 解决方案本身,但是如果可能的话,我想避免反思。

编辑

做一些更多的研究,我认为我正在寻找类似于 Clojure 的东西defmulti

您可以执行以下操作:

(defmulti receive :entity :msgType)

(defmethod receive :fighter :explosion [] "fighter handling explosion message")
(defmethod receive :player-ship :hullbreach []  "player ship handling hull breach")
4

5 回答 5

9

您可以在 Scala 中轻松实现多分派,尽管它没有一流的支持。通过下面的简单实现,您可以对示例进行如下编码:

object Receive extends MultiMethod[(Entity, Message), String]("")

Receive defImpl { case (_: Fighter, _: Explosion) => "fighter handling explosion message" }
Receive defImpl { case (_: PlayerShip, _: HullBreach) => "player ship handling hull breach" }

您可以像使用任何其他功能一样使用您的多方法:

Receive(fighter, explosion) // returns "fighter handling explosion message"

请注意,每个多方法实现(即defImpl调用)必须包含在顶级定义(类/对象/特征体)中,并且由您来确保defImpl在使用方法之前发生相关调用。这个实现还有很多其他的限制和缺点,但我会把这些留给读者作为练习。

执行:

class MultiMethod[A, R](default: => R) {
  private var handlers: List[PartialFunction[A, R]] = Nil

  def apply(args: A): R = {
    handlers find {
      _.isDefinedAt(args)
    } map {
      _.apply(args)
    } getOrElse default
  }

  def defImpl(handler: PartialFunction[A, R]) = {
    handlers +:= handler
  }
}
于 2011-12-21T20:20:33.533 回答
3

如果您真的担心创建/维护 switch 语句所需的工作量,您可以使用元编程通过发现程序中的所有 EventMessage 类型来生成 switch 语句。这并不理想,但元编程通常是在代码中引入新约束的最简洁的方法之一;在这种情况下,如果存在事件类型,则需要有一个调度程序,以及一个可以覆盖的默认(忽略?)处理程序。

如果你不想走那条路,你可以让 EventMessage 成为一个案例类,如果你忘记在你的 switch 语句中处理一个新的消息类型,它应该允许编译器抱怨。我编写了一个有大约 150 万玩家使用的游戏服务器,并使用这种静态类型来确保我的调度是全面的,并且它从未引起实际的生产错误。

于 2011-12-20T19:57:29.333 回答
3

责任链

一个标准机制(不是特定于 Scala 的)是一个处理程序。例如:

trait Handler[Msg] {
  handle(msg: Msg)
}

然后您的实体只需要管理处理程序列表:

abstract class AbstractEntity {

    def handlers: List[Handler]

    def receive(msg: Msg) { handlers foreach handle }
}

然后您的实体可以内联声明处理程序,如下所示:

class Tank {

   lazy val handlers = List(
     new Handler {
       def handle(msg: Msg) = msg match {
         case ied: IedMsg => //handle
         case _           => //no-op
       }
     },
     new Handler {
       def handle(msg: Msg) = msg match {
         case ef: EngineFailureMsg => //handle
         case _                    => //no-op
       }
     }
   )

当然,这里的缺点是您失去了可读性,并且您仍然必须记住样板文件,这是每个处理程序的无操作包罗万象的情况。

演员

我个人会坚持重复。您现在所拥有的看起来很像将每个实体视为一个Actor. 例如:

class Tank extends Entity with Actor {

  def act() { 
    loop {
      react {
         case ied: IedMsg           => //handle
         case ied: EngineFailureMsg => //handle
         case _                     => //no-op
      }
    }
  }
}

至少在这里你养成了case在反应循环中添加语句的习惯。这可以调用你的actor类中的另一个方法来采取适当的行动。当然,这样做的好处是您可以利用参与者范式提供的并发模型。你最终得到一个看起来像这样的循环:

react {
   case ied: IedMsg           => _explosion(ied)
   case efm: EngineFailureMsg => _engineFailure(efm)
   case _                     => 
}

你可能想看看akka,它提供了一个性能更高的参与者系统,具有更多可配置的行为和更多的并发原语(STM、代理、事务等)

于 2011-12-20T21:43:33.877 回答
1

无论如何,您都必须进行一些更新;应用程序不仅会根据事件消息神奇地知道要执行哪个响应操作。

案例很好,但是随着您的对象响应的消息列表变长,它的响应时间也会变长。这是一种响应消息的方法,无论您注册多少消息,它都会以相同的速度响应。该示例确实需要使用 Class 对象,但没有使用其他反射。

public class GameEntity {

HashMap<Class, ActionObject> registeredEvents;

public void receiveMessage(EventMessage message) {
    ActionObject action = registeredEvents.get(message.getClass());
    if (action != null) {
        action.performAction();
    }
    else {
        //Code for if the message type is not registered
    }
}

protected void registerEvent(EventMessage message, ActionObject action) {
    Class messageClass = message.getClass();
    registeredEventes.put(messageClass, action);
}

}

public class Ship extends GameEntity {

public Ship() {
    //Do these 3 lines of code for every message you want the class to register for. This example is for a ship getting hit.
    EventMessage getHitMessage = new GetHitMessage();
    ActionObject getHitAction = new GetHitAction();
    super.registerEvent(getHitMessage, getHitAction);
}

}

如果需要,可以使用 Class.forName(fullPathName) 并传入路径名字符串而不是对象本身。

因为执行动作的逻辑包含在超类中,所以创建子类所要做的就是注册它响应的事件并创建一个包含其响应逻辑的 ActionObject。

于 2011-12-20T20:08:59.017 回答
1

我很想将每种消息类型提升为方法签名和接口。这如何转化为 Scala 我不完全确定,但这是我将采用的 Java 方法。

Killable、KillListener、Breachable、Breachlistener 等将以允许运行时检查 (instanceof) 并有助于提高运行时性能的方式显示对象的逻辑和它们之间的共性。不处理 Kill 事件的事情不会被放入java.util.List<KillListener>通知中。然后,您可以避免一直创建多个新的具体对象(您的 EventMessages)以及大量切换代码。

public interface KillListener{
    void notifyKill(Entity entity);
}

毕竟,Java 中的方法在其他方面被理解为消息 - 只需使用原始 Java 语法。

于 2011-12-20T20:29:07.080 回答