Alright, here's a usage example. A guy with a spear versus a dragon,
and a simulation built up around that.
Untested and full of nonexistent functions:
+++++++++++++++++++++++++++++++++++++++++++++++++
-- Object-object hit test, two parameters. By default, objects don't collide.
local Hit = class.New("Multimethod", 2)
Hit:Define(function()
return false
end, nil, nil) -- nil: accept anything
-- Post-hit response, ditto. No-op by default.
local Response = class.New("Multimethod", 2)
Response:Define(function() end) -- omitting the nils
-- Hit test for a thrown spear and any kind of sphere (I dunno, floating rocks or something). If there's
-- an intersection, it also returns an information packet.
Hit:Define(function(sphere, spear)
-- Sphere-ray intersection at time t: (c - (p + t * v)) . (c - (p + t * v)) = r^2
local diff = sphere.c - spear.p
local t = SolveQuadratic(spear.v * spear.v, -2 * diff * spear.v, diff * diff - sphere.r * sphere.r)
if t then
local point = spear.p + t * spear.v
return true, { p = point, n = Normalize(point - sphere.c), t = t }
end
return false
end, "Sphere", "Spear") -- if not nil, name or unique id used to instantiate class, returned by Type (see below)
-- Response to the above. Note that since the multimethod only knows about two parameters, "how",
-- the collision info from the hit test, will be ignored when deciding the method dispatch.
Response:Define(function(sphere, spear, how)
spear:BounceOff(how.p, how.n, sphere:SurfaceProperties())
end, "Sphere", "Spear")
-- Bubbles? In this case, "SquishySphere" is derived from "Sphere", so this gets called instead when
-- our spear meets the squishier sort of sphere.
Response:Define(function(Sphere, Spear, how)
PlaySound("squish")
Spear:GetStuck(how.p, how.n)
end, "SquishySphere", "Spear")
-- If anything else ran into a sphere (normal, squishy, other), blow it up!
Response:Define(function(Sphere, OtherThing)
Sphere:Explode()
end, "Sphere", nil)
-- Hit test against a composite thing, in this case a dragon. If it hits a part, include
-- that part along with the usual collision info.
Hit:Define(function(dragon, spear)
for part in dragon:GetParts() do
local bHit, arg = Hit(part, spear)
if bHit then
return true, { part = part, info = arg }
end
end
end, "Dragon", "Spear")
-- Another composite case, in this case with one of the built-in types as a parameter.
Hit:Define(function(terrain_things, spear)
for _, thing in ipairs(terrain_things) do
local bHit, arg = Hit(thing, spear)
if bHit then
return true, arg
end
end
return false
end, "table", "Spear")
-- The spear struck a dragon.
Response:Define(function(dragon, spear, how)
local name = how.part:GetName()
-- Spear to the head: dragon flinches and takes damage.
if name == "head" then
how.part:KnockBack(spear.v)
dragon:Hurt(spear:GetPower())
-- Cripple the wing if it's not already in tatters.
elseif name == "wing" then
if how.part:IsIntact() then
how.part:Maim()
end
-- Thick scales everywhere else.
else
spear:BounceOff(how.info.p, how.info.n, how.part:SurfaceProperties())
end
end, "Dragon", "Spear")
-- If the spear hits something else, it just drops dead.
Response:Define(function(object, spear)
PlaySound("clank")
spear.v = Vec3(0, 0, 0)
end, nil, "Spear")
--
-- etc. etc. etc. All kinds of other interactions
--
-- Updates objects, and updates any that run into each other. Owing to laziness, objects are assumed
-- to be ordered so that the arguments are always passed in the order listed in the above signatures.
function RunDynamics (objects, dt)
UpdateObjects(objects, dt)
for i = 1, #objects do
for j = i + 1, #objects do
local bHit, how = Hit(objects[i], objects[j])
if bHit then
Response(objects[i], objects[j], how)
end
end
end
end
+++++++++++++++++++++++++++++++++++++++++++++++++
Comments on the implementation itself:
callops.IsCallable is just this:
int IsCallable (lua_State * L)
{
lua_pushboolean(L, lua_isfunction(L, 1) || luaL_getmetafield(L, index, "__call") != 0);
return 1;
}
Right now, this only supports single inheritance, so class.Supers is actually only returning one value here... I'll probably generalize that
eventually with an iterator up the hierarchy linearization. The three class functions just have to behave like these:
function IsType (item, what)
assert(type ~= nil)
local itype = item ~= nil and WeakTableOfClassInstances[item] or type(item)
repeat
if itype == what then
return true
end
itype = Superclasses[itype]
until itype == nil
return false
end
function Supers (type)
assert(type ~= nil)
return Superclasses[type]
end
function Type (item)
if item ~= nil then
local itype = WeakTableOfClassInstances[item]
-- Is an instance.
if itype ~= nil then
return itype, true
end
end
-- Not an instance.
return type(item), false
end