Skip to content

Instantly share code, notes, and snippets.

@fowlmouth
Last active August 29, 2015 14:01
Show Gist options
  • Save fowlmouth/4ed290e7c6b1f9e47a88 to your computer and use it in GitHub Desktop.
Save fowlmouth/4ed290e7c6b1f9e47a88 to your computer and use it in GitHub Desktop.

this is a rewrite of my entity system (fowltek/entitty)

new things since fowltek/entitty

  • messages are declared as a type MSG message_name, this way when you implement a message with def_msg you get an error if the arguments mismatch.
  • much better code is generated
  • PEntity is a reference type. why not, eh?
  • explicit entity setup functions: allocate, initialize, destroy
  • msgImpl will be renamed to defMsg
  • new function: findMessage(ci:PComponentInfo, messageID:int): TMaybe[pointer] for get the raw function for a message

for the future, maybe

  • domains could include a component set and be limited to those components, instead of the component list being global
import entitty, basic2d
type
Pos = object
x,y: float
proc get_pos: TPoint2d {.unicast.}
proc set_pos (p:TPoint2d){.unicast.}
def_msg(Pos, get_pos) do -> TPoint2d:
return point2d(entity[Pos].x, entity[Pos].y)
def_msg(Pos, set_pos) do (p:TPoint2d):
entity[pos].x = p.x
entity[pos].y = p.y
type
Orientation = object
radians: float
proc set_angle* (rad: float) {.unicast.}
def_msg(Orientation, set_angle) do (rad:float):
entity[Orientation].radians = rad
type
Sprite = object
texture: pointer
type Window = object # example
proc draw* (w: Window) {.multicast.}
Sprite.setDestructor do (x:PEntity):
echo "sprite destructor called for ent ", x.id
def_msg(Sprite, draw) do (w:Window):
#w.draw_texture x[Sprite].texture
echo "WOW! I'm being drawn! ent id = ", entity.id
dump_components
dump_messages
let dom = newDomain()
let ty = dom.getTypeinfo(Pos,Orientation,Sprite)
let ent = ty.instantiate
ent.id = 99
ent.set_pos point2d(10,10)
ent.set_angle deg180
ent[pos] = pos(x:0,y:0)
echo "raw entity pos: ", ent[pos].x.int, ", ", ent[pos].y.int
ent.draw(window())
echo msg_is_unicast("draw")
echo message_id("draw")
ent.destroy
## entitty tests
import entitty
## first define some basic components
import basic2d
type
Position = object
p: TPoint2d
proc multicast_test (components: var seq[int]) {.multicast.}
# each component will add its id to components
defMsg(Position, multicast_test) do (components: var seq[int]):
components.add(componentID(Position))
type
Velocity = object
v: TVector2d
proc get_vel* : TVector2d {.unicast.}
proc set_vel* (v:TVector2d){.unicast.}
defMsg(Velocity, get_vel) do -> TVector2d:
entity[velocity].v
defMsg(Velocity, set_vel) do (v:TVector2d):
entity[velocity].v = v
## safety tests
## components A, B, C
## B requires A
## C conflicts with B
type
X_A = object
X_B = object
X_C = object
X_B.requiresComponent(X_A)
X_C.conflictsWithComponent(X_B)
var ty1 = newTypeinfo(componentIDs(X_B, X_C))
assert ty1.is_valid == false
var ty2 = newTypeinfo(componentIDs(X_B))
assert ty2.is_valid == false
proc does_nothing* [T] (some: T): void = discard
template voidExpr* (x): expr = not compiles(does_nothing(x))
# hack to determine if an expression is void
template test (suite; body:stmt):stmt {.immediate.} =
when not definedInScope(test_failures):
var test_failures: seq[string] = @[]
template testFailed(expression): stmt =
when defined(ContinueRunningOnFailure) and ContinueRunningOnFailure:
test_failures.safeadd astToStr(expression)
else:
echo suite, " [Failed] ", astToStr(expression)
break
template check (expression): stmt =
if not expression:
testFailed(expression)
template shouldRaise (expression; exception): stmt =
try:
when voidExpr(expression):
expression
else:
discard expression
testFailed(expression.shouldRaise(exception)) # repeat the condition here so testFailed gets a nice expression
except:
check( getCurrentException() of exception )
block:
test_failures.setLen 0
body
if test_failures.len > 0:
echo suite, " [Failed (", test_failures.len, ")]"
for f in test_failures:
echo " ", f
else:
echo suite, " [Passed]"
test "instantiation test":
const ContinueRunningOnFailure = true
ty1.instantiate.shouldRaise EBadEntity
ty2.instantiate.shouldRaise EBadEntity
try:
discard ty1.instantiate
except:
let x = getCurrentException().RBadEntity
for error in x.errors.val:
echo error
let dom = newDomain()
block:
var ent1 = dom.newEntity(Position)
var comps: seq[int] = @[]
ent1.multicast_test(comps)
assert comps == componentIDs(Position)
import
tables, macros, strutils, algorithm, typetraits, sequtils,
fowltek/maybe_t
export typetraits.name, tables
type
TEntDestructor* = proc(entity:PEntity){.nimcall.}
PTypeinfo* = ref object
## an aggregate of components
size: int
offsets: seq[int]
messages: seq[RT_Message]
initializers*, destructors*: seq[TEntDestructor]
errors*: TMaybe[seq[string]]
when false:
components: seq[TMaybe[tuple[offset: int]]]
else:
components: seq[bool]
TMessageKind* = enum mNope = 0, mUnicast, mMulticast
RT_Message = object
case kind*: TMessageKind
of mNope:
nil
of mUnicast:
f*: pointer
of mMulticast:
fs*: seq[pointer]
PEntity* = ref TEntity
TEntity* = object of TObject
data*: cstring
ty*: PTypeinfo
id*: int
proc `$`* (m:RTMessage):string =
result = "("
result.add($ m.kind)
case m.kind
of mUnicast:
result.add " set="
result.add($ not m.f.isNil)
of mMulticast:
result.add " len="
result.add($ m.fs.len)
else:
discard
result.add")"
type
# compile time message information
TMessage = tuple
id: int
multicast: bool
params: PNimrodNode
var
## compiletime message info
messageIDs* {.compileTime.} = initTable[string,TMessage]()
m_id_counter {.compileTime.} = 0
## runtime message info
rt_msgs_multicast* : seq[bool] = @[]
template dump_messages* : stmt {.immediate.} =
# dump message info at compile time (it doesnt exist at runtime)
static:
echo m_id_counter, " messages:"
for name,m in pairs(messageIDs):
echo " #",m.id, " ",name, " ", (if m.multicast: "multicast" else: "unicast"), " ", repr(m.params)
proc ensureLen* [T] (S:var seq[T]; L:int) =
if s.len < L:
s.setLen L
proc collect_argnames (formal_params: PNimrodNode): seq[string] {.compiletime.} =
assert formal_params.kind == nnkFormalParams
result = @[]
for i in 1 .. <len(formal_params):
for ii in 0 .. len(formal_params[i])-3:
result.add($ formal_params[i][ii].ident)
template voidExpr* (x): expr = not compiles(type(x))
# hack to determine if an expression is void
macro multicast* (func): stmt =
let f = func.body.copyNimTree
f.params.insert(1, newIdentDefs("entity".ident, "PEntity".ident))
let name = ($basename(f.name).ident).normalize
if messageIDs.hasKey(name):
echo "entity failure ", name, " is already declared! new params: ($#) old params: ($#)".format(
f.params.repr, messageIDs[name].params.repr)
let m_ty = "MSG_"&name
let m_id = m_id_counter
m_id_counter += 1
echo "New multicast message #", $m_id, ": ", name, " ", repr(f.params)
var msg: TMessage
msg.id = m_id
msg.multicast = true
msg.params = f.params
messageIDs[name] = msg
let arg_names = collect_argnames(f.params)
echo arg_names
#result body should be something like
# if entity.ty.messages[m_id].kind == mMulticast:
# for f in entity.ty.messages[m_id].fs.items:
# cast[MSG_name]( f )( arguments... )
let xp = parseStmt("""
if entity.ty.messages[$1].kind == mMulticast:
for f in entity.ty.messages[$1].fs.items:
cast[$2](f)($3)
""".format( m_id, m_ty, arg_names.join(",") ))
f.body = newStmtList(xp)
result = newStmtList()
result.add newNimNode(nnkTypeSection).add(
newNimNode(nnkTypeDef).add(
ident(m_ty).postfix("*"),
newEmptyNode(),
newNimNode(nnkProcTy).add(f.params, newNimNode(nnkPragma).add(ident"nimcall"))))
result.add f
result.add parseExpr("rt_msgs_multicast.ensureLen("& $(1+m_id) &")")
result.add parseExpr("rt_msgs_multicast["& $m_id & "] = true")
result.repr.echo
macro unicast* (func): stmt =
let f = func.body.copyNimTree
f.params.insert(1, newIdentDefs("entity".ident, "PEntity".ident))
let name = ($basename(f.name).ident).normalize
if messageIDs.hasKey(name):
echo "entitty failure: ", name, " is being redeclared! new params: (", f.params.repr, ") old params: (", messageIDs[name].params.repr, ")"#, instantiation_info()
quit 1
var arg_names : seq[string] = @[]
for idx in 1 .. <len(f.params):
for ii in 0 .. len(f.params[idx])-3:
arg_names.add($ f.params[idx][ii])
#let is_void = f.params[0].kind == nnkEmpty or (f.params[0].kind == nnkIdent and ($f.params[0]).tolower == "void")
let m_id = m_id_counter
m_id_counter += 1
echo name, ": id=", m_id, " (", repr(f.params),")"
#if messageIDS.isnil:
# echo "ITS NIL"
# quit 1
var msg: TMessage
msg.id = m_id
msg.multicast = false
msg.params = f.params
messageIDs[name] = msg
discard """ if messageParams.len < m_id+1:
messageParams.setLen m_id+1
messageParams[m_id] = f.params
"""
let body = newStmtList()
#the code i need here:
# if entity.ty.messages[m_id].kind == mUnicast:
# cast[MSG_ name](entity.ty.messages[m_id].f)(entity, args...)
let message_body = """
if entity.ty.messages[$1].kind == mUnicast:
when true:
## TODO test
let f = cast[$2](entity.ty.messages[$1].f)
when voidExpr(f($3)): f($3)
else: return f($3)
else:
when voidExpr(cast[$2](entity.ty.messages[$1].f)($3)):
cast[$2](entity.ty.messages[$1].f)($3)
else:
return cast[$2](entity.ty.messages[$1].f)($3)
""".format(
m_id, "MSG_"&name, arg_names.join(",")
)
body.add parseStmt(message_body)
f.body = body
result = newStmtList()
#type MSG_get_pos = proc(entity:PEntity): TPoint2d {.nimcall.}
result.add newNimNode(nnkTypeSection).add(
newNimNode(nnkTypeDef).add(
ident("MSG_"& name).postfix("*"),
newEmptyNode(),
newNimNode(nnkProcTy).add(f.params, newNimNode(nnkPragma).add(ident"nimcall"))))
result.add f
#some message info is needed at runtime
result.add parseExpr("rt_msgs_multicast.ensureLen("& $(1+m_id) &")")
result.add parseExpr("rt_msgs_multicast["& $m_id & "] = false")
echo repr(result)
proc msg_is_multicast* (msg:string):bool{.compileTime.} =
messageIDs[msg.normalize].multicast
proc msg_is_unicast* (msg:string): bool {.compileTime.} =
not messageIDs[msg.normalize].multicast
discard """ proc get_pos : float {.unicast.}
#proc get_pos : string {.unicast.} """
#echo messageID("FOO")
type
TUnicastMsg = tuple[f:pointer, weight:int32]
PComponentInfo* = ref object
id*: int
name*: string
size*: int
initializer*,destructor*: TEntDestructor
unicast_messages: TTable[int, TUnicastMsg]
multicast_messages: TTable[int, pointer]
requiredComponents,conflictingComponents:seq[int]
var
component_id_counter = 0
all_declared_components*{.global.}: seq[PComponentInfo] = @[]
proc componentID* (T:typedesc): int
proc componentInfo* (id: int): PComponentInfo = all_declared_components[id]
proc componentInfo* (T:Typedesc): PComponentInfo = componentInfo(componentID(T))#all_declared_components[componentID(T)]
proc requiresComponent* (a,b: typedesc) =
componentInfo(a).requiredComponents.add(componentID(b))
proc conflictsWithComponent* (a,b: typedesc) =
componentInfo(a).conflictingComponents.add(componentID(b))
proc setInitializer* (comp:typedesc; f:TEntDestructor) =
componentInfo(comp).initializer = f
proc setDestructor* (comp:typedesc; f:TEntDestructor) =
componentInfo(comp).destructor = f
proc new_component_id (t:typedesc): int =
result = component_id_counter
inc component_id_counter
let ci = PComponentInfo(
id: result,
name: name(T),
size: sizeof(T),
unicast_messages: initTable[int, TUnicastMsg](),
multicast_messages: initTable[int,pointer](),
required_components: @[],
conflicting_components: @[]
)
if all_declared_components.len < result+1:
all_declared_components.setLen result+1
all_declared_components[result] = ci
echo ci.name, " declared."
proc componentID* (T:typedesc): int =
let ID {.global.} = new_component_id(T)
return ID
proc messageID* (m:string): int {.compileTime.} = messageIDs[m.normalize].id
macro defMsg* : stmt {.immediate.} =
# Implements a message for a component
#
# Arguments:
# component: typedesc,
# message: static[string],
# weight: int = 0,
# function: proc(arg1: TArg1, ..)
#
# Usage:
# def_msg(THealthComponent, "TakeDamage") do (damage: int):
# # `entity: PEntity` is injected in the params
# entity[THealthComponent].hp -= damage
# if entity[THealthComponent].hp < 0:
# entity.die
#
let cs = callsite()
assert cs.kind == nnkCall
var
component: PNimrodNode
msg: int
weight = 0
function: PNimrodNode
functionIDX = 3
component = cs[1]
let msg_name = ($cs[2]).normalize
msg = messageID(msg_name)
if cs.len > 4:
weight = cs[3].intval.int
functionIDX = 4
if functionIDX != cs.len-1:
echo treerepr(cs)
quit "Malformed arguments for defMsg "& $functionidx & ", "& $cs.len
if cs[functionIDX].kind == nnkStmtList:
function = newProc(procType = nnkLambda, body = cs[functionIDX])
else:
let f = cs[functionIDX].copyNimTree
function = newNimNode(nnkLambda)
f.copyChildrenTo function
# inject self:PEntity
function.params.insert(1, newIdentDefs("entity".ident, "PEntity".ident))
let blck_stmt = newStmtList()
blck_stmt.add parseStmt("let cfunc: `MSG $1` = $2".format(msg_name, repr(function)))
if messageIDs[msg_name].multicast:
blck_stmt.add parseStmt("componentInfo($1).multicast_messages[$2] = cast[pointer](cfunc)".format(
component.repr, msg))
else:
blck_stmt.add parseStmt("componentInfo($1).unicast_messages[$2] = (cast[pointer](cfunc), $3'i32)".format(
component.repr, msg, weight))
result = newBlockStmt(blck_stmt)
when defined(debug):
echo "defMsg($1,$2) result:".format(component.repr, msg_name)
result.repr.echo
template msgImpl*(component, msg_name, func): stmt {.immediate.} =
block:
let cfunc: `MSG msg_name` = func
const msg_id = messageID(astToStr(msg_name))
echo "Declaring (",msg_id," ",astToStr(msg_name), ") for ", componentInfo(component).name
when msg_is_unicast(astToStr(msg_name)):
componentInfo(component).unicast_messages[msg_id] = (cast[pointer](cfunc),0'i32)
else:
componentInfo(component).multicast_messages[msg_id] = cast[pointer](cfunc)
template dump_components* : stmt {.immediate.} =
echo all_declared_components.len, " declared components:"
for comp in all_declared_components:
echo " #", comp.id, ": ", comp.name
type
TComponentSet* = seq[int]
proc componentIDs* (components: varargs[int,`componentID`]): TComponentSet =
@components
proc isValid* (ty:PTypeinfo): bool =
not ty.errors.has
proc newSeq* [T] (size: int; default: T): seq[T] =
newSeq result, size
for i in 0 .. < size:
result[i] = default
proc newTypeinfo* (components: TComponentSet): PTypeinfo =
# aggregates components so an entity can be instantiated.
# each component has initializers, destructors and responds
# to messages, PTypeinfo just collects them together.
# resulting type may not be valid, check with isValid(ty)
result = PTypeinfo()
let n_my_comps = components.len
if n_my_comps == 0:
result.errors = just(@["Entity has 0 components"])
return
result.initializers.newSeq 0
result.destructors.newSeq 0
result.errors.val.newSeq 0
let n_comps = all_declared_components.len
let n_msgs = rt_msgs_multicast.len #m_id_counter
echo "n_msgs: ", n_msgs
result.messages.newSeq n_msgs
var unicast_weights = newSeq[int](n_msgs, -1)
result.offsets.newSeq n_comps
result.components.newSeq n_comps
# calculate offset for component data
var offset = 0
var requiredComps, conflictComps: seq[int]
requiredComps.newseq 0
conflictComps.newseq 0
for c_id in components:
let c = componentInfo(c_id)
result.offsets[c_id] = offset
result.components[c_id] = true
inc offset, c.size
# collect initializers and destructors
if not c.initializer.isNil: result.initializers.add c.initializer
if not c.destructor.isNil: result.destructors.add c.destructor
requiredComps.add c.requiredComponents
conflictComps.add c.conflictingComponents
for id, f in c.unicast_messages.pairs:
template this_message: expr = result.messages[id]
if this_message.kind == mNope or f.weight > unicast_weights[id]:
this_message = RT_Message(kind: mUnicast, f: f.f)
unicast_weights[id] = f.weight
for id, f in c.multicast_messages.pairs:
if result.messages[id].kind == mNope:
result.messages[id] = RT_Message(kind: mMulticast, fs: @[f])
elif result.messages[id].kind == mMulticast:
result.messages[id].fs.add f
else:
echo "multicast id class ", id, " with ", result.messages[id]
# offset ends up being the size we need to allocate for the datas
result.size = offset
requiredComps = distnct(requiredComps)
requiredComps.sort cmp[int]
for c_id in requiredComps:
if not result.components[c_id]:
result.errors.val.add "Component $# is required" % componentInfo(c_id).name
conflictComps = distnct(conflictComps)
conflictComps.sort cmp[int]
for c_id in conflictComps:
if result.components[c_id]:
result.errors.val.add "Component $# is in conflict" % componentInfo(c_id).name
result.errors.has = result.errors.val.len > 0
proc allocate* (ty:PTypeinfo): cstring = cast[cstring](alloc0(ty.size))
proc initialize* (entity:PEntity) =
for f in entity.ty.initializers: f(entity)
proc destroy* (entity:PEntity) =
# run destructors and dealloc entity data
if entity.data.isNil: return
for f in entity.ty.destructors: f(entity)
dealloc(entity.data)
entity.data = nil
type
RBadEntity* = ref EBadEntity
EBadEntity* = object of EBase
errors*: TMaybe[seq[string]]
proc instantiate* (ty:PTypeinfo, initialize = true): PEntity =
# allocates and initializes a typeinfo
if ty.errors.has:
var x = newException(EBadEntity, "typeinfo has errors").RBadEntity
x.errors = just(ty.errors.val)
raise x
let dat = ty.allocate
if dat.isNil:
raise newException(EBadEntity, "failed to allocate memory")
new(result) do (ent:PEntity):
ent.destroy
result.ty = ty
result.data = dat
if initialize:
result.initialize
type
PDomain* = ref object
# Domain is a cache of entity types
types*: TTable[TComponentSet, PTypeinfo]
proc newDomain* : PDomain =
PDomain(types: initTable[TComponentSet,PTypeinfo]())
proc getTypeinfo* (dom:PDomain; components:varargs[int, `componentID`]): PTypeinfo =
var components = @components
components.sort cmp[int]
result = dom.types[components]
if result.isNil:
result = newTypeinfo(components)
dom.types[components] = result
proc newEntity* (dom:PDomain; components:varargs[int,`componentID`], initialize=true): PEntity =
dom.getTypeinfo(components).instantiate(initialize)
proc get* (entity: PEntity; ty: typedesc): var ty {.inline.} =
# access the data for a component
when true:
cast[var ty](entity.data[entity.ty.offsets[componentID(ty)]].addr)
else:
cast[var ty](entity.data[entity.ty.components[componentID(ty)].val.offset].addr)
proc `[]`* (ent:PEntity; ty:typedesc):var ty {.inline.}= ent.get(ty)
# shortcut for `get(ent,ty)`
proc `[]=`* (ent:PEntity; ty:typedesc; val:ty) {.inline.} = ent.get(ty) = val
# shortcut for `get(ent,ty) = val`
## all the testing is done in another module
## this all has to work from outside this module
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment