9

私は現在、すべて GameEntity クラスから継承する多数のエンティティ (GunBattery、Squadron、EnemyShip、EnemyFighter など) がある Scala でゲームを開発しています。ゲーム エンティティは、イベント/メッセージ システムを介して、関心のあるものをゲームの世界にブロードキャストし、相互にブロードキャストします。多くの EventMesssages (EntityDied、FireAtPosition、HullBreach) があります。

現在、各エンティティには、receive(msg:EventMessage)応答するメッセージ タイプごとに固有の受信メソッド (例: receive(msg:EntityDiedMessage)) があります。一般的なreceive(msg:EventMessage)メソッドは、メッセージの種類に基づいて適切な受信メソッドを呼び出す単なる switch ステートメントです。

ゲームは開発中のため、エンティティとメッセージ (およびどのエンティティがどのメッセージに応答するか) のリストは流動的です。理想的には、ゲーム エンティティが新しいメッセージ タイプを受信できるようにしたい場合は、応答のロジックをコーディングできるようにしたいだけで、それを行うのではなく、他の場所で match ステートメントを更新する必要があります

私が持っていた 1 つの考えは、Game エンティティ階層から receive メソッドを取り出して、def receive(e:EnemyShip,m:ExplosionMessage)および defのような一連の関数を持たせることでしたが、これは、メッセージゲーム エンティティ タイプreceive(e:SpaceStation,m:ExplosionMessage)の両方をカバーするための match ステートメントが必要になるため、問題を悪化させます。

これは、 DoubleおよびMultipleディスパッチの概念と、おそらく Visitor パターンに関連しているように見えますが、頭を悩ませているところがあります。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 ステートメントを生成できます。理想的ではありませんが、メタプログラミングは通常、コードに新しい制約を導入する最もクリーンな方法の 1 つです。この場合、イベント タイプが存在する場合、そのイベント タイプのディスパッチャーと、オーバーライド可能なデフォルト (無視しますか?) ハンドラーが存在する必要があります。

そのルートに行きたくない場合は、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では、react ループ内にステートメントを追加する習慣を身につけます。これにより、適切なアクションを実行するアクター クラスの別のメソッドを呼び出すことができます。もちろん、これの利点は、アクター パラダイムによって提供される同時実行モデルを利用できることです。次のようなループになります。

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 に答える