Applied ringbuff to resolv_tailored for reducing memory usage.

This commit is contained in:
genxium
2023-02-15 12:02:07 +08:00
parent 2d179d0cdf
commit 5b7f35b874
19 changed files with 7279 additions and 407 deletions

View File

@@ -2,8 +2,8 @@ package resolv
// Cell is used to contain and organize Object information.
type Cell struct {
X, Y int // The X and Y position of the cell in the Space - note that this is in Grid position, not World position.
Objects []*Object // The Objects that a Cell contains.
X, Y int // The X and Y position of the cell in the Space - note that this is in Grid position, not World position.
Objects *RingBuffer // The Objects that a Cell contains.
}
// newCell creates a new cell at the specified X and Y position. Should not be used directly.
@@ -11,25 +11,27 @@ func newCell(x, y int) *Cell {
return &Cell{
X: x,
Y: y,
Objects: []*Object{},
Objects: NewRingBuffer(16), // A single cell is so small thus wouldn't have many touching objects simultaneously
}
}
// register registers an object with a Cell. Should not be used directly.
func (cell *Cell) register(obj *Object) {
if !cell.Contains(obj) {
cell.Objects = append(cell.Objects, obj)
cell.Objects.Put(obj)
}
}
// unregister unregisters an object from a Cell. Should not be used directly.
func (cell *Cell) unregister(obj *Object) {
for i, o := range cell.Objects {
rb := cell.Objects
for i := rb.StFrameId; i < rb.EdFrameId; i++ {
o := rb.GetByFrameId(i).(*Object)
if o == obj {
cell.Objects[i] = cell.Objects[len(cell.Objects)-1]
cell.Objects = cell.Objects[:len(cell.Objects)-1]
// swap with the st element
rb.SetByFrameId(rb.GetByFrameId(rb.StFrameId), i)
// pop the current st element
rb.Pop()
break
}
@@ -39,7 +41,9 @@ func (cell *Cell) unregister(obj *Object) {
// Contains returns whether a Cell contains the specified Object at its position.
func (cell *Cell) Contains(obj *Object) bool {
for _, o := range cell.Objects {
rb := cell.Objects
for i := rb.StFrameId; i < rb.EdFrameId; i++ {
o := rb.GetByFrameId(i).(*Object)
if o == obj {
return true
}
@@ -49,7 +53,9 @@ func (cell *Cell) Contains(obj *Object) bool {
// ContainsTags returns whether a Cell contains an Object that has the specified tag at its position.
func (cell *Cell) ContainsTags(tags ...string) bool {
for _, o := range cell.Objects {
rb := cell.Objects
for i := rb.StFrameId; i < rb.EdFrameId; i++ {
o := rb.GetByFrameId(i).(*Object)
if o.HasTags(tags...) {
return true
}
@@ -59,5 +65,5 @@ func (cell *Cell) ContainsTags(tags ...string) bool {
// Occupied returns whether a Cell contains any Objects at all.
func (cell *Cell) Occupied() bool {
return len(cell.Objects) > 0
return 0 < cell.Objects.Cnt
}

View File

@@ -3,23 +3,39 @@ package resolv
// Collision contains the results of an Object.Check() call, and represents a collision between an Object and cells that contain other Objects.
// The Objects array indicate the Objects collided with.
type Collision struct {
checkingObject *Object // The checking object
dx, dy float64 // The delta the checking object was moving on that caused this collision
Objects []*Object // Slice of objects that were collided with; sorted according to distance to calling Object.
Cells []*Cell // Slice of cells that were collided with; sorted according to distance to calling Object.
checkingObject *Object // The checking object
dx, dy float64 // The delta the checking object was moving on that caused this collision
Objects *RingBuffer // Slice of objects that were collided with; sorted according to distance to calling Object.
Cells *RingBuffer // Slice of cells that were collided with; sorted according to distance to calling Object.
}
func NewCollision() *Collision {
return &Collision{
Objects: []*Object{},
Objects: NewRingBuffer(16), // I don't expect it to exceed 10 actually
Cells: NewRingBuffer(16),
}
}
func (cc *Collision) Clear() {
cc.checkingObject = nil
cc.dx = 0
cc.dy = 0
cc.Objects.Clear()
cc.Cells.Clear()
}
func (cc *Collision) FirstCollidedObject() *Object {
if 0 >= cc.Objects.Cnt {
return nil
}
return cc.Objects.Pop().(*Object)
}
// HasTags returns whether any objects within the Collision have all of the specified tags. This slice does not contain the Object that called Check().
func (cc *Collision) HasTags(tags ...string) bool {
for _, o := range cc.Objects {
rb := cc.Objects
for i := rb.StFrameId; i < rb.EdFrameId; i++ {
o := rb.GetByFrameId(i).(*Object)
if o == cc.checkingObject {
continue
}
@@ -38,8 +54,9 @@ func (cc *Collision) ObjectsByTags(tags ...string) []*Object {
objects := []*Object{}
for _, o := range cc.Objects {
rb := cc.Objects
for i := rb.StFrameId; i < rb.EdFrameId; i++ {
o := rb.GetByFrameId(i).(*Object)
if o == cc.checkingObject {
continue
}
@@ -105,7 +122,7 @@ func (cc *Collision) SlideAgainstCell(cell *Cell, avoidTags ...string) Vector {
sp := cc.checkingObject.Space
collidingCell := cc.Cells[0]
collidingCell := cc.Cells.GetByFrameId(cc.Cells.StFrameId).(*Cell)
ccX, ccY := sp.SpaceToWorld(collidingCell.X, collidingCell.Y)
hX := float64(sp.CellWidth) / 2.0
hY := float64(sp.CellHeight) / 2.0

View File

@@ -2,7 +2,6 @@ package resolv
import (
"math"
//"sort"
)
// Object represents an object that can be spread across one or more Cells in a Space. An Object is essentially an AABB (Axis-Aligned Bounding Box) Rectangle.
@@ -10,21 +9,36 @@ type Object struct {
Shape Shape // A shape for more specific collision-checking.
Space *Space // Reference to the Space the Object exists within
X, Y, W, H float64 // Position and size of the Object in the Space
TouchingCells []*Cell // An array of Cells the Object is touching
TouchingCells *RingBuffer // An array of Cells the Object is touching
Data interface{} // A pointer to a user-definable object
ignoreList map[*Object]bool // Set of Objects to ignore when checking for collisions
tags []string // A list of tags the Object has
}
// NewObject returns a new Object of the specified position and size.
func NewObjectSingleTag(x, y, w, h float64, tag string) *Object {
o := &Object{
X: x,
Y: y,
W: w,
H: h,
TouchingCells: NewRingBuffer(512), // [WARNING] Should make N large enough to cover all "TouchingCells", otherwise some cells would fail to unregister an object, resulting in memory corruption and incorrect detection result!
tags: []string{tag},
ignoreList: map[*Object]bool{},
}
return o
}
func NewObject(x, y, w, h float64, tags ...string) *Object {
o := &Object{
X: x,
Y: y,
W: w,
H: h,
tags: []string{},
ignoreList: map[*Object]bool{},
X: x,
Y: y,
W: w,
H: h,
TouchingCells: NewRingBuffer(512),
tags: []string{},
ignoreList: map[*Object]bool{},
}
if len(tags) > 0 {
@@ -59,7 +73,7 @@ func (obj *Object) Update() {
space := obj.Space
obj.Space.Remove(obj)
obj.Space.RemoveSingle(obj)
obj.Space = space
@@ -73,7 +87,7 @@ func (obj *Object) Update() {
if c != nil {
c.register(obj)
obj.TouchingCells = append(obj.TouchingCells, c)
obj.TouchingCells.Put(c)
}
}
@@ -154,17 +168,22 @@ func (obj *Object) BoundsToSpace(dx, dy float64) (int, int, int, int) {
// SharesCells returns whether the Object occupies a cell shared by the specified other Object.
func (obj *Object) SharesCells(other *Object) bool {
for _, cell := range obj.TouchingCells {
rb := obj.TouchingCells
for i := rb.StFrameId; i < rb.EdFrameId; i++ {
cell := rb.GetByFrameId(i).(*Cell)
if cell.Contains(other) {
return true
}
}
return false
}
// SharesCellsTags returns if the Cells the Object occupies have an object with the specified tags.
func (obj *Object) SharesCellsTags(tags ...string) bool {
for _, cell := range obj.TouchingCells {
rb := obj.TouchingCells
for i := rb.StFrameId; i < rb.EdFrameId; i++ {
cell := rb.GetByFrameId(i).(*Cell)
if cell.ContainsTags(tags...) {
return true
}
@@ -218,13 +237,12 @@ func (obj *Object) SetBounds(topLeft, bottomRight Vector) {
// Check checks the space around the object using the designated delta movement (dx and dy). This is done by querying the containing Space's Cells
// so that it can see if moving it would coincide with a cell that houses another Object (filtered using the given selection of tag strings). If so,
// Check returns a Collision. If no objects are found or the Object does not exist within a Space, this function returns nil.
func (obj *Object) Check(dx, dy float64, tags ...string) *Collision {
func (obj *Object) CheckAllWithHolder(dx, dy float64, cc *Collision) bool {
if obj.Space == nil {
return nil
return false
}
cc := NewCollision()
cc.checkingObject = obj
if dx < 0 {
@@ -253,63 +271,36 @@ func (obj *Object) Check(dx, dy float64, tags ...string) *Collision {
if c := obj.Space.Cell(x, y); c != nil {
for _, o := range c.Objects {
rb := c.Objects
for i := rb.StFrameId; i < rb.EdFrameId; i++ {
o := rb.GetByFrameId(i).(*Object)
// We only want cells that have objects other than the checking object, or that aren't on the ignore list.
if ignored := obj.ignoreList[o]; o == obj || ignored {
continue
}
if _, added := objectsAdded[o]; (len(tags) == 0 || o.HasTags(tags...)) && !added {
cc.Objects = append(cc.Objects, o)
if _, added := objectsAdded[o]; !added {
cc.Objects.Put(o)
objectsAdded[o] = true
if _, added := cellsAdded[c]; !added {
cc.Cells = append(cc.Cells, c)
cc.Cells.Put(c)
cellsAdded[c] = true
}
continue
}
}
}
}
}
if len(cc.Objects) == 0 {
return nil
if 0 >= cc.Objects.Cnt {
return false
}
/*
// In my use case, order of objects within a collision instance is not needed, and this also favors both runtime performance & size reduction of `jsexport.js`.
ox, oy := cc.checkingObject.Center()
oc := Vector{ox, oy}
sort.Slice(cc.Objects, func(i, j int) bool {
ix, iy := cc.Objects[i].Center()
jx, jy := cc.Objects[j].Center()
return Vector{ix, iy}.Sub(oc).Magnitude2() < Vector{jx, jy}.Sub(oc).Magnitude2()
})
cw := cc.checkingObject.Space.CellWidth
ch := cc.checkingObject.Space.CellHeight
sort.Slice(cc.Cells, func(i, j int) bool {
return Vector{float64(cc.Cells[i].X*cw + (cw / 2)), float64(cc.Cells[i].Y*ch + (ch / 2))}.Sub(oc).Magnitude2() <
Vector{float64(cc.Cells[j].X*cw + (cw / 2)), float64(cc.Cells[j].Y*ch + (ch / 2))}.Sub(oc).Magnitude2()
})
*/
return cc
return true
}
// Overlaps returns if an Object overlaps another Object.

136
resolv_tailored/ringbuf.go Normal file
View File

@@ -0,0 +1,136 @@
package resolv
const (
RING_BUFF_CONSECUTIVE_SET = int32(0)
RING_BUFF_NON_CONSECUTIVE_SET = int32(1)
RING_BUFF_FAILED_TO_SET = int32(2)
)
type RingBuffer struct {
Ed int32 // write index, open index
St int32 // read index, closed index
EdFrameId int32
StFrameId int32
N int32
Cnt int32 // the count of valid elements in the buffer, used mainly to distinguish what "st == ed" means for "Pop" and "Get" methods
Eles []interface{}
}
func NewRingBuffer(n int32) *RingBuffer {
return &RingBuffer{
Ed: 0,
St: 0,
EdFrameId: 0,
StFrameId: 0,
N: n,
Cnt: 0,
Eles: make([]interface{}, n),
}
}
func (rb *RingBuffer) Put(pItem interface{}) {
for 0 < rb.Cnt && rb.Cnt >= rb.N {
// Make room for the new element
rb.Pop()
}
rb.Eles[rb.Ed] = pItem
rb.EdFrameId++
rb.Cnt++
rb.Ed++
if rb.Ed >= rb.N {
rb.Ed -= rb.N // Deliberately not using "%" operator for performance concern
}
}
func (rb *RingBuffer) Pop() interface{} {
if 0 == rb.Cnt {
return nil
}
pItem := rb.Eles[rb.St]
rb.StFrameId++
rb.Cnt--
rb.St++
if rb.St >= rb.N {
rb.St -= rb.N
}
return pItem
}
func (rb *RingBuffer) GetArrIdxByOffset(offsetFromSt int32) int32 {
if 0 == rb.Cnt || 0 > offsetFromSt {
return -1
}
arrIdx := rb.St + offsetFromSt
if rb.St < rb.Ed {
// case#1: 0...st...ed...N-1
if rb.St <= arrIdx && arrIdx < rb.Ed {
return arrIdx
}
} else {
// if rb.St >= rb.Ed
// case#2: 0...ed...st...N-1
if arrIdx >= rb.N {
arrIdx -= rb.N
}
if arrIdx >= rb.St || arrIdx < rb.Ed {
return arrIdx
}
}
return -1
}
func (rb *RingBuffer) GetByOffset(offsetFromSt int32) interface{} {
arrIdx := rb.GetArrIdxByOffset(offsetFromSt)
if -1 == arrIdx {
return nil
}
return rb.Eles[arrIdx]
}
func (rb *RingBuffer) GetByFrameId(frameId int32) interface{} {
if frameId >= rb.EdFrameId || frameId < rb.StFrameId {
return nil
}
return rb.GetByOffset(frameId - rb.StFrameId)
}
// [WARNING] During a battle, frontend could receive non-consecutive frames (either renderFrame or inputFrame) due to resync, the buffer should handle these frames properly.
func (rb *RingBuffer) SetByFrameId(pItem interface{}, frameId int32) (int32, int32, int32) {
oldStFrameId, oldEdFrameId := rb.StFrameId, rb.EdFrameId
if frameId < oldStFrameId {
return RING_BUFF_FAILED_TO_SET, oldStFrameId, oldEdFrameId
}
// By now "rb.StFrameId <= frameId"
if oldEdFrameId > frameId {
arrIdx := rb.GetArrIdxByOffset(frameId - rb.StFrameId)
if -1 != arrIdx {
rb.Eles[arrIdx] = pItem
return RING_BUFF_CONSECUTIVE_SET, oldStFrameId, oldEdFrameId
}
}
// By now "rb.EdFrameId <= frameId"
ret := RING_BUFF_CONSECUTIVE_SET
if oldEdFrameId < frameId {
rb.St, rb.Ed = 0, 0
rb.StFrameId, rb.EdFrameId = frameId, frameId
rb.Cnt = 0
ret = RING_BUFF_NON_CONSECUTIVE_SET
}
// By now "rb.EdFrameId == frameId"
rb.Put(pItem)
return ret, oldStFrameId, oldEdFrameId
}
func (rb *RingBuffer) Clear() {
for 0 < rb.Cnt {
rb.Pop()
}
rb.St = 0
rb.Ed = 0
rb.StFrameId = 0
rb.EdFrameId = 0
}

View File

@@ -38,8 +38,9 @@ func (line *Line) Project(axis Vector) Vector {
}
func (line *Line) Normal() Vector {
v := line.Vector()
return Vector{v[1], -v[0]}.Unit()
dy := line.End[1] - line.Start[1]
dx := line.End[0] - line.Start[0]
return Vector{dy, -dx}.Unit()
}
func (line *Line) Vector() Vector {
@@ -177,24 +178,20 @@ func (cp *ConvexPolygon) AddPoints(vertexPositions ...float64) {
// Lines returns a slice of transformed Lines composing the ConvexPolygon.
func (cp *ConvexPolygon) Lines() []*Line {
lines := []*Line{}
vertices := cp.Transformed()
linesCnt := len(vertices)
if !cp.Closed {
linesCnt -= 1
}
lines := make([]*Line, linesCnt)
for i := 0; i < len(vertices); i++ {
for i := 0; i < linesCnt; i++ {
start, end := vertices[i], vertices[0]
if i < len(vertices)-1 {
end = vertices[i+1]
} else if !cp.Closed {
break
}
line := NewLine(start[0], start[1], end[0], end[1])
lines = append(lines, line)
lines[i] = line
}
return lines
@@ -203,9 +200,9 @@ func (cp *ConvexPolygon) Lines() []*Line {
// Transformed returns the ConvexPolygon's points / vertices, transformed according to the ConvexPolygon's position.
func (cp *ConvexPolygon) Transformed() []Vector {
transformed := []Vector{}
for _, point := range cp.Points {
transformed = append(transformed, Vector{point[0] + cp.X, point[1] + cp.Y})
transformed := make([]Vector, len(cp.Points))
for i, point := range cp.Points {
transformed[i] = Vector{point[0] + cp.X, point[1] + cp.Y}
}
return transformed
}
@@ -275,12 +272,14 @@ func (cp *ConvexPolygon) Center() Vector {
pos := Vector{0, 0}
for _, v := range cp.Transformed() {
vertices := cp.Transformed()
for _, v := range vertices {
pos.Add(v)
}
pos[0] /= float64(len(cp.Transformed()))
pos[1] /= float64(len(cp.Transformed()))
denom := float64(len(vertices))
pos[0] /= denom
pos[1] /= denom
return pos
@@ -305,10 +304,10 @@ func (cp *ConvexPolygon) Project(axis Vector) Projection {
// SATAxes returns the axes of the ConvexPolygon for SAT intersection testing.
func (cp *ConvexPolygon) SATAxes() []Vector {
axes := []Vector{}
for _, line := range cp.Lines() {
axes = append(axes, line.Normal())
lines := cp.Lines()
axes := make([]Vector, len(lines))
for i, line := range lines {
axes[i] = line.Normal()
}
return axes

View File

@@ -30,7 +30,20 @@ func NewSpace(spaceWidth, spaceHeight, cellWidth, cellHeight int) *Space {
}
// [WARNING] The slice type boxing/unboxing is proved by profiling to be heavy after transpiled to JavaScript, thus adding some "XxxSingle" shortcuts here.
// Add adds the specified Objects to the Space, updating the Space's cells to refer to the Object.
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()
}
func (sp *Space) Add(objects ...*Object) {
if sp == nil {
@@ -50,6 +63,20 @@ func (sp *Space) Add(objects ...*Object) {
// Remove removes the specified Objects from being associated with the Space. This should be done whenever an Object is removed from the
// game.
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
}
func (sp *Space) Remove(objects ...*Object) {
if sp == nil {
@@ -57,13 +84,11 @@ func (sp *Space) Remove(objects ...*Object) {
}
for _, obj := range objects {
for _, cell := range obj.TouchingCells {
for 0 < obj.TouchingCells.Cnt {
cell := obj.TouchingCells.Pop().(*Cell)
cell.unregister(obj)
}
obj.TouchingCells = []*Cell{}
obj.Space = nil
}
@@ -80,16 +105,14 @@ func (sp *Space) Objects() []*Object {
for cy := range sp.Cells {
for cx := range sp.Cells[cy] {
for _, o := range sp.Cells[cy][cx].Objects {
rb := sp.Cells[cy][cx].Objects
for i := rb.StFrameId; i < rb.EdFrameId; i++ {
o := rb.GetByFrameId(i).(*Object)
if _, added := objectsAdded[o]; !added {
objects = append(objects, o)
objectsAdded[o] = true
}
}
}
}
@@ -100,19 +123,13 @@ func (sp *Space) Objects() []*Object {
// Resize resizes the internal Cells array.
func (sp *Space) Resize(width, height int) {
sp.Cells = [][]*Cell{}
sp.Cells = make([][]*Cell, height)
for y := 0; y < height; y++ {
sp.Cells = append(sp.Cells, []*Cell{})
sp.Cells[y] = make([]*Cell, width)
for x := 0; x < width; x++ {
sp.Cells[y] = append(sp.Cells[y], newCell(x, y))
sp.Cells[y][x] = newCell(x, y)
}
}
}
// 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
@@ -137,25 +154,23 @@ func (sp *Space) CheckCells(x, y, w, h int, tags ...string) *Object {
cell := sp.Cell(ix, iy)
if cell != nil {
rb := cell.Objects
if len(tags) > 0 {
if cell.ContainsTags(tags...) {
for _, obj := range cell.Objects {
for i := rb.StFrameId; i < rb.EdFrameId; i++ {
obj := rb.GetByFrameId(i).(*Object)
if obj.HasTags(tags...) {
return obj
}
}
}
} else if cell.Occupied() {
return cell.Objects[0]
return rb.GetByFrameId(rb.StFrameId).(*Object)
}
}
}
}
return nil
@@ -178,10 +193,13 @@ func (sp *Space) CheckCellsWorld(x, y, w, h float64, tags ...string) *Object {
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]
sp.Remove(cell.Objects...)
rb := cell.Objects
for i := rb.StFrameId; i < rb.EdFrameId; i++ {
o := rb.GetByFrameId(i).(*Object)
sp.RemoveSingle(o)
}
}
}