2016-11-02 5 views
2

私はハスケルを使ってゲームを作っています(これは割り当てなので、私を判断しないでください)が、データ型に関する問題に直面しています。抽象レコードまたはレコードインターフェイス?

私が望むのは、位置、速度、角度と回転速度を持つデータ型エンティティです。この考えでは、レコードは非常にうまく機能します。

data Entity = Entity { 
    location :: Vector, 
    velocity :: Vector, 
    angle :: Float, 
    rotation :: Float 
} 

ここで、エンティティのインスタンス、つまりPlayer Rock PickupとBulletが必要です。しかし、Players RocksとBulletsは余分なフィールド、すなわちhealth :: Intを持たなければならず、Pickupは別のフィールド、すなわちpickupType :: PickupTypeを持っていなければなりません。

しかし、どのエンティティタイプでも作業したい特定の方法があります。例:

move :: Entity -> Entity 
move [email protected](Entity {location, velocity, angle, rotation}) = e {location = location + velocity, angle = angle + rotation} 

これを行う方法やこれが可能であるかどうかはわかりません。私はなぜこれが不可能なのか理解できません。他の言語でも可能です。

いくつかの試みとなぜ彼らは私が望むかなり何ではありません。

試み1:

type Player = Player { 
    e  :: Entity, 
    health :: Int 
} 

これは動作しますが、それは本当に醜いです。これは、たとえば、プレーヤーを移動する方法です。

movePlayer :: Player -> Player 
movePlayer [email protected](e) = p {e = move e} 

これはちょうど実際には醜いです。

陽性: 抽象クラスを簡単に作成できます。 簡単にインスタンスを作成できます。 簡単な抽象メソッド。

ネガティブ: インスタンスのエンティティ実装フィールドを取得または設定することは難しいです。

試み2:

class Entity e where 
    getLocation :: e -> Vector 
    getVelocity :: e -> Vector 
    ... 
    setLocation :: Vector -> e -> e 
    setVelocity :: Vector -> e -> e 
    ... 

data Player = Player { 
    playerLocation :: Vector, 
    playerVelocity :: Vector, 
    ... 
    playerHealth :: Int 
} 

instance Entity Player where 
    getLocation = location 
    getVelocity = velocity 
    ... 
    setLocation l e = e {location = l} 
    setVelocity v e = e {playerVelocity = v} 
    ... 

move :: (Entity e) => e -> e 
move e = (setLocation (getLocation e + getVelocity e) . setAngle (getAngle e + getRotation e)) e 

まあ、それは動作しますが、私たちはそれらの定義は今、本当に醜いしていることすべてに同意することができます願っています。どのエンティティでも動作する抽象メソッドは、邪悪になります。唯一の良いことは、movePlayerのようなメソッドがとても簡単になることです。

movePlayer :: Player -> Player 
movePlayer = move 

私はmoveを使うことができるので、movePlayerをもう定義する必要はありません。

陽性: インスタンスのエンティティ実装フィールドを簡単に取得または設定できます。

ネガティブ: 抽象クラスを作成するのは難しいです。 インスタンスを作成するのがさらに難しくなります。 難解な抽象メソッド。

試行3:

インスタンスに必要なすべてのフィールドをEntityに与えます。

data Entity = Entity { 
    location :: Vector, 
    velocity :: Vector, 
    angle  :: Float, 
    rotation :: Float, 
    health  :: Int, 
    pickupType :: PickupType 
} 

このように、インスタンスを定義する必要はなく、エンティティを使用することもできます。唯一の問題は、余分なデータが大量にあることです。これは現在私が使用しているものであり、IMOは私の問題のための最良の解決策ですが、私はまだそれを好きではありません。

陽性: 抽象クラスを作成するのは簡単ですが、それはもはや抽象クラスではありません。 インスタンスを定義する必要はありません。 簡単な抽象メソッド。 インスタンスのエンティティ実装フィールドを簡単に取得または設定できます。

ネガティブ: 多くの未使用データ。 エンティティを作成するたびに多くのナンセンスフィールドを定義する必要があります。

だから私は非常に単純な理由のために、あなたの最初の試みとなるだろう、私はこれらの3以上良くする方法を見つけることができません:(

+3

あまりにも多くのフィールドが常に不整合な状態を意味するため、最後の方法は本質的に悪いことです。 – ThreeFx

+3

その他:ゲームを書くのにHaskellを使うのに間違ったことはありません:) – duplode

答えて

2

、私を助けてください:

それは正確の意思をキャプチャPlayer - それは追加情報をEntity

data Player = Player { 
    e  :: Entity 
    health :: Int 
} 

を扱うすべての関数が最初に書くのは面倒かもしれないが、あなたはおそらくそれらを再度参照する必要はありません、あなたはabstraを提供するという意味。あなたのコードに十分なインターフェイスがあり、Playerの状態に直接アクセスしないようにしてください。

movePlayer :: Player -> Player 
movePlayer [email protected](e) = p {e = move e} 

この関数は一度書かれ、理想的には内部をもう一度処理する必要はありません。それらが意図されているよう

また、今は型クラスを使用することができます:あなたは別のタイプのクラスで抽象movePlayerなどMovableとして:あまりにもPlayersを移動する

class Movable m where 
    move :: m -> m 

-- Obviously, you can move entities 
instance Movable Entity where 
    move e = -- stuff 

しかし、今では本当に簡単です:

instance Movable Player where 
    move (Player entity health) = Player (move entity) health 
    -- works, since `Entity` is movable 

これはさておき、あなたのタイプのクラスのアプローチは、大きな欠陥を持っている:関数についてPlayerはありますが、0123は何はありませんか?この場合、あなたはPlayer継承Entity、このようにしています:

class Entity e => Player e where 
    -- stuff ... 

をしかし、Haskellの型クラスが開いているので、何も当然のことながら、つまり、しない限り、(動作するようになっていますかではありませんPlayer、になることができますあなたの意図)。

+2

異なるプレイヤータイプがある場合(例えば、ゲームのさまざまなバリエーション、あるいはPC対NPCの場合でも)、それらのクラスは意味をなさすことができます。 – dfeuer

0

合計(別名「ユニオン」)タイプを使用できます。

data Entity = 
    Player { 
     location :: Vector, 
     -- etc. 
     health :: Int } 
    | Pickup { 
     location :: Vector, 
     -- etc. 
     pickupType :: PickupType} 

これを排除するには、合計タイプに変化のあるデータを保持させるだけです。

これは、[エンティティ]を持つことができるという利点があります。エンティティのバリエーションがすべて異なる場合(OO言語とは異なります)は不可能です。

ゲームのモデルによっては、残りのプレイヤー情報から位置と速度のデータを分離することもできます。そのデータは、クアッドツリーのような何らかの空間データ構造でよりよく保持されますか?そうすることで、一定のデータをフレームごとに変化するものから分離しておくことができます。

あなたが見なければならないことは、多くのゲッターとセッターで説明した問題を解決するために存在するlensesです。

+2

サムタイプは素晴らしい解決策ですが、この方法で実装すると、 'health'や' pickupType' partialのようなものを作るという欠点があります。コンストラクタの追加のレイヤーと違ってそれを因数分解するのは少し面倒ですが、より安全です。 – duplode

+0

また、 'Entity'固有のフィールドを取り除くと、追加の' Entity'型を追加するときに、コードの重複が少なくなるという追加のボーナスがあります。 – ThreeFx

0

これは答えよりも長いコメントです。私が書くつもりのコアはThreeFx's answerで十分にカバーされています。

まず、OOPの専門用語を使用することは避けてください。混乱の原因となります。あなたの試行#1では、Entityはデータ型であり、クラスではなく、抽象度でもありません。試行#1のPlayerEntityの関係は、単なる構成の1つに過ぎません。PlayerEntityであり、決してEntityのインスタンスではありません。

第2に、レコード更新構文は、ハスケルではかなり醜いです。です。それは複雑ではなく、ちょっと面倒です。だからあなたが言うとき。 「インスタンスのエンティティ実装フィールドを取得または設定するのが難しい」というのは、実際には難しいことではありません。これは、データ型の設計を決定するのに十分な深刻な問題ではありません。

第3に、レンズは(多くの他のものの中でも)レコード更新構文の醜さを避ける方法です。あなたはおそらくその時点でダイビングしたくないでしょう(少なくとも、あなたが課題を終えるまで待ってください)。しかし、将来のある時点であなたが読めるように、関連性の高いチュートリアルへのリンクを残すことに抵抗することはできません:Program imperatively using Haskell lenses

2

@ ThreeFxの回答と同じ理由で、あなたの最初の試みがやり方だと言います。私は若干異なる選択肢を提案するつもりです。

を考えると、これらのタイプ:

data Player = Player { 
    playerEntity :: Entity, 
    health  :: Int 
} 

data Pickup = Pickup { 
    pickupEntity :: Entity, 
    pickupType :: PickupType 
} 

代わりEntity上で行うことができ、各アクションの別の型クラスを持つのは、我々はそれが簡単にEntityアクションを実行できるようにすること、一般的な高階関数を提供することができますPlayer sおよびPickup S:今

overPlayerEntity :: (Entity -> Entity) -> Player -> Player 
overPlayerEntity fn (Player pe h) = Player (fn pe) h 

overPickupEntity :: (Entity -> Entity) -> Pickup -> Pickup 
overPickupEntity fn (Pickup pe t) = Pickup (fn pe) t 

、我々は持つことができます

両方 Player秒で動作します

move' :: HasEntity a => a -> a 
move' = overEntity move 

class HasEntity a where 
    overEntity :: (Entity -> Entity) -> a -> a 

instance HasEntity Player where overEntity = overPlayerEntity 
instance HasEntity Pickup where overEntity = overPickupEntity 

これは、のようなものを可能にする:

我々はまた、それが簡単だけでなく、一般的なコードを書くことにするタイプのクラスにこれを包むことができますおよびPickupである。これにより、moveのような特殊なバージョンの機能が不要になると同時に、Entityのアクセス定型文を1回書くだけで済みます。

なお、このover...Entityは、@ duplode'sと@Paul Johnsonの答えの最後に記載されている「レンズ」技術に近づいています。これらは基本的に2つの(非常に)特化したレンズです。 HasEntityタイプのクラスを追加すると、「高級レンズ」と呼ばれるものがあります(これはlensライブラリで使用されている用語の一種です)。一般的なレンズの概念が何を意味するのか心配する必要はありませんが、将来的にレンズについて学ぶためのエントリーポイントとなります。

関連する問題