Skip to content

Commit

Permalink
switch to non-physical projectile entities
Browse files Browse the repository at this point in the history
  • Loading branch information
fluxionary committed Jan 28, 2024
1 parent b56f74a commit d9f3f2b
Show file tree
Hide file tree
Showing 14 changed files with 269 additions and 423 deletions.
27 changes: 17 additions & 10 deletions API.md
Expand Up @@ -5,6 +5,7 @@ ballistics.register_projectile("mymod:myarrow", {
-- visual parameters, see minetest's lua_api.md for details
is_arrow = true, -- if true, entity will automatically be rotated depending on its velocity
update_period = nil, -- if a positive number, how often to rotate the entity

visual = "mesh",
mesh = "ballistics_arrow.b3d",
visual_size = vector.new(1, 1, 1),
Expand All @@ -21,24 +22,25 @@ ballistics.register_projectile("mymod:myarrow", {
show_on_minimap = false,

-- physical parameters
collisionbox = { -0.2, -0.2, -0.2, 0.2, 0.2, 0.2 },
selectionbox = { -0.2, -0.2, -0.2, 0.2, 0.2, 0.2, rotate = true },
collide_with_objects = true,
collisionbox = { -0.05, -0.05, -0.05, 0.05, 0.05, 0.05 },
selectionbox = { -0.05, -0.05, -0.2, 0.05, 0.05, 0.2, rotate = true },
pointable = true,

-- logical parameters
static_save = false,
hp_max = 1,
immortal = true, -- prevent engine from modifying our HP directly
drag_coefficient = 0.0, -- if > 0, projectile will slow down in air and slow down a lot in water.

-- callbacks
-- no callbacks are mandatory, and some pre-configured behaviors are available for use - see below
on_hit_node = function(self, pos, node, collision_axis, old_velocity, new_velocity) end,
on_hit_object = function(self, object, collision_axis, old_velocity, new_velocity) end,
on_hit_node = function(self, node_pos, node, above_pos, intersection_point, intersection_normal, box_id) end,
on_hit_object = function(self, target, intersection_point, intersection_normal, box_id) end,

on_activate = function(self, staticdata)
-- projectiles are ephemeral (they don't static save), but staticdata can be passed on creation
-- arrows initialize their velocity, acceleration, and some other things before calling this function.
-- staticdata can be passed on creation. it is expected to be a table. internally, the keys "parameters",
-- "velocity" and "acceleration" are used.
-- projectiles will initialize their velocity, acceleration, and some other things before calling this function.
end,

on_step = function(self, dtime, moveresult)
Expand All @@ -48,6 +50,7 @@ ballistics.register_projectile("mymod:myarrow", {
end,

-- these all are as in a standard minetest entity
get_staticdata = function(self) end,
on_attach_child = function(self, child) end,
on_deactivate = function(self, removal) end,
on_death = function(self, killer) end,
Expand Down Expand Up @@ -76,6 +79,10 @@ ballistics.register_projectile("mymod:myarrow", {

the velocity of the projectile at the last server tick

* `self._last_acceleration`

the acceleration of the projectile at the last server tick

* `self._initial_gravity`

acceleration in the y dimension when the projectile was created.
Expand Down Expand Up @@ -113,9 +120,9 @@ ballistics.register_projectile("mymod:myarrow", {
note that you aren't restricted to using a single callback, most of these can easily be used together, e.g.

```lua
on_hit_node = function(self, pos, node, collision_axis, old_velocity, new_velocity)
ballistics.on_hit_node_freeze(self, pos, node, collision_axis, old_velocity, new_velocity)
ballistics.on_hit_node_active_sound_stop(self, pos, node, collision_axis, old_velocity, new_velocity)
on_hit_node = function(...)
ballistics.on_hit_node_freeze(...)
ballistics.on_hit_node_active_sound_stop(...)
end
```

Expand Down
10 changes: 8 additions & 2 deletions README.md
Expand Up @@ -2,7 +2,13 @@

an api for projectiles. see API.md for details.

the goal is to make use of minetest's built-in collision detection to do the heavy lifting, to avoid having to do a
bunch of raycasts, which may not be accurate if there is a lot of lag.
originally, the goal is to make use of minetest's built-in collision detection to do the heavy lifting, to avoid having
to do a bunch of raycasts, which may not be accurate if there is a lot of lag. however there's a number of problems:
* the collision detection doesn't actually tell you *where* the collision happened, nor does it stop movement in
directions perpendicular to the collision axis
* players can jump off of physical projectiles in flight

so now we're going to go back to non-physical and use raycasts - but not in a simple way! the arc of the projectile
will be estimated, so that lag doesn't cause arrows to collide with things that aren't actually in their path.

this is alpha-quality currently - the API is not fully finalized.
123 changes: 50 additions & 73 deletions api/behavior/on_hit_node.lua
@@ -1,44 +1,24 @@
function ballistics.on_hit_node_freeze(self, node_pos, node, axis, old_velocity, new_velocity)
function ballistics.on_hit_node_freeze(self, node_pos, node, above_pos, intersection_point, intersection_normal, box_id)
local obj = self.object
local our_pos = obj:get_pos()
if not our_pos then
return
end

local collision_position = ballistics.guess_collision_position(self, new_velocity)
if collision_position then
obj:set_pos(collision_position)
end
obj:set_pos(intersection_point)

ballistics.freeze(self)
end

local function get_adjacent_node(self, node_pos, axis)
local last_pos = self._last_pos:round()
local delta = vector.zero()
if axis == "x" then
if node_pos.x < last_pos.x then
delta = vector.new(1, 0, 0)
else
delta = vector.new(-1, 0, 0)
end
elseif axis == "y" then
if node_pos.y < last_pos.y then
delta = vector.new(0, 1, 0)
else
delta = vector.new(0, -1, 0)
end
elseif axis == "z" then
if node_pos.z < last_pos.z then
delta = vector.new(0, 0, 1)
else
delta = vector.new(0, 0, -1)
end
end
return node_pos + delta
end

function ballistics.on_hit_node_add_entity(self, node_pos, node, axis, old_velocity, new_velocity)
function ballistics.on_hit_node_add_entity(
self,
node_pos,
node,
above_pos,
intersection_point,
intersection_normal,
box_id
)
local pprops = self._parameters.add_entity
assert(pprops, "must define parameters.add_entity in projectile definition")
local entity_name = pprops.entity_name
Expand All @@ -47,15 +27,23 @@ function ballistics.on_hit_node_add_entity(self, node_pos, node, axis, old_veloc
local staticdata = pprops.staticdata

if math.random(chance) == 1 then
minetest.add_entity(get_adjacent_node(self, node_pos, axis), entity_name, staticdata)
minetest.add_entity(above_pos, entity_name, staticdata)
end

self.object:remove()
return true
end

if minetest.get_modpath("tnt") then
function ballistics.on_hit_node_boom(self, node_pos, node, axis, old_velocity, new_velocity)
function ballistics.on_hit_node_boom(
self,
node_pos,
node,
above_pos,
intersection_point,
intersection_normal,
box_id
)
local boom = self._parameters.boom or {}
local def = table.copy(boom)
if self._source_obj and minetest.is_player(self._source_obj) then
Expand All @@ -67,55 +55,48 @@ if minetest.get_modpath("tnt") then
end
end

-- TODO: the ball never stops bouncing and "rolls" endlessly?
function ballistics.on_hit_node_bounce(self, node_pos, node, axis, old_velocity)
local bounce = self._parameters.bounce or {}
local efficiency = bounce.efficiency or 1
local clamp = bounce.clamp or 0

local delta_velocity = vector.zero()
if axis == "x" then
local dx = -old_velocity.x * efficiency
if dx > clamp then
delta_velocity.x = dx
end
elseif axis == "y" then
local dy = -old_velocity.y * efficiency
if dy > clamp then
delta_velocity.y = dy
end
elseif axis == "z" then
local dz = -old_velocity.z * efficiency
if dz > clamp then
delta_velocity.z = dz
end
end

if not delta_velocity:equals(vector.zero()) then
self.object:add_velocity(delta_velocity)
end
end

function ballistics.on_hit_node_dig(self, node_pos, node, axis, old_velocity, new_velocity)
function ballistics.on_hit_node_dig(self, node_pos, node, above_pos, intersection_point, intersection_normal, box_id)
minetest.node_dig(node_pos, node, self._source_obj)
self.object:remove()
return true
end

-- TODO: allow specifying multiple possible targets, groups
function ballistics.on_hit_node_replace(self, node_pos, node, axis, old_velocity, new_velocity)
local pos0 = get_adjacent_node(self, node_pos, axis)
return ballistics.util.replace(self, pos0)
function ballistics.on_hit_node_replace(
self,
node_pos,
node,
above_pos,
intersection_point,
intersection_normal,
box_id
)
return ballistics.util.replace(self, above_pos)
end

function ballistics.on_hit_node_active_sound_stop(self)
function ballistics.on_hit_node_active_sound_stop(
self,
node_pos,
node,
above_pos,
intersection_point,
intersection_normal,
box_id
)
if self._active_sound_handle then
minetest.sound_stop(self._active_sound_handle)
self._active_sound_handle = nil
end
end

function ballistics.on_hit_node_hit_sound_play(self)
function ballistics.on_hit_node_hit_sound_play(
self,
node_pos,
node,
above_pos,
intersection_point,
intersection_normal,
box_id
)
local pos = self.object:get_pos() or self._last_pos
if not pos then
return
Expand All @@ -132,7 +113,3 @@ function ballistics.on_hit_node_hit_sound_play(self)
parameters.exclude_player = nil
minetest.sound_play(spec, parameters, true)
end

function ballistics.on_hit_node_become_non_physical(self)
self.object:set_properties({ physical = false })
end
45 changes: 10 additions & 35 deletions api/behavior/on_hit_object.lua
Expand Up @@ -8,7 +8,7 @@ local function get_target_visual_size(target)
end

-- TODO: this function currently does *NOT* work correctly
function ballistics.on_hit_object_stick(self, target, axis, old_velocity, new_velocity)
function ballistics.on_hit_object_stick(self, target, intersection_point, intersection_normal, box_id)
local obj = self.object
if not obj:get_pos() then
return
Expand Down Expand Up @@ -63,7 +63,7 @@ local function scale_tool_capabilities(tool_capabilities, scale_speed, velocity)
return scaled_caps
end

function ballistics.on_hit_object_punch(self, target, axis, old_velocity, new_velocity)
function ballistics.on_hit_object_punch(self, target, intersection_point, intersection_normal, box_id)
local pprops = self._parameters.punch
assert(
pprops and pprops.tool_capabilities,
Expand All @@ -87,7 +87,7 @@ function ballistics.on_hit_object_punch(self, target, axis, old_velocity, new_ve
target:punch(
puncher,
tool_capabilities.full_punch_interval or math.huge,
scale_tool_capabilities(tool_capabilities, scale_speed, old_velocity),
scale_tool_capabilities(tool_capabilities, scale_speed, self.object:get_velocity()),
direction
)
end
Expand All @@ -98,44 +98,23 @@ function ballistics.on_hit_object_punch(self, target, axis, old_velocity, new_ve
end
end

function ballistics.on_hit_object_add_entity(self, target, axis, old_velocity, new_velocity)
function ballistics.on_hit_object_add_entity(self, target, intersection_point, intersection_normal, box_id)
local pprops = self._parameters.add_entity
local entity_name = pprops.entity_name
local chance = pprops.chance or 1
local staticdata = pprops.staticdata

local target_pos = target:get_pos()
if target_pos and math.random(chance) == 1 then
local last_pos = self._last_pos:round()
local delta = vector.zero()
if axis == "x" then
if target_pos.x < last_pos.x then
delta = vector.new(1, 0, 0)
else
delta = vector.new(-1, 0, 0)
end
elseif axis == "y" then
if target_pos.y < last_pos.y then
delta = vector.new(0, 1, 0)
else
delta = vector.new(0, -1, 0)
end
elseif axis == "z" then
if target_pos.z < last_pos.z then
delta = vector.new(0, 0, 1)
else
delta = vector.new(0, 0, -1)
end
end
minetest.add_entity(target_pos + delta, entity_name, staticdata)
minetest.add_entity(target_pos + intersection_normal, entity_name, staticdata)
end

self.object:remove()
return true
end

if minetest.get_modpath("tnt") then
function ballistics.on_hit_object_boom(self, target, axis, old_velocity, new_velocity)
function ballistics.on_hit_object_boom(self, target, intersection_point, intersection_normal, box_id)
local boom = self._parameters.boom or {}
local def = table.copy(boom)
if self._source_obj and minetest.is_player(self._source_obj) then
Expand All @@ -154,22 +133,22 @@ if minetest.get_modpath("tnt") then
end

-- TODO: allow specifying multiple possible targets, groups
function ballistics.on_hit_object_replace(self, object, axis, old_velocity, new_velocity)
function ballistics.on_hit_object_replace(self, object, intersection_point, intersection_normal, box_id)
local pos0 = object:get_pos() or self.object:get_pos()
if not pos0 then
return
end
return ballistics.util.replace(self, pos0)
end

function ballistics.on_hit_object_active_sound_stop(self)
function ballistics.on_hit_object_active_sound_stop(self, object, intersection_point, intersection_normal, box_id)
if self._active_sound_handle then
minetest.sound_stop(self._active_sound_handle)
self._active_sound_handle = nil
end
end

function ballistics.on_hit_object_hit_sound_play(self)
function ballistics.on_hit_object_hit_sound_play(self, object, intersection_point, intersection_normal, box_id)
local pos = self.object:get_pos() or self._last_pos
if not pos then
return
Expand All @@ -187,7 +166,7 @@ function ballistics.on_hit_object_hit_sound_play(self)
minetest.sound_play(spec, parameters, true)
end

function ballistics.on_hit_object_drop_item(self)
function ballistics.on_hit_object_drop_item(self, object, intersection_point, intersection_normal, box_id)
local pprops = self._parameters.drop_item
assert(pprops and pprops.item, "must specify parameters.drop_item.item in projectile definition")
local item = pprops.item
Expand All @@ -207,7 +186,3 @@ function ballistics.on_hit_object_drop_item(self)
obj:remove()
return true
end

function ballistics.on_hit_object_become_non_physical(self)
self.object:set_properties({ physical = false })
end

0 comments on commit d9f3f2b

Please sign in to comment.