Refactor character and enemy AI movement logic; implement delta time for smoother updates and replace object (a reserved keyword) with prop.
This commit is contained in:
+12
-9
@@ -1,7 +1,7 @@
|
||||
class Character
|
||||
attr_reader :world_x, :world_y
|
||||
|
||||
SPEED = 3
|
||||
SPEED = 180.0
|
||||
SIZE = [25, 30]
|
||||
|
||||
def initialize
|
||||
@@ -56,7 +56,7 @@ class Character
|
||||
false
|
||||
end
|
||||
|
||||
def update
|
||||
def update(dt)
|
||||
dx = 0.0
|
||||
dy = 0.0
|
||||
|
||||
@@ -89,23 +89,26 @@ class Character
|
||||
end
|
||||
|
||||
# Apply speed
|
||||
dx *= SPEED
|
||||
dy *= SPEED
|
||||
dx *= SPEED * dt
|
||||
dy *= SPEED * dt
|
||||
|
||||
steps = SPEED.ceil
|
||||
step_dx = dx / steps.to_f
|
||||
step_dy = dy / steps.to_f
|
||||
distance = Math.sqrt(dx * dx + dy * dy)
|
||||
steps = (distance / 3.0).ceil
|
||||
steps = 1 if steps < 1
|
||||
|
||||
step_dx = dx / steps
|
||||
step_dy = dy / steps
|
||||
|
||||
steps.times do
|
||||
# X axis
|
||||
@world_x += step_dx
|
||||
if ($bus.get_all(:collides?, rect) & [:wall, :object])&.any?
|
||||
if ($bus.get_all(:collides?, rect) & [:wall, :prop])&.any?
|
||||
@world_x -= step_dx
|
||||
end
|
||||
|
||||
# Y axis
|
||||
@world_y += step_dy
|
||||
if ($bus.get_all(:collides?, rect) & [:wall, :object])&.any?
|
||||
if ($bus.get_all(:collides?, rect) & [:wall, :prop])&.any?
|
||||
@world_y -= step_dy
|
||||
end
|
||||
end
|
||||
|
||||
+15
-15
@@ -1,6 +1,6 @@
|
||||
class EnemyAI
|
||||
AGGRO_RADIUS = 2000
|
||||
SPEED = 2
|
||||
SPEED = 120.0
|
||||
CORNER_OFFSET = 35 # how far from tile center to place corner waypoint
|
||||
|
||||
def initialize(enemy, type)
|
||||
@@ -12,7 +12,7 @@ class EnemyAI
|
||||
@avoiding_obstacle = false
|
||||
end
|
||||
|
||||
def update(player_x, player_y)
|
||||
def update(player_x, player_y, dt)
|
||||
distance = Gosu.distance(@enemy.x, @enemy.y, player_x, player_y)
|
||||
return unless distance < AGGRO_RADIUS
|
||||
|
||||
@@ -21,14 +21,14 @@ class EnemyAI
|
||||
|
||||
return if $bus.get_all(:collides?, @enemy.rect)&.include?(:character)
|
||||
|
||||
object_in_tile = $bus.get(:object_at, enemy_tile[0], enemy_tile[1])
|
||||
prop_in_tile = $bus.get(:prop_at, enemy_tile[0], enemy_tile[1])
|
||||
|
||||
if player_tile == enemy_tile
|
||||
los_blocked = !object_in_tile.nil? && !(
|
||||
line_of_sight?(@enemy.x - @enemy.w / 2, @enemy.y - @enemy.h / 2, player_x, player_y, object_in_tile.rect) &&
|
||||
line_of_sight?(@enemy.x + @enemy.w / 2, @enemy.y + @enemy.h / 2, player_x, player_y, object_in_tile.rect) &&
|
||||
line_of_sight?(@enemy.x - @enemy.w / 2, @enemy.y + @enemy.h / 2, player_x, player_y, object_in_tile.rect) &&
|
||||
line_of_sight?(@enemy.x + @enemy.w / 2, @enemy.y - @enemy.h / 2, player_x, player_y, object_in_tile.rect)
|
||||
los_blocked = !prop_in_tile.nil? && !(
|
||||
line_of_sight?(@enemy.x - @enemy.w / 2, @enemy.y - @enemy.h / 2, player_x, player_y, prop_in_tile.rect) &&
|
||||
line_of_sight?(@enemy.x + @enemy.w / 2, @enemy.y + @enemy.h / 2, player_x, player_y, prop_in_tile.rect) &&
|
||||
line_of_sight?(@enemy.x - @enemy.w / 2, @enemy.y + @enemy.h / 2, player_x, player_y, prop_in_tile.rect) &&
|
||||
line_of_sight?(@enemy.x + @enemy.w / 2, @enemy.y - @enemy.h / 2, player_x, player_y, prop_in_tile.rect)
|
||||
)
|
||||
|
||||
if los_blocked
|
||||
@@ -48,7 +48,7 @@ class EnemyAI
|
||||
@avoiding_obstacle = true
|
||||
end
|
||||
else
|
||||
move_towards(player_x, player_y)
|
||||
move_towards(player_x, player_y, dt)
|
||||
return
|
||||
end
|
||||
end
|
||||
@@ -56,7 +56,7 @@ class EnemyAI
|
||||
if @waypoints.empty? || @last_player_tile != player_tile
|
||||
@last_player_tile = player_tile
|
||||
@tile_path = $bus.get(:maze_solve, enemy_tile[0], enemy_tile[1], player_tile[0], player_tile[1]) || []
|
||||
if $bus.get(:object_at, enemy_tile[0], enemy_tile[1]).nil?
|
||||
if $bus.get(:prop_at, enemy_tile[0], enemy_tile[1]).nil?
|
||||
@tile_path.shift
|
||||
end
|
||||
@waypoints = build_waypoints(@tile_path)
|
||||
@@ -72,7 +72,7 @@ class EnemyAI
|
||||
return
|
||||
end
|
||||
|
||||
move_towards(target_x, target_y)
|
||||
move_towards(target_x, target_y, dt)
|
||||
end
|
||||
|
||||
def draw
|
||||
@@ -92,7 +92,7 @@ class EnemyAI
|
||||
center_x = tx * 120 + 90
|
||||
center_y = ty * 120 + 90
|
||||
|
||||
unless $bus.get(:object_at, tx, ty).nil?
|
||||
unless $bus.get(:prop_at, tx, ty).nil?
|
||||
next_tile = tile_path[i + 1]
|
||||
prev_tile = i > 0 ? tile_path[i - 1] : nil
|
||||
|
||||
@@ -126,10 +126,10 @@ class EnemyAI
|
||||
waypoints
|
||||
end
|
||||
|
||||
def move_towards(target_x, target_y)
|
||||
def move_towards(target_x, target_y, dt)
|
||||
angle = Gosu.angle(@enemy.x, @enemy.y, target_x, target_y)
|
||||
dx = Gosu.offset_x(angle, SPEED)
|
||||
dy = Gosu.offset_y(angle, SPEED)
|
||||
dx = Gosu.offset_x(angle, SPEED * dt)
|
||||
dy = Gosu.offset_y(angle, SPEED * dt)
|
||||
|
||||
@enemy.x += dx
|
||||
@enemy.y += dy
|
||||
|
||||
+2
-2
@@ -25,9 +25,9 @@ class Enemy
|
||||
false
|
||||
end
|
||||
|
||||
def update
|
||||
def update(dt)
|
||||
player_pos = $bus.get(:player_position)
|
||||
@ai.update(*player_pos) if player_pos
|
||||
@ai.update(*player_pos, dt) if player_pos
|
||||
end
|
||||
|
||||
def draw
|
||||
|
||||
@@ -5,9 +5,9 @@ class EnemyHandler
|
||||
@enemies = []
|
||||
end
|
||||
|
||||
def update
|
||||
def update(dt)
|
||||
spawn!
|
||||
@enemies.each(&:update)
|
||||
@enemies.each { |enemy| enemy.update(dt) }
|
||||
end
|
||||
|
||||
def spawn!
|
||||
|
||||
@@ -1,159 +0,0 @@
|
||||
class Placeable
|
||||
SIZE = [25, 15]
|
||||
|
||||
# The base class for all placeables in the game. Placeables are things that can be interacted with, but do not move (unlike characters).
|
||||
# They cannot be walked through, but can be interacted with (e.g. a chest can be opened, a torch can be lit, a radar can be used).
|
||||
# They are placed at the center of a tile, and their position is represented by the tile coordinate
|
||||
# An object is one that is placed at the center of a tile (radar, torch & chest)
|
||||
attr_reader :x, :y
|
||||
|
||||
def initialize(x, y)
|
||||
@x = x
|
||||
@y = y
|
||||
@frames_count = 0
|
||||
@spritesheet = nil
|
||||
end
|
||||
|
||||
def collides?(rect)
|
||||
return true if intersects?(self.rect, rect)
|
||||
false
|
||||
end
|
||||
|
||||
def rect
|
||||
[@x * 120 + 90 - SIZE[0] / 2, @y * 120 + 90 - SIZE[1], *SIZE]
|
||||
end
|
||||
|
||||
def update
|
||||
# For now, placeables don't have any behavior
|
||||
end
|
||||
|
||||
def draw
|
||||
return unless @spritesheet
|
||||
|
||||
cam_x, cam_y = $bus.get(:camera_pos) || [0, 0]
|
||||
screen_x = @x * 120 + 90 - cam_x
|
||||
screen_y = @y * 120 + 90 - cam_y
|
||||
|
||||
elapsed = Gosu.milliseconds / 1000.0
|
||||
frame_index = ((elapsed * @frames_count).floor) % @frames_count
|
||||
|
||||
@spritesheet[frame_index].draw(screen_x - 20, screen_y - 20, screen_y, 2, 2)
|
||||
|
||||
return unless DEBUG
|
||||
|
||||
# collision box centered on same point
|
||||
Gosu.draw_rect(screen_x - SIZE[0] / 2, screen_y - SIZE[1], *SIZE, Gosu::Color.new(0x88ff0000), Float::INFINITY)
|
||||
end
|
||||
end
|
||||
|
||||
class Radar < Placeable
|
||||
def initialize(x, y)
|
||||
super(x, y)
|
||||
@frames_count = 4
|
||||
@spritesheet = Gosu::Image.load_tiles("assets/images/radar.png", 20, 20, retro: true)
|
||||
end
|
||||
end
|
||||
|
||||
class Torch < Placeable
|
||||
def initialize(x, y)
|
||||
super(x, y)
|
||||
@frames_count = 4
|
||||
@spritesheet = Gosu::Image.load_tiles("assets/images/torch.png", 20, 20, retro: true)
|
||||
end
|
||||
end
|
||||
|
||||
class Chest < Placeable
|
||||
def initialize(x, y)
|
||||
super(x, y)
|
||||
@wood = rand(0..64)
|
||||
@metal = rand(0..16)
|
||||
@science = rand(0..4)
|
||||
@frames_count = 1
|
||||
@spritesheet = Gosu::Image.load_tiles("assets/images/chest.png", 20, 20, retro: true)
|
||||
end
|
||||
end
|
||||
|
||||
class ObjectHandler
|
||||
attr_reader :objects
|
||||
|
||||
def initialize
|
||||
@objects = []
|
||||
@grid = {}
|
||||
|
||||
spawn_chests!
|
||||
|
||||
$bus.on(:collides?) do |rect|
|
||||
nearby_objects(rect).any? { |obj| obj.collides?(rect) } ? :object : nil
|
||||
end
|
||||
|
||||
$bus.on(:object_at) do |x, y|
|
||||
@grid[[x, y]]
|
||||
end
|
||||
|
||||
$bus.on(:nearby_torches) do |rect|
|
||||
nearby_objects(rect).select { |obj| obj.is_a?(Torch) }.map { |t| t.rect }
|
||||
end
|
||||
end
|
||||
|
||||
def spawn_chests!
|
||||
world_size = $bus.get(:maze_size) || [0, 0]
|
||||
|
||||
cell_size = 10
|
||||
|
||||
(0...world_size[0]).step(cell_size) do |cx|
|
||||
(0...world_size[1]).step(cell_size) do |cy|
|
||||
|
||||
# Random position inside this cell
|
||||
x = cx + rand(cell_size)
|
||||
y = cy + rand(cell_size)
|
||||
|
||||
next if x >= world_size[0] || y >= world_size[1]
|
||||
|
||||
# Keep your spawn exclusion
|
||||
next if (x - 2).abs <= 1 && (y - 2).abs <= 1
|
||||
|
||||
next if $bus.get(:room?, x, y)
|
||||
|
||||
chest = Chest.new(x, y)
|
||||
@objects << chest
|
||||
@grid[[x, y]] = chest
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def nearby_objects(rect)
|
||||
x, y, w, h = rect
|
||||
|
||||
min_tx = ((x) / 120).floor
|
||||
max_tx = ((x + w) / 120).floor
|
||||
min_ty = ((y) / 120).floor
|
||||
max_ty = ((y + h) / 120).floor
|
||||
|
||||
results = []
|
||||
|
||||
(min_tx..max_tx).each do |tx|
|
||||
(min_ty..max_ty).each do |ty|
|
||||
obj = @grid[[tx, ty]]
|
||||
results << obj if obj
|
||||
end
|
||||
end
|
||||
|
||||
results
|
||||
end
|
||||
|
||||
def add(object)
|
||||
@objects << object
|
||||
|
||||
key = [object.x, object.y]
|
||||
@grid[key] = object
|
||||
end
|
||||
|
||||
def update
|
||||
@objects.each(&:update)
|
||||
end
|
||||
|
||||
def draw
|
||||
cam = $bus.get(:camera_pos) || [0, 0]
|
||||
nearby_objects([*cam, *SCREEN_SIZE]).each(&:draw)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,74 @@
|
||||
class Prop
|
||||
SIZE = [25, 15]
|
||||
|
||||
# The base class for all props in the game. Props are things that can be interacted with, but do not move (unlike characters).
|
||||
# They cannot be walked through, but can be interacted with (e.g. a chest can be opened, a torch can be lit, a radar can be used).
|
||||
# They are placed at the center of a tile, and their position is represented by the tile coordinate
|
||||
# A prop is one that is placed at the center of a tile (radar, torch & chest)
|
||||
attr_reader :x, :y
|
||||
|
||||
def initialize(x, y)
|
||||
@x = x
|
||||
@y = y
|
||||
@frames_count = 0
|
||||
@spritesheet = nil
|
||||
end
|
||||
|
||||
def collides?(rect)
|
||||
return true if intersects?(self.rect, rect)
|
||||
false
|
||||
end
|
||||
|
||||
def rect
|
||||
[@x * 120 + 90 - SIZE[0] / 2, @y * 120 + 90 - SIZE[1], *SIZE]
|
||||
end
|
||||
|
||||
def update
|
||||
# For now, placeables don't have any behavior
|
||||
end
|
||||
|
||||
def draw
|
||||
return unless @spritesheet
|
||||
|
||||
cam_x, cam_y = $bus.get(:camera_pos) || [0, 0]
|
||||
screen_x = @x * 120 + 90 - cam_x
|
||||
screen_y = @y * 120 + 90 - cam_y
|
||||
|
||||
elapsed = Gosu.milliseconds / 1000.0
|
||||
frame_index = ((elapsed * @frames_count).floor) % @frames_count
|
||||
|
||||
@spritesheet[frame_index].draw(screen_x - 20, screen_y - 20, screen_y, 2, 2)
|
||||
|
||||
return unless DEBUG
|
||||
|
||||
# collision box centered on same point
|
||||
Gosu.draw_rect(screen_x - SIZE[0] / 2, screen_y - SIZE[1], *SIZE, Gosu::Color.new(0x88ff0000), Float::INFINITY)
|
||||
end
|
||||
end
|
||||
|
||||
class Radar < Prop
|
||||
def initialize(x, y)
|
||||
super(x, y)
|
||||
@frames_count = 4
|
||||
@spritesheet = Gosu::Image.load_tiles("assets/images/radar.png", 20, 20, retro: true)
|
||||
end
|
||||
end
|
||||
|
||||
class Torch < Prop
|
||||
def initialize(x, y)
|
||||
super(x, y)
|
||||
@frames_count = 4
|
||||
@spritesheet = Gosu::Image.load_tiles("assets/images/torch.png", 20, 20, retro: true)
|
||||
end
|
||||
end
|
||||
|
||||
class Chest < Prop
|
||||
def initialize(x, y)
|
||||
super(x, y)
|
||||
@wood = rand(0..64)
|
||||
@metal = rand(0..16)
|
||||
@science = rand(0..4)
|
||||
@frames_count = 1
|
||||
@spritesheet = Gosu::Image.load_tiles("assets/images/chest.png", 20, 20, retro: true)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,86 @@
|
||||
require_relative "base"
|
||||
|
||||
class PropsHandler
|
||||
attr_reader :props
|
||||
|
||||
def initialize
|
||||
@props = []
|
||||
@grid = {}
|
||||
|
||||
spawn_chests!
|
||||
|
||||
$bus.on(:collides?) do |rect|
|
||||
nearby_props(rect).any? { |p| p.collides?(rect) } ? :prop : nil
|
||||
end
|
||||
|
||||
$bus.on(:prop_at) do |x, y|
|
||||
@grid[[x, y]]
|
||||
end
|
||||
|
||||
$bus.on(:nearby_torches) do |rect|
|
||||
nearby_props(rect).select { |p| p.is_a?(Torch) }.map { |t| t.rect }
|
||||
end
|
||||
end
|
||||
|
||||
def spawn_chests!
|
||||
world_size = $bus.get(:maze_size) || [0, 0]
|
||||
|
||||
cell_size = 10
|
||||
|
||||
(0...world_size[0]).step(cell_size) do |cx|
|
||||
(0...world_size[1]).step(cell_size) do |cy|
|
||||
|
||||
# Random position inside this cell
|
||||
x = cx + rand(cell_size)
|
||||
y = cy + rand(cell_size)
|
||||
|
||||
next if x >= world_size[0] || y >= world_size[1]
|
||||
|
||||
# Keep your spawn exclusion
|
||||
next if (x - 2).abs <= 1 && (y - 2).abs <= 1
|
||||
|
||||
next if $bus.get(:room?, x, y)
|
||||
|
||||
chest = Chest.new(x, y)
|
||||
@props << chest
|
||||
@grid[[x, y]] = chest
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def nearby_props(rect)
|
||||
x, y, w, h = rect
|
||||
|
||||
min_tx = ((x) / 120).floor
|
||||
max_tx = ((x + w) / 120).floor
|
||||
min_ty = ((y) / 120).floor
|
||||
max_ty = ((y + h) / 120).floor
|
||||
|
||||
results = []
|
||||
|
||||
(min_tx..max_tx).each do |tx|
|
||||
(min_ty..max_ty).each do |ty|
|
||||
p = @grid[[tx, ty]]
|
||||
results << p if p
|
||||
end
|
||||
end
|
||||
|
||||
results
|
||||
end
|
||||
|
||||
def add(prop)
|
||||
@props << prop
|
||||
|
||||
key = [prop.x, prop.y]
|
||||
@grid[key] = prop
|
||||
end
|
||||
|
||||
def update
|
||||
@props.each(&:update)
|
||||
end
|
||||
|
||||
def draw
|
||||
cam = $bus.get(:camera_pos) || [0, 0]
|
||||
nearby_props([*cam, *SCREEN_SIZE]).each(&:draw)
|
||||
end
|
||||
end
|
||||
+16
-15
@@ -1,7 +1,7 @@
|
||||
require_relative 'state'
|
||||
require_relative 'maze'
|
||||
require_relative 'hud'
|
||||
require_relative 'placeables/base'
|
||||
require_relative 'props/handler'
|
||||
require_relative 'enemy/handler'
|
||||
require_relative 'character'
|
||||
|
||||
@@ -17,7 +17,7 @@ class Game < Scene
|
||||
@maze = Maze.new
|
||||
@character = Character.new
|
||||
@enemies = EnemyHandler.new
|
||||
@objects = ObjectHandler.new
|
||||
@props = PropsHandler.new
|
||||
|
||||
@hud = HUD.new
|
||||
|
||||
@@ -37,7 +37,7 @@ class Game < Scene
|
||||
@maze.draw
|
||||
@character.draw
|
||||
@enemies.draw
|
||||
@objects.draw
|
||||
@props.draw
|
||||
@hud.draw
|
||||
draw_fog!
|
||||
draw_debug! if DEBUG
|
||||
@@ -50,11 +50,6 @@ class Game < Scene
|
||||
end
|
||||
|
||||
def draw_fog!
|
||||
# This part is a shader but due to time constraints, I'm doing it on the CPU.
|
||||
# If done on the GPU, we could do a single pass and have much better performance.
|
||||
# and also make it more fine-grained and smoother gradients.
|
||||
# doing it on the GPU would require better knowledge of the ruby openGL bindings and shader programming, which I don't have right now.
|
||||
|
||||
cam_x, cam_y = @camera
|
||||
cell = 10
|
||||
p_i_radius = 80.0
|
||||
@@ -65,16 +60,22 @@ class Game < Scene
|
||||
# Player screen position
|
||||
px = SCREEN_SIZE[0] / 2.0
|
||||
py = SCREEN_SIZE[1] / 2.0
|
||||
|
||||
|
||||
# Collect torch positions relative to camera
|
||||
torches = $bus.get(:nearby_torches,
|
||||
[cam_x - SCREEN_SIZE[0] / 2, cam_y - SCREEN_SIZE[1] / 2, SCREEN_SIZE[0] * 2, SCREEN_SIZE[1] * 2]
|
||||
[cam_x - SCREEN_SIZE[0] / 2, cam_y - SCREEN_SIZE[1] / 2, SCREEN_SIZE[0] * 2, SCREEN_SIZE[1] * 2]
|
||||
) || []
|
||||
torch_lights = torches.map { |t| [t[0] - cam_x, t[1] - cam_y] }
|
||||
|
||||
|
||||
tiles_x = (SCREEN_SIZE[0] / cell) + 2
|
||||
tiles_y = (SCREEN_SIZE[1] / cell) + 2
|
||||
|
||||
# This part is a shader but due to time constraints, I'm doing it on the CPU.
|
||||
# If done on the GPU, we could have much better performance.
|
||||
# and also make it more fine-grained and smoother gradients.
|
||||
# doing it on the GPU would require better knowledge of the ruby openGL bindings and shader programming,
|
||||
# which I don't have right now.
|
||||
|
||||
tiles_y.times do |j|
|
||||
tiles_x.times do |i|
|
||||
sx = i * cell
|
||||
@@ -143,10 +144,10 @@ class Game < Scene
|
||||
end
|
||||
end
|
||||
|
||||
def update
|
||||
@character.update
|
||||
@enemies.update
|
||||
@objects.update
|
||||
def update(dt)
|
||||
@character.update(dt)
|
||||
@enemies.update(dt)
|
||||
@props.update
|
||||
end
|
||||
|
||||
def button_down(id, _pos)
|
||||
|
||||
@@ -17,6 +17,7 @@ class Window < Gosu::Window
|
||||
self.caption = "Mist"
|
||||
|
||||
@scene = Menu.new
|
||||
@last_time = Gosu.milliseconds
|
||||
|
||||
$bus.on(:quit_game) { close! }
|
||||
|
||||
@@ -35,7 +36,11 @@ class Window < Gosu::Window
|
||||
end
|
||||
|
||||
def update
|
||||
@scene.update
|
||||
now = Gosu.milliseconds
|
||||
dt = (now - @last_time) / 1000.0
|
||||
@last_time = now
|
||||
|
||||
@scene.update(dt)
|
||||
end
|
||||
|
||||
def compute_transform
|
||||
|
||||
+1
-1
@@ -13,7 +13,7 @@ class Menu < Scene
|
||||
@last_mouse_pos = nil
|
||||
end
|
||||
|
||||
def update
|
||||
def update(_dt)
|
||||
pos = $bus.get(:mouse_pos)
|
||||
return unless pos
|
||||
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ class Scene
|
||||
def initialize
|
||||
end
|
||||
|
||||
def update
|
||||
def update(_dt)
|
||||
end
|
||||
|
||||
def draw
|
||||
|
||||
Reference in New Issue
Block a user