3

I am currently working on a game / simulation of a computer like logistics system (like the minecraft mod applied energestics).

The main part of the game is the 2d grid of blocks.

All the blocks have common properties like a position.

But then there are supposed to be different kinds of Blocks like:

  • Item-Containers,
  • Input and Export Busses,
  • etc.

In an imperative object-oriented language (like Java) I would implement this with:

  • a main block class
    • with the common properties like position
  • then have sub classes
    • that inherit from the block class
    • These subclasses would implement the different properties of the different block types.

In ocaml I am a litle bit lost.

I could create objects which inherit but this does not work Like in Java.

For example:

  • I cant put objects of different subclasses in one list together.

I also wanted to approach the data structure differently by separating data from logic. I would not add methods to the objects. I tried using records instead of objects.

I don't know how to implement the different block types.

I tried using custom data types like this:

type blockType = Container | Input | Output | Air
type block = {blockType :blockType; pos :int * int}

I struggled to add the individual additional properties. I tried to add an entity field to the block record type which would hold the additional properties:

type entity = Container of inventory | OutputEntity of facing | InputEntity of facing | NoEntity

(Where inventory and facing are also custom types)

This solution doesn't really feel fitting.

One problem I have is that there are logic operations I want to perform on blocks of type Input and Output. I have to repeat code like this:

let rotateBlock block =
  match block.blockType with
  | Input -> {block with entity = nextDirection block.entity}
  | Output -> {block with entity = nextDirection block.entity}
  |  _ -> block

That's not that bad with two types but I plan to add much more so it is a big negative in terms of scalability.

Another point of critism of this kind of structure is that it is a litle bit inconsistent. I use a field in the record to implement the different types on the block level and multiple constructors on the entity level. I did it to be able to access the positon of every block easily with block.pos instead of using pattern matching.

I am not really happy with this solution.

Request

I hope somebody can point me in the right direction regarding the data structure.

jwpfox
  • 5,124
  • 11
  • 45
  • 42

3 Answers3

5

You're trying to satisfy competing goals. You can't have both, a rigid static model of blocks and a dynamic extensible block type. So you need to choose. Fortunately, OCaml provides solutions for both, and even for something between, but as always for the middle-ground solutions, they kind of bad in both. So let's try.

Rigid static hierarchy using ADT

We can use sum types to represent a static hierarchy of objects. In this case, it would be easy for us to add new methods, but hard to add new types of objects. As the base type, we will use a polymorphic record, that is parametrized with a concrete block type (the concrete block type could be polymorphic itself, and this will allow us to build the third layer of hierarchy and so on).

type pos = {x : int; y : int}
type 'a block = {pos : pos; info = 'a}
type block_info = Container of container | Input of facing | Air | Solid

where info is an additional concrete block specific payload, i.e., a value of type block_info. This solution allows us to write polymorphic functions that accept different blocks, e.g.,

let distance b1 b2 = 
  sqrt ((float (b1.x - b2.x))**2. + (float (b1.y - b2.y)) **2.)

The distance function has type 'a blk -> 'b blk -> float and will compute the distance between two blocks of any type.

This solution has several drawbacks:

  1. Hard to extend. Adding a new kind of block is hard, you basically need to design beforehand what blocks do you need and hope that you do not need to add a new block in the future. It looks like that you're expecting that you will need to add new block types, so this solution might not suit you. However, I believe that you will actually need a very small number of block kinds if you will treat each block as a syntactical element of the world grammar, you will soon notice that the minimal set of block kinds is quite small. Especially, if you will make your blocks recursive, i.e., if you will allow block composition (i.e., mixtures of different blocks in the same block).

  2. You can't put blocks of different kinds in the same container. Because to do this, you need to forget the type of block. If you will do this, you will eventually end up with a container of positions. We will try to alleviate this in our middle-ground solution by using existential types.

  3. Your type model doesn't impose right constraints. The world constraint is that the world is composed of blocks, and each coordinate either has a block or doesn't have one (i.e., it is the void). In your case, two blocks may have the same coordinates.

Not so rigid hierarchy using GADT

We may relax few restrictions of the previous solution, by using existential GADT. The idea of the existential is that you can forget the kind of a block, and later recover it. This is essentially the same as a variant type (or dynamic type in C#). With existentials, you can have an infinite amount of block kinds and even put them all in the same container. Essentially, an existential is defined as a GADT that forgets its type, e.g., the first approximation

type block = Block : block_info -> {pos : pos; info : block_info}

So now we have a unified block type, that is locally quantified with the type of block payload. You may even move further, and make the block_info type extensible, e.g.,

type block_info = ..
type block_info += Air

Instead of building an existential representation by yourself (it's a nice exercise in GADT), you may opt to use some existing libraries. Search for "universal values" or "universals" in the OPAM repositories, there are a couple of solutions.

This solution is more dynamic and allows us to store values of the same type in the same container. The hierarchy is extensible. This comes with a price of course, as now we can't have one single point of definition for a particular method, in fact, method definitions would be scattered around your program (kind of close to the Common Lisp CLOS model). However, this is an expected price for an extensible dynamic system. Also, we lose the static property of our model, so we will use lots of wildcard in pattern matching, and we can't rely on the type system to check that we covered all possible combinations. And the main problem is still there our model is not right.

Not so rigid structure with OO

OCaml has the Object Oriented Layer (hence the name) so you can build classical OO hierarchies. E.g.,

class block x y = object
   val x = x
   val y = y 
   method x = x
   method y = y 
   method with_x x = {< x = x >}
   method with_y y = {< y = y >}
end

class input_block facing = object
   inherit block 
   val facing = facing
   method facing = facing
   method with_facing f = {< facing = f >}
end

This solution is essentially close to the first solution, except that your hierarchy is now extensible at the price that the set of methods is now fixed. And although you can put different blocks in the same container by forgetting a concrete type of a block using upcasting, this won't make much sense since OCaml doesn't have the down-cast operator, so you will end up with a container of coordinates. And we still have the same problem - our model is not right.

Dynamic world structure using Flyweights

This solution kills two bunnies at the same time (and I believe that this should be the way how it is implemented in Minecraft). Let's start with the second problem. If you will represent every item in your world with a concrete record that has all attributes of that item you will end up with lots of duplicates and extreme memory consumption. That's why in real-world applications a pattern called Flyweight is used. So, if you think about scalability you will still end up in using this approach. The idea of the Flyweight pattern is that your objects share attributes by using finite mapping, and objects itself are represented as identifiers, e.g.,

type block = int
type world = {
  map : pos Int.Map.t;
  facing : facing Int.Map.t;
  air : Int.Set.t;
}

where 'a Int.Map.t is a mapping from int to 'a, and Int.Set.t is a set of integers (I'm using the Core library here).

In fact, you may even decide that you don't need a closed world type, and just have a bunch of finite mappings, where each particular module adds and maintains its own set of mappings. You can use abstract types to store this mapping in a central repository.

You may also consider the following representation of the block type, instead of one integer, you may use two integers. The first integer denotes an identity of a block and the second denotes its equality.

type block = {id : int; eq : int}

The idea is that every block in the game will have a unique id that will distinguish it from other blocks even if they are equal "as two drops of water". And eq will denote structural equality of two blocks, i.e., two blocks with exact same attributes will have the same eq number. This solution is hard to implement if you world structure is not closed though (as in this case the set of attributes is not closed).

The main drawback of this solution is that it is so dynamic that it sorts of leaving the OCaml type system out of work. That's a reasonable penalty, in fact you can't have a dynamic system that is fully verified in static time. (Unless you have a language with dependent types, but this is a completely different story).

To summarize, if I were devising such kind of game, I will use the last solution. Mainly because it scales well to a large number of blocks, thanks to hashconsing (another name for Flyweight). If scalability is not an issue, then I will build a static structure of blocks with different composition operators, e.g.,

type block = 
  | Regular of regular
  | ...  
  | Compose of compose_kind * block * block

type compose_kind = Horizontal | Vertical | Inplace

And now the world is just a block. This solution is pure mathematical though, and doesn't really scale to larger worlds.

ivg
  • 34,431
  • 2
  • 35
  • 63
  • wow! I really didnt expect answers this detailed! ! But i am afraid all this is a litle overkill for my project. i guess my use of the word "scalability" was a little missleading.Thanks anyway! – Harold Fincher Dec 29 '17 at 15:29
2

Sounds like fun.

I cant put objects of different subclasses in one list together.

You can actually. Suppose you had lots of different block-objects that all had a 'decay' method. You could have a function "get me the decayables" and it could put all those blocks in a list for you, and you could then at timed intervals iterate over the list and apply the decay method on each of those blocks. This is all well typed and easy to do with OCaml's object system. What you can't do is take out a 'decayable' from that list and say, actually, this was always also an AirBlock, and I want to treat it like a full-fledged AirBlock now, instead of a decayable.

...

type blockType = Container | Input | Output | Air

You can only have 240 so variants per type. If you plan on having more blocks than this, an easy way to gain extra space would be to categorize your blocks and work with e.g. Solid Rock | Liquid Lava rather than Rock | Lava.

type block = {blockType :blockType; pos :int * int}

What's the position of a block in your inventory? What's the position of a block that's been mined out of its place in the world, and is now sort of sitting on the ground, waiting to be picked up? Why not keep the positions in the array indices or map key that you're using to represent the locations of blocks in the world? Otherwise you also have to consider what it means for blocks to have the same position, or impossible positions.

let rotateBlock block =
  match block.blockType with
  | Input -> {block with entity = nextDirection block.entity}
  | Output -> {block with entity = nextDirection block.entity}
  |  _ -> block

I don't really follow this input/output stuff, but it seems that in this function you're interested in some kind of property like "has a next direction to face, if rotated". Why not name that property and make it what you match on?

type block = {
  id : blockType;
  burnable : bool;
  consumable : bool;
  wearable : bodypart option;  (* None - not wearable *)
  hitpoints : int option;      (* None - not destructible *)
  oriented : direction option; (* None - doesn't have distinct faces *)
}

let rotateBlock block =
  match block.oriented with
  | None -> block
  | Some dir -> {block with oriented = Some (nextDirection dir)}

let burn block =
  match block.burnable, block.hitpoints with
  | false, _ | true, None     -> block
  | true, Some hp when hp > 5 -> { block with hitpoints = Some (hp - 5) }
  | true, Some hp             -> ash
Julian Fondren
  • 5,459
  • 17
  • 29
  • Thanks for your extensive answer! I heard about that possibilty with objects but i dont quite understand how i could use it. How would i store the different kinds of blocks in the first place before i for example "have it get all the decayables"? – Harold Fincher Dec 27 '17 at 12:36
  • And regarding your proposal of named properties for stuff like "oriented": would this mean that every Block no matter the type would have all this fields? For basic stuff like "oriented" i guess thats ok but what if some blocks are supposed to have quite complexe functionality which require multiple specific fields like an import bus would have a the id of a system which it is connected to, a Whitelist for what to import etc. If i understood correctly you suggest to make all these option fields, but still let every block have them right? Wouldnt it make sence to some how "sub-categorize" them? – Harold Fincher Dec 27 '17 at 12:42
  • Yes you would need every block to have those fields, unless you combined with that with the categorization idea: `type solidattrs = { burnable : bool; consumable : bool; ... } type blocks = Solid of solidattrs | Liquid of liquidattrs | ...` – Julian Fondren Dec 27 '17 at 19:58
0

The block type is interesting because each kind will have different operations.

Item-Containers, Input and Export Busses, etc.

type container = { specificinfo : int ; etc ...}
type bus .....

type block = 
    | Item of  position * container    
    | Bus of position * bus

type inventory = block list

My intuition say me that you can perhaps use GADT for creating the type of the operations on block and implement an evaluator easily for the simulator.

UPDATE to answer to your comment :

If you have a common information to all your variants, you need to extract them, you can imagine something like :

type block_info = 
    | Item of specific_item_type ....
    | Bus of specific_bus_type

type block = {position:Vector.t ; information : block_info}

let get_block_position b = b.position
Aldrik
  • 136
  • 6
  • 1
    Thanks for the answer! I actually tried this kind of approach but what if i want to apply a function to all the blocks, no matter the type, just using for example the positon. How would i get the position without matching every type? – Harold Fincher Dec 27 '17 at 12:22