-- 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