Quod erat demonstrandum
Descrição
O script implementa classes auxiliares para sistemas de detecção de colisão via hitbox, inspirado nesse post do [member=1597]Jorge_Maker[/member].
Vale ressaltar que esse script não adiciona funcionalidades ao jogo por si só, apenas disponibiliza as ferramentas para que outros scripts o façam em seus próprios sistemas.
Inicialmente, o script era implementado puramente em Ruby. Conforme fui fazendo testes de performance, porém, ficou claro que o desempenho deixava a desejar, e como o intuito desse script é justamente ser eficiente, fiz o que precisava ser feito: implementei toda a lógica de colisão em C++, com paralelismo e tudo a que se tem direito, e botei numa DLL.
Esse script implementa duas classes:
- Hitbox: Classe de hitbox. É basicamente um Rect que implementa funções de interseção.
- Stage: Classe usada para colocar as hitboxes e testar colisão entre elas.
Instruções
Cole acima do script Main e abaixo do DLL Utils. Não esqueça de baixar a DLL disponível abaixo e salvar na pasta do projeto com o nome hitboxes.dll.
Exemplo de Uso
Segue código que faz uso de todos os recursos que o script oferece:
Código:
# Espaço para checagem de colisões
stage = Stage.new(Graphics.width, Graphics.height)
# Criamos 16 hitboxes de vários tamanhos em posições aleatórias
hitboxes = Array.new(16) do |i|
Hitbox.new(rand(Graphics.width), rand(Graphics.height), 4 * i, 4 * i)
end
# Índice de hitboxes que colidiram, para colorir diferente
collided = {}
# Desenho na tela
screen = Sprite.new
screen.bitmap = Bitmap.new(Graphics.width, Graphics.height)
BLUE = Color.new(0, 0, 255)
RED = Color.new(255, 0, 0)
def update_hitbox(stage, hitbox)
# Move a hitbox em direção ao centro da tela
cx = Graphics.width / 2
cy = Graphics.width / 2
if cx != hitbox.rect.x
t = Math.atan2(cy - hitbox.rect.y, cx - hitbox.rect.x)
hitbox.move(Math.cos(t) * 2, Math.sin(t) * 2)
end
# No momento, o Stage não suporta hitboxes em movimento. Então tiramos
# ela e colocamos de novo para atualizar a quadtree
stage.delete(hitbox)
stage.push(hitbox)
end
loop do
screen.bitmap.clear
for h in hitboxes
# Desenha a hitbox na tela
screen.bitmap.fill_rect(h.rect, collided[h.handle] ? BLUE : RED)
# Atualiza o estado da hitbox
update_hitbox(stage, h)
end
# Atualiza o estado das colisões
stage.update
# Sinaliza colisões
for a, b in stage.collisions
collided[a.handle] = true
collided[b.handle] = true
end
Graphics.update
end
Observações
Esta é uma versão inicial do script e deve ser vista como prova de conceito.
Um problema grave da implementação atual é que a Quadtree não lida com mudança de posição das hitboxes, e deve ser reconstruída a todo frame.
Também pode ocorrer travamento do sistema por completo caso mais que 16 objetos estejam numa mesma posição no espaço. Isso acontece pela forma como a quadtree está implementada. Estou estudando uma forma de corrigir esse problema.
Ajustes voltados para a correção desses problemas serão feitos no futuro.
Download
Script
Ruby:
#==============================================================================
# Hitboxes | v0.1.0 | por Masked
#
# para RPG Maker VX Ace
#------------------------------------------------------------------------------
# Implementa hitboxes com posição e dimensão e operações básicas como colisão
# e movimento.
# Feito com intuito de servir de base para o desenvolvimento de outros
# scripts.
#==============================================================================
__END__ if ($modules ||= {})[:hitboxes]
$modules[:hitboxes] = 2.0
#==============================================================================
# ** Vector2D
#------------------------------------------------------------------------------
# Classe de vetor bidimensional
#==============================================================================
class Vector2D
#--------------------------------------------------------------------------
# * Atributos
#--------------------------------------------------------------------------
attr_reader :x, :y
#--------------------------------------------------------------------------
# * Construtor
#--------------------------------------------------------------------------
def initialize(x, y)
@x = x
@y = y
end
#--------------------------------------------------------------------------
# * Construtor
#--------------------------------------------------------------------------
class << self
alias [] new
end
#--------------------------------------------------------------------------
# * Obtém a norma (comprimento) do vetor
#--------------------------------------------------------------------------
def norm
Math.hypot(@x, @y)
end
#--------------------------------------------------------------------------
# * Obtém a norma quadrada do vetor
#--------------------------------------------------------------------------
def norm2
@x ** 2 + @y ** 2
end
#--------------------------------------------------------------------------
# * Verifica igualdade entre vetores
#--------------------------------------------------------------------------
def ==(other)
return false unless other.is_a? Vector2D
self.x == other.x and self.y == other.y
end
#--------------------------------------------------------------------------
# * Soma dois vetores
#--------------------------------------------------------------------------
def +(other)
Vector2D.new(self.x + other.x, self.y + other.y)
end
#--------------------------------------------------------------------------
# * Subtrai dois vetores
#--------------------------------------------------------------------------
def -(other)
Vector2D.new(self.x - other.x, self.y - other.y)
end
#--------------------------------------------------------------------------
# * Negativa o vetor
#--------------------------------------------------------------------------
def -@
Vector2D.new(-self.x, -self.y)
end
#--------------------------------------------------------------------------
# * Multiplica o vetor por um número
#--------------------------------------------------------------------------
def *(scale)
Vector2D.new(self.x * scale, self.y * scale)
end
#--------------------------------------------------------------------------
# * Divide o vetor por um número
#--------------------------------------------------------------------------
def /(scale)
Vector2D.new(self.x / scale, self.y / scale)
end
#--------------------------------------------------------------------------
# * Produto vetorial
#--------------------------------------------------------------------------
def dot(other)
self.x * other.x + self.y * other.y
end
#--------------------------------------------------------------------------
# * Módulo do produto em cruz
#--------------------------------------------------------------------------
def cross(other)
self.x * other.y - self.y * other.x
end
#--------------------------------------------------------------------------
# * Obtém um elemento no vetor por posição
#--------------------------------------------------------------------------
def [](i)
to_a[i]
end
#--------------------------------------------------------------------------
# * Converte o vetor em array
#--------------------------------------------------------------------------
def to_a
[@x, @y]
end
end
#==============================================================================
# ** Matrix2D
#------------------------------------------------------------------------------
# Classe de matrix 2x2
#==============================================================================
class Matrix2D
#--------------------------------------------------------------------------
# * Construtor
#--------------------------------------------------------------------------
def initialize(a, b, c, d)
@values = [a, b, c, d]
end
#--------------------------------------------------------------------------
# * Construtor
#--------------------------------------------------------------------------
class << self
alias [] new
end
#--------------------------------------------------------------------------
# * Obtém o valor na posição m, n da matriz
#--------------------------------------------------------------------------
def [](m, n)
@values[m * 2 + n]
end
#--------------------------------------------------------------------------
# * Obtém a determinante da matriz
#--------------------------------------------------------------------------
def determinant
self[0, 0] * self[0, 1] - self[1, 0] * self[1, 1]
end
#--------------------------------------------------------------------------
# * Verifica igualdade entre matrizes
#--------------------------------------------------------------------------
def ==(other)
return false unless other.is_a? Matrix2D
@values == other.instance_variable_get(:@values)
end
#--------------------------------------------------------------------------
# * Mapeia cada valor da matriz usando uma função dada
#--------------------------------------------------------------------------
def map &block
Matrix2D[*@values.map(&block)]
end
#--------------------------------------------------------------------------
# * Mapeia cada valor da matriz com posição usando uma função dada
#--------------------------------------------------------------------------
def map_with_index &block
a = @values.zip([0, 0, 1, 1], [0, 1, 0, 1]).map &block
Matrix2D[*a]
end
#--------------------------------------------------------------------------
# * Soma duas matrizes
#--------------------------------------------------------------------------
def +(other)
map_with_index do |v, m, n|
v + other[m, n]
end
end
#--------------------------------------------------------------------------
# * Subtrai dois vetores
#--------------------------------------------------------------------------
def -(other)
map_with_index do |v, m, n|
v - other[m, n]
end
end
#--------------------------------------------------------------------------
# * Negativa o vetor
#--------------------------------------------------------------------------
def -@
map do |v|
-v
end
end
#--------------------------------------------------------------------------
# * Multiplica o vetor por um número
#--------------------------------------------------------------------------
def *(other)
if other.is_a? Numeric
scale other
elsif other.is_a? Matrix2D
map_with_index do |v, m, n|
self[m, 0] * self[0, n] + self[m, 1] * self[1, n]
end
elsif other.is_a? Vector2D
Vector2D[
self[0, 0] * other.x + self[0, 1] * other.y,
self[1, 0] * other.x + self[1, 1] * other.y,
]
else
raise ArgumentError.new "Argument is neither Numeric, Matrix2D or Vector2D"
end
end
#--------------------------------------------------------------------------
# * Multiplica a matriz por um escalar
#--------------------------------------------------------------------------
def scale(x)
map do |v|
v * x
end
end
#--------------------------------------------------------------------------
# * Divide o vetor por um número
#--------------------------------------------------------------------------
def /(scale)
map do |v|
v / scale
end
end
#--------------------------------------------------------------------------
# * Retorna a transposta da matriz
#--------------------------------------------------------------------------
def transpose
map_with_index do |v, m, n|
self[n, m]
end
end
#--------------------------------------------------------------------------
# * Converte a matriz em array
#--------------------------------------------------------------------------
def to_a
@values
end
end
#==============================================================================
# ** Hitbox
#------------------------------------------------------------------------------
# Classe para caixa de colisão alinhada aos eixos (i.e. não rotacionada)
#==============================================================================
class Hitbox
#--------------------------------------------------------------------------
# * Atributos
#--------------------------------------------------------------------------
attr_reader :x, :y
attr_reader :width, :height
#--------------------------------------------------------------------------
# * Construtor
# x : Posição X da hitbox
# y : Posição Y da hitbox
# width : Largura da hitbox
# height : Altura da hitbox
#--------------------------------------------------------------------------
def initialize(*args)
if args[0].is_a? Rect
rect = args[0]
else
rect = Rect.new(*args)
end
@x, @y, @width, @height = rect.x, rect.y, rect.width, rect.height
end
#--------------------------------------------------------------------------
# * Posição XY do centro da hitbox
#--------------------------------------------------------------------------
def center
[(left + right) / 2, (top + bottom) / 2]
end
#--------------------------------------------------------------------------
# * Posição esquerda da hitbox
#--------------------------------------------------------------------------
alias left x
#--------------------------------------------------------------------------
# * Posição superior da hitbox
#--------------------------------------------------------------------------
alias top y
#--------------------------------------------------------------------------
# * Posição direita da hitbox
#--------------------------------------------------------------------------
def right
x + width
end
#--------------------------------------------------------------------------
# * Posição inferior da hitbox
#--------------------------------------------------------------------------
def bottom
y + height
end
#--------------------------------------------------------------------------
# * Retângulo da hitbox
#--------------------------------------------------------------------------
def rect
Rect.new(x, y, width, height)
end
#--------------------------------------------------------------------------
# * Converte a hitbox em array
#--------------------------------------------------------------------------
def to_a
[left, top, right, bottom]
end
#--------------------------------------------------------------------------
# * Soma um vetor à hitbox
#--------------------------------------------------------------------------
def +(vector)
Hitbox.new(x + vector.x, y + vector.y, width, height)
end
#--------------------------------------------------------------------------
# * Subtrai um vetor da hitbox
#--------------------------------------------------------------------------
def -(vector)
Hitbox.new(x - vector.x, y - vector.y, width, height)
end
#--------------------------------------------------------------------------
# * Vértices da hitbox em sentido horário
#--------------------------------------------------------------------------
def vertices
[
Vector2D[left, top ],
Vector2D[right, top ],
Vector2D[right, bottom],
Vector2D[left, bottom]
]
end
#--------------------------------------------------------------------------
# * Verifica se a hitbox contém um ponto
# px : Coordenada X do ponto
# py : Coordenada Y do ponto
#--------------------------------------------------------------------------
def contains?(*args)
return contains_vector?(args[0]) if args[0].is_a? Vector2D
px, py = *args
px.between?(left, right) and py.between?(top, bottom)
end
#--------------------------------------------------------------------------
# * Verifica se a hitbox contém um ponto representado por um vetor
# vector : Vetor representando o ponto
#--------------------------------------------------------------------------
def contains_vector?(vector)
contains? *vector
end
#--------------------------------------------------------------------------
# * Verifica interseção com outra hitbox
#--------------------------------------------------------------------------
def intersects? other
if other.is_a? RotatedHitbox
other.intersects? self
else
right >= other.left and left <= other.right and
bottom >= other.top and top <= other.bottom
end
end
end
#==============================================================================
# ** RotatedHitbox
#------------------------------------------------------------------------------
# Classe para caixa de colisão rotacionável
#==============================================================================
class RotatedHitbox < Hitbox
#--------------------------------------------------------------------------
# * Atributos
#--------------------------------------------------------------------------
attr_reader :angle
#--------------------------------------------------------------------------
# * Construtor
#--------------------------------------------------------------------------
def initialize(*args)
return copy *args if args[0].is_a? Hitbox
unless args.size == 5
raise ArgumentError.new(
"wrong number of arguments (given #{args.size}, expected 5)")
end
@x, @y, @width, @height = *args
self.angle = args[4]
end
#--------------------------------------------------------------------------
# * Define o ângulo da hitbox
#--------------------------------------------------------------------------
def angle=(angle)
@angle = angle % (2 * Math::PI)
@cos = Math.cos @angle
@sin = Math.sin @angle
@vertices = nil
end
#--------------------------------------------------------------------------
# * Cria a partir de uma hitbox e rotaciona ela
# box : Hitbox a ser copiada
# angle : Ângulo a rotacionar
#--------------------------------------------------------------------------
def copy(box, angle = 0)
@x, @y, @width, @height = box.x, box.y, box.width, box.height
@angle = angle
@angle += @angle.angle if box.respond_to? :angle
end
#--------------------------------------------------------------------------
# * Verifica se a hitbox contém um ponto
# px : Coordenada X do ponto
# py : Coordenada Y do ponto
#--------------------------------------------------------------------------
def contains?(px, py)
a, b, c, d = *vertices
p = Vector2D[px, py]
ad = a - d
cd = c - d
tpc = p * 2 - a - c
cd.dot(tpc - cd) <= 0 and cd.dot(tpc + cd) >= 0 and
ad.dot(tpc - ad) <= 0 and ad.dot(tpc + ad) >= 0
end
#--------------------------------------------------------------------------
# * Verifica interseção com outra hitbox
#--------------------------------------------------------------------------
def intersects?(other)
other.vertices.any? {|p| self.contains? p } or
self.vertices.any? {|p| other.contains? p }
end
#--------------------------------------------------------------------------
# * Vértices da hitbox
#--------------------------------------------------------------------------
def vertices
return @vertices if @vertices
origin = self.center()
@vertices = super.map do |v|
origin + rotation_matrix * (v - origin)
end
@vertices
end
#--------------------------------------------------------------------------
# * Matriz de rotação relacionada à rotação da hitbox
#--------------------------------------------------------------------------
def rotation_matrix
Matrix2D[
@cos, -@sin,
@sin, @cos
]
end
end
#==============================================================================
# ** Quadtree
#------------------------------------------------------------------------------
# Implementação de PR (Point-Region) quadtree, para implementar detecção de
# colisões eficiente entre hitboxes
#==============================================================================
class Quadtree
#--------------------------------------------------------------------------
# * Constantes
#--------------------------------------------------------------------------
NODE_CAPACITY = 8
#--------------------------------------------------------------------------
# * Atributos
#--------------------------------------------------------------------------
attr_reader :boundary
attr_reader :objects
attr_reader :children
#--------------------------------------------------------------------------
# * Construtor
#--------------------------------------------------------------------------
def initialize(*args)
@boundary = Hitbox.new(*args)
@objects = []
@children = nil
end
#--------------------------------------------------------------------------
# * Verifica se a árvore é folha (i.e. não tem filhos)
#--------------------------------------------------------------------------
def leaf?
children.nil?
end
#--------------------------------------------------------------------------
# * Insere um ponto na árvore
#--------------------------------------------------------------------------
def push(hitbox)
return false unless hitbox.intersects? @boundary
if objects.size < NODE_CAPACITY and leaf?
objects << hitbox
return true
end
subdivide if leaf?
for c in children
c.push hitbox
end
end
alias << push
#--------------------------------------------------------------------------
# * Subdivide a árvore em quadrantes
#--------------------------------------------------------------------------
def subdivide
l, t, r, b = *boundary.to_a
cx, cy = *boundary.center
@children = [
Quadtree.new(l, t, cx - 1 - l, cy - 1 - t),
Quadtree.new(cx, t, r - cx, cy - 1 - t),
Quadtree.new(cx, cy, r - cx, b - cy),
Quadtree.new(l, cy, cx - 1 - l, b - cy),
]
end
#--------------------------------------------------------------------------
# * Obtém as hitboxes que possivelmente colidem com uma hitbox dada
#--------------------------------------------------------------------------
def near(hitbox)
return [] unless hitbox.intersects? @boundary
result = objects.select {|h| hitbox.intersects? h}
return result if leaf?
@children.inject(result) {|r, tree| r.concat tree.near hitbox }
end
end
Licensa: Creative Commons Attribution-ShareAlike 4.0 International