Hi! I have listened to Rich's presentation Simple Made Easy, and he has convinced me of the virtures of simplicity. However, I am not so good at designing simple stuff myself. Writing things down makes me think more clearly, and I was also hoping that you can help me by giving some input and critique. One of the tips he gave was just to think about "what, how, who, when, where and why", and try to break those aspects apart, so that's what I'm gonna try to do.
Let's start with "what". I want to make a role-playing game. But this post is not just for the game I want to make; I want to get better at designing simple stuff in general. Throughout this thread, try to think of the game as just an example. I care more about the general ideas. Anyhow, let's quickly go over some requirements, so that we can come up with abstractions. I want the game to be multiplayer, over a network. A player is a character that can fight monsters, pick up things, use things, gain experience, etc. Non-player character can be friendly or not, fight, talk, etc. There must obviously be a user inferface, with graphics, sound, and an input system. There must be a way to save the players' progress. And, let's not forget, there is a world, an environment, in which the game takes place. Rich said that one should try to come up with abstractions. "Form abstractions from related sets of functions." Ok, let's start with an easy one; there must be two functions (impure, but anyway) that can save and load a character from some storage on the server. So, we have a storage abstractions that can save and load stuff. I think that seems like a good abstraction. A storage is not only an abstractions, but kind of a major subsystem in the game. But let's take an example from Clojure; the seq abstraction. The seq abstraction is a bit different, because it is not a subsystem in the same way; seqs are used everywhere. So, in a similar vein, should there be abstractions for items, like "usable", "breakable", "pickupable"? It sounds nice, because there might be many concrete things that share these capabilities. But how would it work? It is players that pick things up, the items themselves do not do anything when they are being picked up. So maybe pickupable is a useless abstraction. Maybe there should be a container abstraction instead. What about usable? What happens when a player uses an item? All sorts of things could happen; to the player, to the item, or to anything else. The use function has to take the whole world as input. Should it be associated with the item? Maybe. What if the result of the use depends not only on the item but on the user? And maybe several different items should have the same effect/action. Have I complected the action with the item by making the item implement usable, i.e. have its own use function? An alternative would be to have a table describing what happens when different items are used by different users. I guess the important question is: How do I tell if I should be introducing an abstraction like "usable"? By introducing an abstraction I guess I mean by defining a protocol. Also, instead of inventing a container abstraction, I could just put the items (values) in data structures. But those are collections---an abstraction that is essentially a container. Then I have regular values in regular collections, and I have not introduced any abstractions. Maybe that's OK, but I have not really got used to this whole "program to abstractions" thing. Another problem is the user interface, i.e. the graphics and sound. The graphics subsystem should implement an abstraction, because if I wanted to change graphics library or whatever, the rest of the game code should not have to care. Do the grahpics and sound subsystems have something in common? Should they share an abstraction? The difference between them is that everything needs to be drawn/shown all the time, while sounds are played in a more reactive manner, when certain things occur. Rich said that we should not complect "what" with "how". But when I start to think about an abstraction, I start to think about how these subsystems will work. I mean, the answer to "What (and not how) should a graphics subsystem do?" is "Show stuff." But how useful is that answer? There is, for example, a difference between just drawing static images that do not move, and drawing things that move around. Because you can't just draw stuff that move around once. Maybe I should think about the graphics that way, that everything is just drawn once. Then I could, every frame, just call draw and pretend that everything needs to be drawn every frame, instead of be moved in a scene graph, which is how 3d engines usually work. But, I still think about the "how" part. I'm thinking: "Well, if I get as input to the draw function something that is alreay in the scenec graph, then I will just update the position instead of adding the thing to the scene graph. But for that I need some kind of id, to know if the thing is in the scene graph already. There will be id's for all characters in the game, and I can use that." But then I have complected the graphics with the rest of the game. The graphics should ideally not have to care about the id's that the rest of the games uses, because the id does not really have anything to do with the thing's graphical representation. The "how" and the "what" is complected. And not only that, I must take the thing out of the scene graph if it disappears from the game. So, I pretend that I must call "draw" every frame, but still I must call "remove" if the thing disappears. By the way, I have quick question: If I define a protocol for the graphics system with, let's say, just a draw function. Then I must make some sort of type or record that implements the protocol right? I usually think about subsystems as namespaces. So, would I make like a graphics namespace that defines a type GraphicsSubsystem that implements the protocol and that uses the functions in the namespace? And the rest of the game calls functions on the GraphicsSubsystem type? It just feels a little wierd to have to define a type when I have a namespace, in a way. Do you see what I mean? And another quick question: The draw function by itself is not a "set of related functions". Is it somehow wrong to form an abstraction out of just one function? But I have recently been thinking about another design for the whole game that would solve, I hope, the above problems. Every function that does something to the model/state of the game world does not update the world right away, but instead produce a "delta", i.e. a piece of data that describes the change that would have to be made to the current state in order to produce the new state. Now instead of calling draw for all the characters and stuff every frame, what if the graphics subsystem just subscribed to the deltas. (The deltas would of course have to be sent over the network first.) Then the rest of the game would not have to know anything about the graphics subsystem. One great thing about this is that the sound subsystem and networking subsystem could do exactly the same. But what if I decide to change the data model of the game, and therefore the deltas now look a bit different? That would affect the subscribers. But, somehow, they must know about the format of deltas, or some derivatives. The best thing I could think of is to introduce a layer of indirection, so that the new kind of deltas can be translated to the old kind, if possible. Then at least the graphics subsystem would not have to change. Another good thing with deltas is that many functions can become pure by returning deltas instead of modifying the state. They could of course return a new state instead of deltas in order to be pure, but that would not help the subscribers. And lots of different delta-producing functions could be run in parallel. But that could not happen if they returned a new state instead. When Rich talked about "where and when" he said that if subsystems call each other directly, they are complected because they must know where the other one is, and it also implies a temporal connection. He talked about using queues to solve this problem. I have never used queues in that way, but it seems like using deltas and a publish-and-subscribe design is kind of what he was talking about, or is it? Comments, tips and suggestions? I'm all ears. -- You received this message because you are subscribed to the Google Groups "Clojure" group. To post to this group, send email to [email protected] Note that posts from new members are moderated - please be patient with your first post. To unsubscribe from this group, send email to [email protected] For more options, visit this group at http://groups.google.com/group/clojure?hl=en
