13c57c98500101653d346c82599259cb5fa1d120
1 (* Copyright (C) DooM 2D:Forever Developers
3 * This program is free software: you can redistribute it and/or modify
4 * it under the terms of the GNU General Public License as published by
5 * the Free Software Foundation, either version 3 of the License, or
6 * (at your option) any later version.
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU General Public License for more details.
13 * You should have received a copy of the GNU General Public License
14 * along with this program. If not, see <http://www.gnu.org/licenses/>.
16 // universal spatial grid
17 {$INCLUDE ../shared/a_modes.inc}
24 TBodyProxyId
= Integer;
26 generic TBodyGridBase
<ITP
> = class(TObject
)
28 type TGridQueryCB
= function (obj
: ITP
; tag
: Integer): Boolean is nested
; // return `true` to stop
29 type TGridRayQueryCB
= function (obj
: ITP
; tag
: Integer; x
, y
, prevx
, prevy
: Integer): Boolean is nested
; // return `true` to stop
31 const TagDisabled
= $40000000;
35 GridDefaultTileSize
= 32;
36 GridCellBucketSize
= 8; // WARNING! can't be less than 2!
40 PBodyProxyRec
= ^TBodyProxyRec
;
41 TBodyProxyRec
= record
43 mX
, mY
, mWidth
, mHeight
: Integer; // aabb
44 mQueryMark
: LongWord; // was this object visited at this query?
47 nextLink
: TBodyProxyId
; // next free or nothing
50 procedure setup (aX
, aY
, aWidth
, aHeight
: Integer; aObj
: ITP
; aTag
: Integer);
53 PGridCell
= ^TGridCell
;
55 bodies
: array [0..GridCellBucketSize
-1] of Integer; // -1: end of list
56 next
: Integer; // in this cell; index in mCells
59 TGridInternalCB
= function (grida
: Integer): Boolean of object; // return `true` to stop
63 mMinX
, mMinY
: Integer; // so grids can start at any origin
64 mWidth
, mHeight
: Integer; // in tiles
65 mGrid
: array of Integer; // mWidth*mHeight, index in mCells
66 mCells
: array of TGridCell
; // cell pool
67 mFreeCell
: Integer; // first free cell index or -1
70 mProxies
: array of TBodyProxyRec
;
71 mProxyFree
: TBodyProxyId
; // free
72 mProxyCount
: Integer; // currently used
73 mProxyMaxCount
: Integer;
75 mUData
: TBodyProxyId
; // for inserter/remover
76 mTagMask
: Integer; // for iterator
77 mItCB
: TGridQueryCB
; // for iterator
80 function allocCell
: Integer;
81 procedure freeCell (idx
: Integer); // `next` is simply overwritten
83 function allocProxy (aX
, aY
, aWidth
, aHeight
: Integer; aObj
: ITP
; aTag
: Integer): TBodyProxyId
;
84 procedure freeProxy (body
: TBodyProxyId
);
86 procedure insert (body
: TBodyProxyId
);
87 procedure remove (body
: TBodyProxyId
);
89 function forGridRect (x
, y
, w
, h
: Integer; cb
: TGridInternalCB
): Boolean;
91 function inserter (grida
: Integer): Boolean;
92 function remover (grida
: Integer): Boolean;
94 function getProxyEnabled (pid
: TBodyProxyId
): Boolean; inline;
95 procedure setProxyEnabled (pid
: TBodyProxyId
; val
: Boolean); inline;
98 constructor Create (aMinPixX
, aMinPixY
, aPixWidth
, aPixHeight
: Integer; aTileSize
: Integer=GridDefaultTileSize
);
99 destructor Destroy (); override;
101 function insertBody (aObj
: ITP
; ax
, ay
, aWidth
, aHeight
: Integer; aTag
: Integer=0): TBodyProxyId
;
102 procedure removeBody (aObj
: TBodyProxyId
); // WARNING! this WILL destroy proxy!
104 procedure moveBody (body
: TBodyProxyId
; dx
, dy
: Integer);
105 procedure resizeBody (body
: TBodyProxyId
; sx
, sy
: Integer);
106 procedure moveResizeBody (body
: TBodyProxyId
; dx
, dy
, sx
, sy
: Integer);
108 function insideGrid (x
, y
: Integer): Boolean; inline;
110 //WARNING: can't do recursive queries
111 // no callback: return `true` on the first hit
112 function forEachInAABB (x
, y
, w
, h
: Integer; cb
: TGridQueryCB
; tagmask
: Integer=-1): Boolean;
114 //WARNING: can't do recursive queries
115 // no callback: return `true` on the first hit
116 function forEachAtPoint (x
, y
: Integer; cb
: TGridQueryCB
; tagmask
: Integer=-1): Boolean;
118 //WARNING: can't do recursive queries
119 // cb with `(nil)` will be called before processing new tile
120 // no callback: return `true` on the nearest hit
121 function traceRay (x0
, y0
, x1
, y1
: Integer; cb
: TGridRayQueryCB
; tagmask
: Integer=-1): Boolean; overload
;
122 function traceRay (out ex
, ey
: Integer; x0
, y0
, x1
, y1
: Integer; cb
: TGridRayQueryCB
; tagmask
: Integer=-1): Boolean;
124 procedure dumpStats ();
126 //WARNING! no sanity checks!
127 property proxyEnabled
[pid
: TBodyProxyId
]: Boolean read getProxyEnabled write setProxyEnabled
;
137 // ////////////////////////////////////////////////////////////////////////// //
138 procedure TBodyGridBase
.TBodyProxyRec
.setup (aX
, aY
, aWidth
, aHeight
: Integer; aObj
: ITP
; aTag
: Integer);
151 // ////////////////////////////////////////////////////////////////////////// //
152 constructor TBodyGridBase
.Create (aMinPixX
, aMinPixY
, aPixWidth
, aPixHeight
: Integer; aTileSize
: Integer=GridDefaultTileSize
);
156 if aTileSize
< 1 then aTileSize
:= 1;
157 if aTileSize
> 8192 then aTileSize
:= 8192; // arbitrary limit
158 if aPixWidth
< aTileSize
then aPixWidth
:= aTileSize
;
159 if aPixHeight
< aTileSize
then aPixHeight
:= aTileSize
;
160 mTileSize
:= aTileSize
;
163 mWidth
:= (aPixWidth
+aTileSize
-1) div aTileSize
;
164 mHeight
:= (aPixHeight
+aTileSize
-1) div aTileSize
;
165 SetLength(mGrid
, mWidth
*mHeight
);
166 SetLength(mCells
, mWidth
*mHeight
);
167 SetLength(mProxies
, 8192);
170 for idx
:= 0 to High(mCells
) do
172 mCells
[idx
].bodies
[0] := -1;
173 mCells
[idx
].next
:= idx
+1;
175 mCells
[High(mCells
)].next
:= -1; // last cell
177 for idx
:= 0 to High(mGrid
) do mGrid
[idx
] := -1;
179 for idx
:= 0 to High(mProxies
) do mProxies
[idx
].nextLink
:= idx
+1;
180 mProxies
[High(mProxies
)].nextLink
:= -1;
189 e_WriteLog(Format('created grid with size: %dx%d (tile size: %d); pix: %dx%d', [mWidth
, mHeight
, mTileSize
, mWidth
*mTileSize
, mHeight
*mTileSize
]), MSG_NOTIFY
);
193 destructor TBodyGridBase
.Destroy ();
202 procedure TBodyGridBase
.dumpStats ();
204 idx
, mcb
, cidx
, cnt
: Integer;
207 for idx
:= 0 to High(mGrid
) do
214 cidx
:= mCells
[cidx
].next
;
216 if (mcb
< cnt
) then mcb
:= cnt
;
218 e_WriteLog(Format('grid size: %dx%d (tile size: %d); pix: %dx%d; used cells: %d; max bodies in cell: %d; max proxies allocated: %d; proxies used: %d', [mWidth
, mHeight
, mTileSize
, mWidth
*mTileSize
, mHeight
*mTileSize
, mUsedCells
, mcb
, mProxyMaxCount
, mProxyCount
]), MSG_NOTIFY
);
222 function TBodyGridBase
.insideGrid (x
, y
: Integer): Boolean; inline;
227 result
:= (x
>= 0) and (y
>= 0) and (x
< mWidth
*mTileSize
) and (y
< mHeight
*mTileSize
);
231 function TBodyGridBase
.getProxyEnabled (pid
: TBodyProxyId
): Boolean; inline;
233 if (pid
>= 0) then result
:= ((mProxies
[pid
].mTag
and TagDisabled
) = 0) else result
:= false;
237 procedure TBodyGridBase
.setProxyEnabled (pid
: TBodyProxyId
; val
: Boolean); inline;
243 mProxies
[pid
].mTag
:= mProxies
[pid
].mTag
and not TagDisabled
;
247 mProxies
[pid
].mTag
:= mProxies
[pid
].mTag
or TagDisabled
253 function TBodyGridBase
.allocCell
: Integer;
257 if (mFreeCell
< 0) then
259 // no free cells, want more
260 mFreeCell
:= Length(mCells
);
261 SetLength(mCells
, mFreeCell
+32768); // arbitrary number
262 for idx
:= mFreeCell
to High(mCells
) do
264 mCells
[idx
].bodies
[0] := -1;
265 mCells
[idx
].next
:= idx
+1;
267 mCells
[High(mCells
)].next
:= -1; // last cell
270 mFreeCell
:= mCells
[result
].next
;
271 mCells
[result
].next
:= -1;
272 mCells
[result
].bodies
[0] := -1;
274 //e_WriteLog(Format('grid: allocated new cell #%d (total: %d)', [result, mUsedCells]), MSG_NOTIFY);
278 procedure TBodyGridBase
.freeCell (idx
: Integer);
280 if (idx
>= 0) and (idx
< Length(mCells
)) then
282 //if mCells[idx].body = -1 then exit; // the thing that should not be
283 mCells
[idx
].bodies
[0] := -1;
284 mCells
[idx
].next
:= mFreeCell
;
291 function TBodyGridBase
.allocProxy (aX
, aY
, aWidth
, aHeight
: Integer; aObj
: ITP
; aTag
: Integer): TBodyProxyId
;
296 if (mProxyFree
= -1) then
298 // no free proxies, resize list
299 olen
:= Length(mProxies
);
300 SetLength(mProxies
, olen
+8192); // arbitrary number
301 for idx
:= olen
to High(mProxies
) do mProxies
[idx
].nextLink
:= idx
+1;
302 mProxies
[High(mProxies
)].nextLink
:= -1;
306 result
:= mProxyFree
;
307 px
:= @mProxies
[result
];
308 mProxyFree
:= px
.nextLink
;
309 px
.setup(aX
, aY
, aWidth
, aHeight
, aObj
, aTag
);
314 if (mProxyMaxCount
< mProxyCount
) then mProxyMaxCount
:= mProxyCount
;
317 procedure TBodyGridBase
.freeProxy (body
: TBodyProxyId
);
319 if (body
< 0) or (body
> High(mProxies
)) then exit
; // just in case
320 if (mProxyCount
= 0) then raise Exception
.Create('wutafuuuuu in grid (no allocated proxies, what i should free now?)');
322 mProxies
[body
].mObj
:= nil;
323 mProxies
[body
].nextLink
:= mProxyFree
;
329 function TBodyGridBase
.forGridRect (x
, y
, w
, h
: Integer; cb
: TGridInternalCB
): Boolean;
332 gw
, gh
, tsize
: Integer;
335 if (w
< 1) or (h
< 1) or not assigned(cb
) then exit
;
340 if (x
+w
<= 0) or (y
+h
<= 0) then exit
;
344 if (x
>= gw
*tsize
) or (y
>= gh
*tsize
) then exit
;
345 for gy
:= y
div tsize
to (y
+h
-1) div tsize
do
347 if (gy
< 0) then continue
;
348 if (gy
>= gh
) then break
;
349 for gx
:= x
div tsize
to (x
+w
-1) div tsize
do
351 if (gx
< 0) then continue
;
352 if (gx
>= gw
) then break
;
353 result
:= cb(gy
*gw
+gx
);
360 function TBodyGridBase
.inserter (grida
: Integer): Boolean;
367 result
:= false; // never stop
368 // add body to the given grid cell
374 for f
:= 0 to High(TGridCell
.bodies
) do
376 if (pi
.bodies
[f
] = -1) then
379 pi
.bodies
[f
] := mUData
;
380 if (f
+1 < Length(TGridCell
.bodies
)) then pi
.bodies
[f
+1] := -1;
385 // either no room, or no cell at all
387 mCells
[cidx
].bodies
[0] := mUData
;
388 mCells
[cidx
].bodies
[1] := -1;
389 mCells
[cidx
].next
:= pc
;
390 mGrid
[grida
] := cidx
;
394 procedure TBodyGridBase
.insert (body
: TBodyProxyId
);
398 if (body
< 0) or (body
> High(mProxies
)) then exit
; // just in case
399 px
:= @mProxies
[body
];
401 forGridRect(px
.mX
, px
.mY
, px
.mWidth
, px
.mHeight
, inserter
);
405 function TBodyGridBase
.remover (grida
: Integer): Boolean;
408 pidx
, idx
, tmp
: Integer;
411 result
:= false; // never stop
412 // find and remove cell
417 tmp
:= mCells
[idx
].next
;
420 while (f
< High(TGridCell
.bodies
)) do
422 if (pc
.bodies
[f
] = mUData
) then
425 if (f
= 0) and (pc
.bodies
[1] = -1) then
427 // this cell contains no elements, remove it
428 tmp
:= mCells
[idx
].next
;
429 if (pidx
= -1) then mGrid
[grida
] := tmp
else mCells
[pidx
].next
:= tmp
;
434 // remove element from bucket
436 while (f
< High(TGridCell
.bodies
)) do
438 pc
.bodies
[f
-1] := pc
.bodies
[f
];
439 if (pc
.bodies
[f
] = -1) then break
;
442 pc
.bodies
[High(TGridCell
.bodies
)] := -1; // just in case
444 exit
; // assume that we cannot have one object added to bucket twice
454 // absolutely not tested
455 procedure TBodyGridBase
.remove (body
: TBodyProxyId
);
459 if (body
< 0) or (body
> High(mProxies
)) then exit
; // just in case
460 px
:= @mProxies
[body
];
462 forGridRect(px
.mX
, px
.mY
, px
.mWidth
, px
.mHeight
, remover
);
466 function TBodyGridBase
.insertBody (aObj
: ITP
; aX
, aY
, aWidth
, aHeight
: Integer; aTag
: Integer=0): TBodyProxyId
;
468 result
:= allocProxy(aX
, aY
, aWidth
, aHeight
, aObj
, aTag
);
473 procedure TBodyGridBase
.removeBody (aObj
: TBodyProxyId
);
475 if (aObj
< 0) or (aObj
> High(mProxies
)) then exit
; // just in case
481 procedure TBodyGridBase
.moveResizeBody (body
: TBodyProxyId
; dx
, dy
, sx
, sy
: Integer);
485 if (body
< 0) or (body
> High(mProxies
)) then exit
; // just in case
486 if ((dx
= 0) and (dy
= 0) and (sx
= 0) and (sy
= 0)) then exit
;
488 px
:= @mProxies
[body
];
496 procedure TBodyGridBase
.moveBody (body
: TBodyProxyId
; dx
, dy
: Integer);
498 moveResizeBody(body
, dx
, dy
, 0, 0);
501 procedure TBodyGridBase
.resizeBody (body
: TBodyProxyId
; sx
, sy
: Integer);
503 moveResizeBody(body
, 0, 0, sx
, sy
);
507 // ////////////////////////////////////////////////////////////////////////// //
508 // no callback: return `true` on the first hit
509 function TBodyGridBase
.forEachAtPoint (x
, y
: Integer; cb
: TGridQueryCB
; tagmask
: Integer=-1): Boolean;
519 if (tagmask
= 0) then exit
;
521 // make coords (0,0)-based
524 if (x
< 0) or (y
< 0) or (x
>= mWidth
*mTileSize
) or (y
>= mHeight
*mTileSize
) then exit
;
526 curci
:= mGrid
[(y
div mTileSize
)*mWidth
+(x
div mTileSize
)];
531 // increase query counter
533 if (mLastQuery
= 0) then
535 // just in case of overflow
537 for idx
:= 0 to High(mProxies
) do mProxies
[idx
].mQueryMark
:= 0;
541 while (curci
<> -1) do
543 cc
:= @mCells
[curci
];
544 for f
:= 0 to High(TGridCell
.bodies
) do
546 if (cc
.bodies
[f
] = -1) then break
;
547 px
:= @mProxies
[cc
.bodies
[f
]];
548 if (px
.mQueryMark
<> lq
) then
551 if ((ptag
and TagDisabled
) = 0) and ((px
.mTag
and tagmask
) <> 0) then
553 if (x
>= px
.mX
) and (y
>= px
.mY
) and (x
< px
.mX
+px
.mWidth
) and (y
< px
.mY
+px
.mHeight
) then
556 if assigned(cb
) then result
:= cb(px
.mObj
, px
.mTag
) else result
:= true;
567 // no callback: return `true` on the first hit
568 function TBodyGridBase
.forEachInAABB (x
, y
, w
, h
: Integer; cb
: TGridQueryCB
; tagmask
: Integer=-1): Boolean;
582 if (w
< 1) or (h
< 1) then exit
;
594 if (x
+w
<= 0) or (y
+h
<= 0) then exit
;
595 if (x
>= gw
*tsize
) or (y
>= mHeight
*tsize
) then exit
;
597 // increase query counter
599 if (mLastQuery
= 0) then
601 // just in case of overflow
603 for idx
:= 0 to High(mProxies
) do mProxies
[idx
].mQueryMark
:= 0;
605 //e_WriteLog(Format('grid: query #%d: (%d,%d)-(%dx%d)', [mLastQuery, minx, miny, maxx, maxy]), MSG_NOTIFY);
609 for gy
:= y
div tsize
to (y
+h
-1) div tsize
do
611 if (gy
< 0) then continue
;
612 if (gy
>= mHeight
) then break
;
613 for gx
:= x
div tsize
to (x
+w
-1) div tsize
do
615 if (gx
< 0) then continue
;
616 if (gx
>= gw
) then break
;
618 curci
:= mGrid
[gy
*gw
+gx
];
619 while (curci
<> -1) do
621 cc
:= @mCells
[curci
];
622 for f
:= 0 to High(TGridCell
.bodies
) do
624 if (cc
.bodies
[f
] = -1) then break
;
625 px
:= @mProxies
[cc
.bodies
[f
]];
626 if (px
.mQueryMark
<> lq
) then
629 if ((ptag
and TagDisabled
) = 0) and ((px
.mTag
and tagmask
) <> 0) then
631 if (x0
>= px
.mX
+px
.mWidth
) or (y0
>= px
.mY
+px
.mHeight
) then continue
;
632 if (x0
+w
<= px
.mX
) or (y0
+h
<= px
.mY
) then continue
;
634 if assigned(cb
) then result
:= cb(px
.mObj
, px
.mTag
) else result
:= true;
646 // ////////////////////////////////////////////////////////////////////////// //
647 // no callback: return `true` on the nearest hit
648 function TBodyGridBase
.traceRay (x0
, y0
, x1
, y1
: Integer; cb
: TGridRayQueryCB
; tagmask
: Integer=-1): Boolean;
652 result
:= traceRay(ex
, ey
, x0
, y0
, x1
, y1
, cb
, tagmask
);
656 // no callback: return `true` on the nearest hit
657 function TBodyGridBase
.traceRay (out ex
, ey
: Integer; x0
, y0
, x1
, y1
: Integer; cb
: TGridRayQueryCB
; tagmask
: Integer=-1): Boolean;
667 tsize
: Integer; // tile size
669 lastGA
: Integer = -1;
670 ga
: Integer = -1; // last used grid address
678 prevX
, prevY
: Integer;
681 lastDistSq
, distSq
: Integer;
682 wasHit
: Boolean = false;
686 if (tagmask
= 0) then begin ex
:= x0
; ey
:= y0
; exit
; end;
688 // make coords (0,0)-based
699 if (dx
> 0) then incx
:= 1 else if (dx
< 0) then incx
:= -1 else incx
:= 0;
700 if (dy
> 0) then incy
:= 1 else if (dy
< 0) then incy
:= -1 else incy
:= 0;
705 if (dx
> dy
) then d
:= dx
else d
:= dy
;
710 // increase query counter
712 if (mLastQuery
= 0) then
714 // just in case of overflow
716 for i
:= 0 to High(mProxies
) do mProxies
[i
].mQueryMark
:= 0;
725 lastDistSq
:= (x1
-x0
)*(x1
-x0
)+(y1
-y0
)*(y1
-y0
)+1;
731 Inc(xerr
, dx
); if (xerr
>= d
) then begin Dec(xerr
, d
); Inc(x
, incx
); end;
732 Inc(yerr
, dy
); if (yerr
>= d
) then begin Dec(yerr
, d
); Inc(y
, incy
); end;
734 if (x
>= 0) and (y
>= 0) and (x
<= maxx
) and (y
<= maxy
) then
736 ga
:= (y
div tsize
)*gw
+(x
div tsize
);
737 if (lastGA
<> ga
) then
741 ccidx
:= mGrid
[lastGA
];
746 if (ccidx
<> -1) then
749 if assigned(cb
) then result
:= cb(nil, 0, x
+minx
, y
+miny
, prevX
, prevY
) else result
:= wasHit
;
754 if (ccidx
<> -1) then
760 while (curci
<> -1) do
762 cc
:= @mCells
[curci
];
763 for f
:= 0 to High(TGridCell
.bodies
) do
765 if (cc
.bodies
[f
] = -1) then break
;
766 px
:= @mProxies
[cc
.bodies
[f
]];
767 if (px
.mQueryMark
<> lq
) then
770 if ((ptag
and TagDisabled
) = 0) and ((px
.mTag
and tagmask
) <> 0) then
772 if (x
>= px
.mX
) and (y
>= px
.mY
) and (x
< px
.mX
+px
.mWidth
) and (y
< px
.mY
+px
.mHeight
) then
777 result
:= cb(px
.mObj
, px
.mTag
, x
, y
, prevX
, prevY
);
778 if result
then begin ex
:= prevX
; ey
:= prevY
; exit
; end;
782 distSq
:= (prevX
-x
)*(prevX
-x
)+(prevY
-y
)*(prevY
-y
);
783 if (distSq
< lastDistSq
) then
786 lastDistSq
:= distSq
;
801 if not hasUntried
then
803 // don't process this cell anymore
805 if assigned(cb
) then result
:= cb(nil, 0, x
, y
, prevX
, prevY
) else result
:= wasHit
;
806 if result
then exit
; // don't update lasthit: it is done in real checker