2022-12-25 10:44:29 +00:00
package resolv
import (
"math"
)
// Space represents a collision space. Internally, each Space contains a 2D array of Cells, with each Cell being the same size. Cells contain information on which
// Objects occupy those spaces.
type Space struct {
Cells [ ] [ ] * Cell
CellWidth , CellHeight int // Width and Height of each Cell in "world-space" / pixels / whatever
}
// NewSpace creates a new Space. spaceWidth and spaceHeight is the width and height of the Space (usually in pixels), which is then populated with cells of size
// cellWidth by cellHeight. Generally, you want cells to be the size of the smallest collide-able objects in your game, and you want to move Objects at a maximum
// speed of one cell size per collision check to avoid missing any possible collisions.
func NewSpace ( spaceWidth , spaceHeight , cellWidth , cellHeight int ) * Space {
sp := & Space {
CellWidth : cellWidth ,
CellHeight : cellHeight ,
}
sp . Resize ( spaceWidth / cellWidth , spaceHeight / cellHeight )
// sp.Resize(int(math.Ceil(float64(spaceWidth)/float64(cellWidth))),
// int(math.Ceil(float64(spaceHeight)/float64(cellHeight))))
return sp
}
2023-02-15 04:02:07 +00:00
// [WARNING] The slice type boxing/unboxing is proved by profiling to be heavy after transpiled to JavaScript, thus adding some "XxxSingle" shortcuts here.
2022-12-25 10:44:29 +00:00
// Add adds the specified Objects to the Space, updating the Space's cells to refer to the Object.
2023-02-15 04:02:07 +00:00
func ( sp * Space ) AddSingle ( obj * Object ) {
if sp == nil {
panic ( "ERROR: space is nil" )
}
obj . Space = sp
// We call Update() once to make sure the object gets its cells added.
obj . Update ( )
}
2022-12-25 10:44:29 +00:00
func ( sp * Space ) Add ( objects ... * Object ) {
if sp == nil {
panic ( "ERROR: space is nil" )
}
for _ , obj := range objects {
obj . Space = sp
// We call Update() once to make sure the object gets its cells added.
obj . Update ( )
}
}
// Remove removes the specified Objects from being associated with the Space. This should be done whenever an Object is removed from the
// game.
2023-02-15 04:02:07 +00:00
func ( sp * Space ) RemoveSingle ( obj * Object ) {
if sp == nil {
panic ( "ERROR: space is nil" )
}
for 0 < obj . TouchingCells . Cnt {
cell := obj . TouchingCells . Pop ( ) . ( * Cell )
cell . unregister ( obj )
}
obj . Space = nil
}
2022-12-25 10:44:29 +00:00
func ( sp * Space ) Remove ( objects ... * Object ) {
if sp == nil {
panic ( "ERROR: space is nil" )
}
for _ , obj := range objects {
2023-02-15 04:02:07 +00:00
for 0 < obj . TouchingCells . Cnt {
cell := obj . TouchingCells . Pop ( ) . ( * Cell )
2022-12-25 10:44:29 +00:00
cell . unregister ( obj )
}
obj . Space = nil
}
}
// Objects loops through all Cells in the Space (from top to bottom, and from left to right) to return all Objects
// that exist in the Space. Of course, each Object is counted only once.
func ( sp * Space ) Objects ( ) [ ] * Object {
objectsAdded := map [ * Object ] bool { }
objects := [ ] * Object { }
for cy := range sp . Cells {
for cx := range sp . Cells [ cy ] {
2023-02-15 04:02:07 +00:00
rb := sp . Cells [ cy ] [ cx ] . Objects
for i := rb . StFrameId ; i < rb . EdFrameId ; i ++ {
o := rb . GetByFrameId ( i ) . ( * Object )
2022-12-25 10:44:29 +00:00
if _ , added := objectsAdded [ o ] ; ! added {
objects = append ( objects , o )
objectsAdded [ o ] = true
}
}
}
}
return objects
}
// Resize resizes the internal Cells array.
func ( sp * Space ) Resize ( width , height int ) {
2023-02-15 04:02:07 +00:00
sp . Cells = make ( [ ] [ ] * Cell , height )
2022-12-25 10:44:29 +00:00
for y := 0 ; y < height ; y ++ {
2023-02-15 04:02:07 +00:00
sp . Cells [ y ] = make ( [ ] * Cell , width )
2022-12-25 10:44:29 +00:00
for x := 0 ; x < width ; x ++ {
2023-02-15 04:02:07 +00:00
sp . Cells [ y ] [ x ] = newCell ( x , y )
2022-12-25 10:44:29 +00:00
}
}
}
// Cell returns the Cell at the given cellular / spatial (not world) X and Y position in the Space. If the X and Y position are
// out of bounds, Cell() will return nil.
func ( sp * Space ) Cell ( x , y int ) * Cell {
if y >= 0 && y < len ( sp . Cells ) && x >= 0 && x < len ( sp . Cells [ y ] ) {
return sp . Cells [ y ] [ x ]
}
return nil
}
// CheckCells checks a set of cells (from x,y to x + w, y + h in cellular coordinates) and return the first object within those Cells that contains any of the tags given.
// If no tags are given, then CheckCells will return the first Object found in any Cell.
func ( sp * Space ) CheckCells ( x , y , w , h int , tags ... string ) * Object {
for ix := x ; ix < x + w ; ix ++ {
for iy := y ; iy < y + h ; iy ++ {
cell := sp . Cell ( ix , iy )
if cell != nil {
2023-02-15 04:02:07 +00:00
rb := cell . Objects
2022-12-25 10:44:29 +00:00
if len ( tags ) > 0 {
if cell . ContainsTags ( tags ... ) {
2023-02-15 04:02:07 +00:00
for i := rb . StFrameId ; i < rb . EdFrameId ; i ++ {
obj := rb . GetByFrameId ( i ) . ( * Object )
2022-12-25 10:44:29 +00:00
if obj . HasTags ( tags ... ) {
return obj
}
}
}
} else if cell . Occupied ( ) {
2023-02-15 04:02:07 +00:00
return rb . GetByFrameId ( rb . StFrameId ) . ( * Object )
2022-12-25 10:44:29 +00:00
}
}
}
}
return nil
}
// CheckCellsWorld checks the cells of the Grid with the given world coordinates.
// Internally, this is just syntactic sugar for calling Space.WorldToSpace() on the
// position and size given.
func ( sp * Space ) CheckCellsWorld ( x , y , w , h float64 , tags ... string ) * Object {
sx , sy := sp . WorldToSpace ( x , y )
cw , ch := sp . WorldToSpace ( w , h )
return sp . CheckCells ( sx , sy , cw , ch , tags ... )
}
// UnregisterAllObjects unregisters all Objects registered to Cells in the Space.
func ( sp * Space ) UnregisterAllObjects ( ) {
for y := 0 ; y < len ( sp . Cells ) ; y ++ {
for x := 0 ; x < len ( sp . Cells [ y ] ) ; x ++ {
cell := sp . Cells [ y ] [ x ]
2023-02-15 04:02:07 +00:00
rb := cell . Objects
for i := rb . StFrameId ; i < rb . EdFrameId ; i ++ {
o := rb . GetByFrameId ( i ) . ( * Object )
sp . RemoveSingle ( o )
}
2022-12-25 10:44:29 +00:00
}
}
}
// WorldToSpace converts from a world position (x, y) to a position in the Space (a grid-based position).
func ( sp * Space ) WorldToSpace ( x , y float64 ) ( int , int ) {
fx := int ( math . Floor ( x / float64 ( sp . CellWidth ) ) )
fy := int ( math . Floor ( y / float64 ( sp . CellHeight ) ) )
return fx , fy
}
// SpaceToWorld converts from a position in the Space (on a grid) to a world-based position, given the size of the Space when first created.
func ( sp * Space ) SpaceToWorld ( x , y int ) ( float64 , float64 ) {
fx := float64 ( x * sp . CellWidth )
fy := float64 ( y * sp . CellHeight )
return fx , fy
}
// Height returns the height of the Space grid in Cells (so a 320x240 Space with 16x16 cells would have a height of 15).
func ( sp * Space ) Height ( ) int {
return len ( sp . Cells )
}
// Width returns the width of the Space grid in Cells (so a 320x240 Space with 16x16 cells would have a width of 20).
func ( sp * Space ) Width ( ) int {
if len ( sp . Cells ) > 0 {
return len ( sp . Cells [ 0 ] )
}
return 0
}
func ( sp * Space ) CellsInLine ( startX , startY , endX , endY int ) [ ] * Cell {
cells := [ ] * Cell { }
cell := sp . Cell ( startX , startY )
endCell := sp . Cell ( endX , endY )
if cell != nil && endCell != nil {
dv := Vector { float64 ( endX - startX ) , float64 ( endY - startY ) } . Unit ( )
dv [ 0 ] *= float64 ( sp . CellWidth / 2 )
dv [ 1 ] *= float64 ( sp . CellHeight / 2 )
pX , pY := sp . SpaceToWorld ( startX , startY )
p := Vector { pX + float64 ( sp . CellWidth / 2 ) , pY + float64 ( sp . CellHeight / 2 ) }
alternate := false
for cell != nil {
if cell == endCell {
cells = append ( cells , cell )
break
}
cells = append ( cells , cell )
if alternate {
p [ 1 ] += dv [ 1 ]
} else {
p [ 0 ] += dv [ 0 ]
}
cx , cy := sp . WorldToSpace ( p [ 0 ] , p [ 1 ] )
c := sp . Cell ( cx , cy )
if c != cell {
cell = c
}
alternate = ! alternate
}
}
return cells
}