Problem 2: Identifying Game Objects with Unique IDs

Suppose that we now wish to go further by identifying game objects with a unique ID and outputting this ID when rendering an object. In C++, this could be modelled as follows:  typedef int GoID; class GameObject { GoID _id; public: GameObject (GoID id) : _id(id) {} GoID id () const { return _id; } virtual void render () const = 0; }; class Apple : public GameObject { public: Apple (GoID id) : GameObject(id) {} void render () const { cout << "apple: id " << id() << endl; } }; class Banana : public GameObject { public: Banana (GoID id) : GameObject(id) {} void render () const { cout << "banana: id " << id() << endl; } }; 

Same Data, Different Behaviour

Since the ID attribute is common to all game objects, our old design adapts to the new situation without any major changes. All we need to do is incorporate the new ID attribute into the GameObject data type:  type GoID = Int data GameObject = GameObject { goID :: GoID , render :: IO () } apple goid = GameObject goid $putStrLn ("apple: id " ++ show goid) banana goid = GameObject goid$ putStrLn ("banana: id " ++ show goid)  Adding data common to all game objects is not a problem; we just need to slightly complicate the GameObject data type to reflect the changes. Notice that since apple and banana are functions that build GameObject values, we must supply them with the game object ID that identifies the game object being built.

Problem 3: Updating Game Objects

A static game is not all that fun, so eventually we will want game objects to be updated over the course of time. For this reason, suppose that apples now have a level attribute that is included in their render, and that apples level up on every game tick. This is certainly not very realistic, but is enough to illustrate the next problem. Finally, let bananas have no levels whatsoever. The C++ version of the game could resemble the following:  typedef int GoID; class GameObject { GoID _id; public: GameObject (GoID id) : _id(id) {} GoID id () const { return _id; } virtual void render () const = 0; virtual void update () {} }; class Apple : public GameObject { int level; public: Apple (GoID id) : GameObject(id), level(0) {} void render () const { cout << "apple: id " << id() << ", level " << level << endl; } void update () { level++; } }; class Banana : public GameObject { public: Banana (GoID id) : GameObject(id) {} void render () const { cout << "banana: id " << id() << endl; } };  To reflect the changes in Haskell, we might be tempted to write:  data GameObject = GameObject { level :: Int , goID :: GoID , render :: IO () }  This representation is not entirely accurate, however, because bananas have no levels. Pushing the level attribute all the way up to the GameObject data type would imply that all game objects have a level, which is not the case. How about the following, then:  data GameObject = Apple { level :: Int, ... } | Banana { ... }  This would once again remove the possibility of adding new kinds of game objects. The problem is that while a GameObject is still just something that can render, update, and uniquely identify itself, an apple needs to carry that additional level attribute with it.

Conclusions

Many times, we might feel tempted to think of new entities as subtypes of a more general type. This, however, might not be the best solution to a given problem and does not properly map to languages lacking subtyping. In Haskell, we can use plain functions to encode these new entities as closures and to provide a mechanism to build values of that more general type that act as the subtypes we were first trying to define. This effectively dodges any use of subtyping and yields code that is equally extensible. When we are doing functional programming, we have to stop thinking of functions as just a mere means of computation; functions in a functional language will often go all the way and beyond.

Report Article

User Feedback

I have several problems with your article. First of all, the article do not have in my opinion a well defined target. The reader should be already familiar with haskell to understand it, but it isn't particularly useful for an experienced haskeller. Moreover, the article basically explains how to write an object oriented program in haskell. This is not functional programming.. This is not how a program should be designed in haskell. You shouldn't start by assuming you have some class GameObject which some methods and some subclasses like Apple and Banana. This is OOD!

Share on other sites

To be fair, the approach presented here is more or less "Reactive"; I would recommend people to read the various papers on Functional Reactive Programming to get a feel for it; the Space Invaders coded with Yampa Arcade is a good example I think : http://haskell.cs.yale.edu/wp-content/uploads/2011/01/yampa-arcade.pdf

Share on other sites

I don't think it is very reactive. There are in fact some similarities, but the article mostly describes how to implement some kind of fake object oriented hierarchy in haskell. It does not describe how to implement game mechanics or object behaviors which is what (I think) FRP is all about. Describing some data (with associated functions) is not difficult in haskell. There are actually often several different ways of doing it. The difficult part is IMO making the various parts work together in an efficient way. How often do you have a set of completely independent objects without any interaction? What changes are needed to update your apples based, for example, on the number of other apples at the same level? What if you want to add user interaction? These are the reasons I think this article is not particularly useful.

Share on other sites

I'm no fluent haskeller yet, but I would have wrote the banana function with a let construct instead of the recursive call to self:

banana goid =
let b = GameObject
{ goID   = goid
, render = printf "banana: id %d\n" goid
, update = b }
in b


for the first updatable instance, and for the second:

banana goid =
let b = GameObject
{ goID   = goid
, render = printf "banana: id %d\n" goid
, update = const \$ b }
in b


For what I understood (why recursive let make space efficient, Tying the Knot), this gives a truly "identical banana" each time update is called instead of a chunk that yields a similar banana.

Share on other sites

Thank you, this is good approach.

There is an error, you missed a goID field:

data Apple = Apple
{ level :: Level
, elapsed :: Float }

because it was used here:

updateApple (Apple goid level elapsed) dt = ...

Create an account

Register a new account

• 11
• 13
• 86
• 11
• 10
×

Important Information

We are the game development community.

Whether you are an indie, hobbyist, AAA developer, or just trying to learn, GameDev.net is the place for you to learn, share, and connect with the games industry. Learn more About Us or sign up!

Sign me up!