9

私はnetwireとOpenGLを使ってHaskellでリアルタイムゲームを書くつもりです。基本的な考え方は、各オブジェクトがワイヤーで表され、入力としてある程度のデータを取得してその状態を出力し、次にすべてを1つの大きなワイヤーに接続して、GUIの状態を入力として取得するというものです。そして、ワールド状態を出力します。これをレンダラーと、衝突検出などの「グローバル」ロジックに渡すことができます。

よくわからないことの1つは、ワイヤーをどのように入力するかです。すべてのエンティティが同じ入力を持っているわけではありません。プレイヤーはキー入力の状態にアクセスできる唯一のエンティティであり、ミサイルを探すにはターゲットの位置が必要です。

  • 1つのアイデアは、すべてに渡されるObjectInputタイプを持つことですが、不要な依存関係を誤って導入する可能性があるため、それは私には悪いようです。
  • 一方、SeekerWire、PlayerWire、EnemyWireなどはほぼ「同一」であるため、それらの間で機能を複製する必要があるため、それらを使用するのが良いかどうかはわかりません。

私は何をすべきか?

4

3 に答える 3

7

抑制モノイドは抑制例外eのタイプです。これはワイヤーが生成するものではありませんが、のとほぼ同じ役割を果たします。つまり、ワイヤをで結合する場合、出力タイプは等しくなければなりません。eEither e a<|>

GUIイベントが入力を介してワイヤに渡され、継続的なキーダウンイベントがあるとします。これをモデル化する1つの方法は、最も簡単です。

keyDown :: (Monad m, Monoid e) => Key -> Wire e m GameState ()

このワイヤは、現在のゲーム状態を入力として受け取り()、キーが押されている場合にを生成します。キーが押されていない間、それは単に禁止します。ほとんどのアプリケーションは、ワイヤが抑制される理由を実際には気にしないため、ほとんどのワイヤはで抑制されmemptyます。

このイベントを表現するためのはるかに便利な方法は、リーダーモナドを使用することです。

keyDown :: (Monoid e) => Key -> Wire e (Reader GameState) a a

このバリアントの本当に便利な点は、ゲームの状態を入力として渡す必要がないことです。代わりに、このワイヤーは、発生した場合はIDワイヤーのように機能し、発生しなかった場合は抑制します。

quitScreen . keyDown Escape <|> mainGame

エスケープキーを押すと、イベントワイヤkeyDown Escapeが一時的に消えます。これは、イベントワイヤがIDワイヤのように機能するためです。したがって、ワイヤ全体quitScreenは、それ自体を阻害しないと想定するように機能します。キーを離すとイベントワイヤーが抑制されるので、コンポジションもquitScreen抑制します。したがって、ワイヤ全体はのように機能しmainGameます。

ワイヤーが見ることができるゲームの状態を制限したい場合は、そのためのワイヤーコンビネーターを簡単に作成できます。

trans :: (forall a. m' a -> m a) -> Wire e m' a b -> Wire e m a b

これにより、以下を適用できますwithReaderT

trans (withReaderT fullGameStateToPartialGameState)
于 2013-02-03T21:55:23.197 に答える
2

Elmには、オートマトン用のライブラリがあります。これは、あなたがしていることと似ていると思います。

何かにアクセスさせたい状態のタイプごとにタイプクラスを使用できます。次に、ゲームの状態全体に対してこれらの各クラスを実装します(すべてを保持する1つの大きなファットオブジェクトがあると仮定します)。

-- bfgo = Big fat game object
class HasUserInput bfgo where
    mouseState :: bfgo -> MouseState
    keyState   :: bfgo -> KeyState

class HasPositionState bfgo where
    positionState :: bfgo -> [Position] -- Use your data structure

次に、データを使用するための関数を作成するときに、それらの関数が使用する型クラスを指定するだけです。

{-#LANGUAGE RankNTypes #-}

data Player i = Player 
    {playerRun :: (HasUserInput i) => (i -> Player i)}

data Projectile i = Projectile
    {projectileRun :: (HasPositionState i) => (i -> Projectile i)}
于 2013-02-03T12:16:40.490 に答える
2

これには非常に単純で一般的な解決策があります。重要なアイデアは、異なるタイプのソースを決してマージしないことです。代わりに、同じタイプのソースのみをマージします。これを機能させる秘訣は、すべての多様なソースの出力を代数的データ型でラップすることです。

私はあまり詳しくないnetwireので、よろしければpipes例として使用します。必要なのは、mergeソースのリストを取得し、それらを1つのソースに結合して、それらの出力を同時にマージし、すべてが完了すると終了する関数です。キータイプの署名は次のとおりです。

merge
 :: (Proxy p)
 => [() -> Producer ProxyFast a IO r] -> () -> Producer p a IO ()

Producerこれは、タイプの値のリストを取得し、それらをタイプの値のa1つに結合することを意味します。興味があり、フォローしたい場合は、の実装を次に示します。Produceramerge

import Control.Concurrent
import Control.Concurrent.Chan
import Control.Monad
import Control.Proxy

fromNChan :: (Proxy p) => Int -> Chan (Maybe a) -> () -> Producer p a IO ()
fromNChan n0 chan () = runIdentityP $ loop n0 where
    loop 0 = return ()
    loop n = do
        ma <- lift $ readChan chan
        case ma of
            Nothing -> loop (n - 1)
            Just a  -> do
                respond a
                loop n

toChan :: (Proxy p) => Chan ma -> () -> Consumer p ma IO r
toChan chan () = runIdentityP $ forever $ do
    ma <- request ()
    lift $ writeChan chan ma

merge
 :: (Proxy p)
 => [() -> Producer ProxyFast a IO r] -> () -> Producer p a IO ()
merge producers () = runIdentityP $ do
    chan <- lift newChan
    lift $ forM_ producers $ \producer -> do
        let producer' () = do
                (producer >-> mapD Just) ()
                respond Nothing
        forkIO $ runProxy $ producer' >-> toChan chan
    fromNChan (length producers) chan ()

ここで、2つの入力ソースがあると想像してみましょう。最初のものは、1秒間隔でから1までの整数を生成します。10

throttle :: (Proxy p) => Int -> () -> Pipe p a a IO r
throttle microseconds () = runIdentityP $ forever $ do
    a <- request ()
    respond a
    lift $ threadDelay microseconds

source1 :: (Proxy p) => () -> Producer p Int IO ()
source1 = enumFromS 1 10 >-> throttle 1000000

String2番目のソースは、ユーザー入力から3秒を読み取ります。

source2 :: (Proxy p) => () -> Producer p String IO ()
source2 = getLineS >-> takeB_ 3

これら2つのソースを結合したいのですが、それらの出力タイプが一致しないため、代数的データ型を定義して、それらの出力を単一のタイプに統合します。

data Merge = UserInput String | AutoInt Int deriving Show

これで、それらの出力を代数的データ型でラップすることにより、それらを同じ型のプロデューサーの単一のリストに組み合わせることができます。

producers :: (Proxy p) => [() -> Producer p Merge IO ()]
producers =
    [ source1 >-> mapD UserInput
    , source2 >-> mapD AutoInt
    ]

そして、私たちはそれを本当に素早くテストすることができます:

>>> runProxy $ merge producers >-> printD
AutoInt 1
Test<Enter>
UserInput "Test"
AutoInt 2
AutoInt 3
AutoInt 4
AutoInt 5
Apple<Enter>
UserInput "Apple"
AutoInt 6
AutoInt 7
AutoInt 8
AutoInt 9
AutoInt 10
Banana<Enter>
UserInput "Banana"
>>>

これで、結合されたソースができました。次に、ゲームエンジンを作成して、そのソースから読み取り、入力のパターンマッチを行い、適切に動作するようにします。

engine :: (Proxy p) => () -> Consumer p Merge IO ()
engine () = runIdentityP loop where
    loop = do
        m <- request ()
        case m of
            AutoInt   n   -> do
                lift $ putStrLn $ "Generate unit wave #" ++ show n
                loop
            UserInput str -> case str of
                "quit" -> return ()
                _      -> loop

試してみよう:

>>> runProxy $ merge producers >-> engine
Generate unit wave #1
Generate unit wave #2
Generate unit wave #3
Test<Enter>
Generate unit wave #4
quit<Enter>
>>>

同じトリックがうまくいくと思いnetwireます。

于 2013-02-03T07:17:49.583 に答える