Reputation: 115
I want to program a game and would like to use a component pattern for multiple entities.
In a language with interfaces / type-classes / multiple inheritance there would be no problem.
I want some entities to be updateable but not renderable and some shall be both.
Haskell:
class Updateable a where
update :: Float -> a -> a
class Renderable a where
render :: a -> Picture
class InputHandler a where
handleInput :: Event -> a -> a
I can create a list of things that can be updated.
updateAll :: Updateable a => Float -> [a] -> [a]
updateAll delta objs = map (update delta) objs
In Java/D/... this could be implemented via Interfaces
interface Updateable {
void update(float delta);
}
// somewhere in a method
List<Updateable> objs = ...;
for (Updateable o : objs) {
o.update(delta);
}
Now I am wondering how this can be implemented in nim with multimethods.
Can the existence of a fitting multimethod be expressed in a type?
var objs: seq[???] = @[]
Upvotes: 4
Views: 2638
Reputation: 9859
Swift has the same problem and there they use Type Erasure, which is the same as proposed in the previous comments but a bit more strutured. The general pattern in Nim is like this:
#-------------------------------------------------------------
# types
#-------------------------------------------------------------
type C = concept type C
proc name(x: C, msg: string): string
type AnyC = object
name: proc(msg: string): string # doesn't contain C
type A = object
type B = object
#-------------------------------------------------------------
# procs
#-------------------------------------------------------------
proc name(x: A, msg: string): string = "A" & msg
proc name(x: B, msg: string): string = "B" & msg
proc name(x: AnyC, msg: string): string = x.name(msg) # AnyC implements C
proc to_any(x: A): AnyC = AnyC(
name: proc (msg: string): string = name(x, msg) # x captured by proc
)
proc to_any(x: B): AnyC = AnyC(
name: proc (msg: string): string = name(x, msg) # x captured by proc
)
# actually use C
proc print_name(x: C, msg: string) = echo x.name(msg)
#-------------------------------------------------------------
# main
#-------------------------------------------------------------
let a = A()
let b = B()
let cs = [a.to_any(), b.to_any()] # the main goal of most erasure cases
for c in cs:
c.print_name(" erased") # e.g. "A erased"
In this example AnyC
implements C
, A
and B
also implement C
but more importantly can be converted to AnyC
. The Any*
types usually contain closures to effectively erase the type and also implement the concept
itself by trivial forwarding the arguments.
I wish there was a macro or something that would implement Any*
and to_any
automatically.
Upvotes: 0
Reputation: 3144
I'm not sure if this answers your question, but it's worth mentioning.
If you were to store you game objects in separate lists based on type, you could still write a lot of generic logic. Storing objects by type has better better performance because of read-ahead and branch prediction. See this lecture, from a guy who should know what he's talking about: Multiprocessor Game Loops: Lessons from Uncharted 2: Among Thieves.
For instance, if you have defined a texture
proc for some of your object types, then you can write a generic draw(t: T) = magicRenderToScreen(texture(t))
proc that will work for all of them. This is also useful if you are implementing resource pools, or any kind of general behaviour really.
You do have to include each affected object type in the render and update loops somehow, but that's usually not a big deal in practice. You can even use a simple macro to make this less verbose, so your render loop simply contains something like renderAll(players, enemies, sprites, tiles)
Generic lists are not straightforward in compiled languages, and nim forces you to see it, which is kind of good when you're working on a game. To have generic lists you typically either have to use pointers and dynamic dispatch, or some kind of union type. I seem to remember that nim used to be able to dispatch to the correct multi-methods from parent object ref's, (which would enable lists to contain several types and dispatch dynamically at runtime) but I'm honestly not sure if that can still be done...?
Someone more knowledgeable please let us know!
Upvotes: 4
Reputation: 7681
The lack of an explicit interface
keyword is common question in the Nim community. Taking Araq's answer and applying it to a hypothetical case based on your Java/D snippet we could write something like this:
import strutils # For formatFloat
type
IUpdateable =
tuple[
update: proc(v: float) {.closure.},
show: proc(): string {.closure.}
]
Rounded = ref object
internalValue: float
Real = ref object
a_real_value: float
# Here goes our rounded type.
proc `$`(x: Rounded): string =
result = "Rounded{" & $int(x.internalValue) & "}"
proc updateRounded(x: Rounded, delta: float) =
x.internalValue += delta
proc getUpdateable(x: Rounded): IUpdateable =
result = (
update: proc(v: float) = x.updateRounded(v),
show: proc(): string = `$`(x)
)
converter toIUpdateable(x: Rounded): IUpdateable =
result = x.getUpdateable
# Here goes our Real type.
proc `$`(x: Real): string =
result = "Real{" &
x.a_real_value.format_float(precision = 3) & "}"
proc update_real(x: Real, delta: float) =
x.a_real_value += delta
proc getUpdateable(x: Real): IUpdateable =
result = (
update: proc(v: float) = x.update_real(v),
show: proc(): string = `$`(x)
)
# Here goes the usage
proc main() =
var objs: seq[IUpdateable] = @[]
var a = Rounded()
var b = Real()
a.internalValue = 3.5
b.a_real_value = 3.5
objs.add(a) # works because of toIUpdateable()
objs.add(b.getUpdateable)
for obj in objs:
echo "Going through one loop iteration"
echo "\t", obj.show()
obj.update(0.4)
echo "\t", obj.show()
obj.update(0.4)
echo "\t", obj.show()
main()
# -> Going through one loop iteration
# -> Rounded{3}
# -> Rounded{3}
# -> Rounded{4}
# -> Going through one loop iteration
# -> Real{3.50}
# -> Real{3.90}
# -> Real{4.30}
However, as you can read in that forum thread, depending on what exactly you need interfaces for other approaches may be better. Also, presumably the future way to go are concepts, but as usual the manual is dry and the related unit tests are cryptic so I couldn't manage to translate the previous tuple example to concepts.
If you feel like going for concepts you should ask in the forum directly, but beware, as the manual says, concepts are still in development.
Upvotes: 3