SNKRX/engine/game/steering.lua

318 lines
12 KiB
Lua

-- Sets the object as a steerable object.
-- This is implemented in the Physics mixin because it plays well with the rest of it, thus, to make a game object steerable it needs to implement the Physics mixin.
-- The implementation of steering behaviors here mostly follows the one from chapter 3 of the book "Programming Game AI by Example"
-- https://github.com/wangchen/Programming-Game-AI-by-Example-src
-- self:set_as_steerable(100, 1000)
function Physics:set_as_steerable(max_v, max_f, max_turn_rate, turn_multiplier)
self.steerable = true
self.steering_enabled = true
self.heading = Vector()
self.side = Vector()
self.steering_force = Vector()
self.applied_force = Vector()
self.applied_impulse = Vector()
self.mass = 1
self.max_v = max_v or 100
self.max_f = max_f or 2000
self.max_turn_rate = max_turn_rate or 2*math.pi
self.turn_multiplier = turn_multiplier or 2
self.seek_f = Vector()
self.flee_f = Vector()
self.pursuit_f = Vector()
self.evade_f = Vector()
self.wander_f = Vector()
local r = random:float(0, 2*math.pi)
self.wander_target = Vector(40*math.cos(r), 40*math.sin(r))
self.path_follow_f = Vector()
self.separation_f = Vector()
self.alignment_f = Vector()
self.cohesion_f = Vector()
self.apply_force_f = Vector()
self.apply_impulse_f = Vector()
end
function Physics:steering_update(dt)
if self.steerable and self.steering_enabled then
local steering_force = self:calculate_steering_force(dt):div(self.mass)
local applied_force = self:calculate_applied_force(dt):div(self.mass)
local applied_impulse = self:calculate_applied_impulse(dt):div(self.mass)
self:apply_force(steering_force.x + applied_force.x, steering_force.y + applied_force.y)
local vx, vy = self:get_velocity()
local v = Vector(vx, vy):truncate(self.max_v)
self:set_velocity(v.x + applied_impulse.x, v.y + applied_impulse.y)
if v:length_squared() > 0.00001 then
self.heading = v:clone():normalize()
self.side = self.heading:perpendicular()
end
self.apply_force_f:set(0, 0)
self.apply_impulse_f:set(0, 0)
end
end
function Physics:calculate_steering_force(dt)
self.steering_force:set(0, 0)
if self.seeking then self.steering_force:add(self.seek_f) end
if self.fleeing then self.steering_force:add(self.flee_f) end
if self.pursuing then self.steering_force:add(self.pursuit_f) end
if self.evading then self.steering_force:add(self.evade_f) end
if self.wandering then self.steering_force:add(self.wander_f) end
if self.path_following then self.steering_force:add(self.path_follow_f) end
if self.separating then self.steering_force:add(self.separation_f) end
if self.aligning then self.steering_force:add(self.alignment_f) end
if self.cohesing then self.steering_force:add(self.cohesion_f) end
self.seeking = false
self.fleeing = false
self.pursuing = false
self.evading = false
self.wandering = false
self.path_following = false
self.separating = false
self.aligning = false
self.cohesing = false
return self.steering_force:truncate(self.max_f)
end
function Physics:calculate_applied_force(dt)
self.applied_force:set(0, 0)
if self.applying_force then self.applied_force:add(self.apply_force_f) end
return self.applied_force
end
function Physics:calculate_applied_impulse(dt)
self.applied_impulse:set(0, 0)
if self.applying_impulse then self.applied_impulse:add(self.apply_impulse_f) end
return self.applied_impulse
end
-- Applies force f to the object at the given angle r for duration s
-- This plays along with steering behaviors, whereas the apply_force function simply applies it directly to the body and doesn't work when steering behaviors are enabled
-- self:apply_steering_force(100, math.pi/4)
function Physics:apply_steering_force(f, r, s)
self.applying_force = true
self.apply_force_f:set(f*math.cos(r), f*math.sin(r))
if s then
self.t:after((s or 0.01)/2, function()
self.t:tween((s or 0.01)/2, self.apply_force_f, {x = 0, y = 0}, math.linear, function()
self.applying_force = false
self.apply_force_f:set(0, 0)
end, 'apply_steering_force_2')
end, 'apply_steering_force_1')
end
end
-- Applies impulse f to the object at the given angle r for duration s
-- This plays along with steering behaviors, whereas the apply_impulse function simply applies it directly to the body and doesn't work when steering behaviors are enabled
-- self:apply_steering_impulse(100, math.pi/4, 0.5)
function Physics:apply_steering_impulse(f, r, s)
self.applying_impulse = true
self.apply_impulse_f:set(f*math.cos(r), f*math.sin(r))
if s then
self.t:after((s or 0.01)/2, function()
self.t:tween((s or 0.01)/2, self.apply_impulse_f, {x = 0, y = 0}, math.linear, function()
self.applying_impulse = false
self.apply_impulse_f:set(0, 0)
end, 'apply_steering_impulse_2')
end, 'apply_steering_impulse_1')
end
end
-- Arrive steering behavior
-- Makes this object accelerate towards a destination, slowing down the closer it gets to it
-- deceleration - how fast the object will decelerate once it gets closer to the target, higher values will make the deceleration more abrupt, do not make this value 0
-- weight - how much the force of this behavior affects this object compared to others
-- self:seek_point(player.x, player.y)
function Physics:seek_point(x, y, deceleration, weight)
self.seeking = true
local tx, ty = x - self.x, y - self.y
local d = math.length(tx, ty)
if d > 0 then
local v = d/((deceleration or 1)*0.08)
v = math.min(v, self.max_v)
local dvx, dvy = v*tx/d, v*ty/d
local vx, vy = self:get_velocity()
self.seek_f:set((dvx - vx)*self.turn_multiplier*(weight or 1), (dvy - vy)*self.turn_multiplier*(weight or 1))
else self.seek_f:set(0, 0) end
end
-- Same as self:seek_point but for objects instead.
-- self:seek_object(player)
function Physics:seek_object(object, deceleration, weight)
return self:seek_point(object.x, object.y, deceleration, weight)
end
-- Same as self:seek_point and self:seek_object but for the mouse instead.
-- self:seek_mouse()
function Physics:seek_mouse(deceleration, weight)
local mx, my = self.group.camera:get_mouse_position()
return self:seek_point(mx, my, deceleration, weight)
end
-- Separation steering behavior
-- Keeps this object separated from other objects of specific classes according to the radius passed in
-- What this function does is simply look at all nearby objects and apply forces to this object such that it remains separated from them
-- self:separate(40, {Enemy}) -> when this is called every frame, this applies forces to this object to keep it separated from other Enemy instances by 40 units at all times
function Physics:steering_separate(rs, class_avoid_list, weight)
self.separating = true
local fx, fy = 0, 0
local objects = table.flatten(table.foreachn(class_avoid_list, function(v) return self.group:get_objects_by_class(v) end), true)
for _, object in ipairs(objects) do
if object.id ~= self.id and math.distance(object.x, object.y, self.x, self.y) < 2*rs then
local tx, ty = self.x - object.x, self.y - object.y
local nx, ny = math.normalize(tx, ty)
local l = math.length(nx, ny)
fx = fx + rs*(nx/l)
fy = fy + rs*(ny/l)
end
end
self.separation_f:set(fx*(weight or 1), fy*(weight or 1))
end
-- Wander steering behavior
-- Makes the object move in a jittery manner, adding some randomness to its movement while keeping the overall direction
-- What this function does is project a circle in front of the entity and then choose a point randomly inside that circle for the entity to move towards and it does that every frame
-- rs - the radius of the circle
-- distance - the distance of the circle from this object, the further away the smoother the changes to movement will be
-- jitter - the amount of jitter to the movement, the higher it is the more abrupt the changes will be
-- self:wander(50, 100, 20)
function Physics:wander(rs, distance, jitter, weight)
self.wandering = true
self.wander_target:add(random:float(-1, 1)*(jitter or 20), random:float(-1, 1)*(jitter or 20))
self.wander_target:normalize()
self.wander_target:mul(rs or 40)
local target_local = self.wander_target:clone():add(distance or 40, 0)
local target_world = steering.point_to_world_space(target_local, self.heading, self.side, Vector(self.x, self.y))
self.wander_f:set((target_world.x - self.x)*(weight or 1), (target_world.y - self.y)*(weight or 1))
end
-- Steering behavior specific auxiliary functions, shouldn't really be used elsewhere
C2DMatrix = Object:extend()
function C2DMatrix:init()
self._11, self._12, self._13 = 0, 0, 0
self._21, self._22, self._23 = 0, 0, 0
self._31, self._32, self._33 = 0, 0, 0
self:identity()
end
function C2DMatrix:multiply(other)
local mat_temp = C2DMatrix()
mat_temp._11 = (self._11 * other._11) + (self._12 * other._21) + (self._13 * other._31);
mat_temp._12 = (self._11 * other._12) + (self._12 * other._22) + (self._13 * other._32);
mat_temp._13 = (self._11 * other._13) + (self._12 * other._23) + (self._13 * other._33);
mat_temp._21 = (self._21 * other._11) + (self._22 * other._21) + (self._23 * other._31);
mat_temp._22 = (self._21 * other._12) + (self._22 * other._22) + (self._23 * other._32);
mat_temp._23 = (self._21 * other._13) + (self._22 * other._23) + (self._23 * other._33);
mat_temp._31 = (self._31 * other._11) + (self._32 * other._21) + (self._33 * other._31);
mat_temp._32 = (self._31 * other._12) + (self._32 * other._22) + (self._33 * other._32);
mat_temp._33 = (self._31 * other._13) + (self._32 * other._23) + (self._33 * other._33);
self._11 = mat_temp._11; self._12 = mat_temp._12; self._13 = mat_temp._13
self._21 = mat_temp._21; self._22 = mat_temp._22; self._23 = mat_temp._23
self._31 = mat_temp._31; self._32 = mat_temp._32; self._33 = mat_temp._33
end
function C2DMatrix:identity()
self._11, self._12, self._13 = 1, 0, 0
self._21, self._22, self._23 = 0, 1, 0
self._31, self._32, self._33 = 0, 0, 1
end
function C2DMatrix:transform_vector(point)
local temp_x = (self._11 * point.x) + (self._21 * point.y) + (self._31)
local temp_y = (self._12 * point.x) + (self._22 * point.y) + (self._32)
point.x, point.y = temp_x, temp_y
end
function C2DMatrix:translate(x, y)
local mat = C2DMatrix()
mat._11 = 1; mat._12 = 0; mat._13 = 0;
mat._21 = 0; mat._22 = 1; mat._23 = 0;
mat._31 = x; mat._32 = y; mat._33 = 1;
self:multiply(mat)
end
function C2DMatrix:scale(sx, sy)
local mat = C2DMatrix()
mat._11 = sx; mat._12 = 0; mat._13 = 0;
mat._21 = 0; mat._22 = sy; mat._23 = 0;
mat._31 = 0; mat._32 = 0; mat._33 = 1;
self:multiply(mat)
end
function C2DMatrix:rotate(fwd, side)
local mat = C2DMatrix()
mat._11 = fwd.x; mat._12 = fwd.y; mat._13 = 0;
mat._21 = side.x; mat._22 = side.y; mat._23 = 0;
mat._31 = 0; mat._32 = 0; mat._33 = 1;
self:multiply(mat)
end
function C2DMatrix:rotater(r)
local mat = C2DMatrix()
local sin = math.sin(r)
local cos = math.cos(r)
mat._11 = cos; mat._12 = sin; mat._13 = 0;
mat._21 = -sin; mat._22 = cos; mat._23 = 0;
mat._31 = 0; mat._32 = 0; mat._33 = 1;
self:multiply(mat)
end
steering = {}
function steering.point_to_world_space(point, heading, side, position)
local trans_point = Vector(point.x, point.y)
local mat_transform = C2DMatrix()
mat_transform:rotate(heading, side)
mat_transform:translate(position.x, position.y)
mat_transform:transform_vector(trans_point)
return trans_point
end
function steering.point_to_local_space(point, heading, side, position)
local trans_point = Vector(point.x, point.y)
local mat_transform = C2DMatrix()
local tx, ty = -position:dot(heading), -position:dot(side)
mat_transform._11 = heading.x; mat_transform._12 = side.x;
mat_transform._21 = heading.y; mat_transform._22 = side.y;
mat_transform._31 = tx; mat_transform._32 = ty;
mat_transform:transform_vector(trans_point)
return trans_point
end
function steering.vector_to_world_space(v, heading, side)
local trans_v = Vector(v.x, v.y)
local mat_transform = C2DMatrix()
mat_transform:rotate(heading, side)
mat_transform:transform_vector(trans_v)
return trans_v
end
function steering.rotate_vector_around_origin(v, r)
local mat = C2DMatrix()
mat:rotater(r)
mat:transform_vector(v)
return v
end