DEADSOFTWARE

e45fede6f34b818d9a9f41191fc0d0c8bef8e32b
[d2df-sdl.git] / src / game / g_grid.pas
1 (* Copyright (C) DooM 2D:Forever Developers
2 *
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.
7 *
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.
12 *
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/>.
15 *)
16 // universal spatial grid
17 {$INCLUDE ../shared/a_modes.inc}
18 unit g_grid;
20 interface
23 type
24 TBodyProxyId = Integer;
26 generic TBodyGridBase<ITP> = class(TObject)
27 public
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;
32 const TagFullMask = $3fffffff;
34 private
35 const
36 GridDefaultTileSize = 32;
37 GridCellBucketSize = 8; // WARNING! can't be less than 2!
39 private
40 type
41 PBodyProxyRec = ^TBodyProxyRec;
42 TBodyProxyRec = record
43 private
44 mX, mY, mWidth, mHeight: Integer; // aabb
45 mQueryMark: LongWord; // was this object visited at this query?
46 mObj: ITP;
47 mTag: Integer; // `TagDisabled` set: disabled ;-)
48 nextLink: TBodyProxyId; // next free or nothing
50 private
51 procedure setup (aX, aY, aWidth, aHeight: Integer; aObj: ITP; aTag: Integer);
52 end;
54 PGridCell = ^TGridCell;
55 TGridCell = record
56 bodies: array [0..GridCellBucketSize-1] of Integer; // -1: end of list
57 next: Integer; // in this cell; index in mCells
58 end;
60 TGridInternalCB = function (grida: Integer; bodyId: TBodyProxyId): Boolean of object; // return `true` to stop
62 private
63 //mTileSize: Integer;
64 const mTileSize = GridDefaultTileSize;
66 private
67 mMinX, mMinY: Integer; // so grids can start at any origin
68 mWidth, mHeight: Integer; // in tiles
69 mGrid: array of Integer; // mWidth*mHeight, index in mCells
70 mCells: array of TGridCell; // cell pool
71 mFreeCell: Integer; // first free cell index or -1
72 mLastQuery: LongWord;
73 mUsedCells: Integer;
74 mProxies: array of TBodyProxyRec;
75 mProxyFree: TBodyProxyId; // free
76 mProxyCount: Integer; // currently used
77 mProxyMaxCount: Integer;
79 private
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 insertInternal (body: TBodyProxyId);
87 procedure removeInternal (body: TBodyProxyId);
89 function forGridRect (x, y, w, h: Integer; cb: TGridInternalCB; bodyId: TBodyProxyId): Boolean;
91 function inserter (grida: Integer; bodyId: TBodyProxyId): Boolean;
92 function remover (grida: Integer; bodyId: TBodyProxyId): Boolean;
94 function getProxyEnabled (pid: TBodyProxyId): Boolean; inline;
95 procedure setProxyEnabled (pid: TBodyProxyId; val: Boolean); inline;
97 public
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=-1): 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: don't modify grid while any query is in progress (no checks are made!)
111 // you can set enabled/disabled flag, tho (but iterator can still return objects disabled inside it)
112 // no callback: return `true` on the first hit
113 function forEachInAABB (x, y, w, h: Integer; cb: TGridQueryCB; tagmask: Integer=-1; allowDisabled: Boolean=false): ITP;
115 //WARNING: don't modify grid while any query is in progress (no checks are made!)
116 // you can set enabled/disabled flag, tho (but iterator can still return objects disabled inside it)
117 // no callback: return `true` on the first hit
118 function forEachAtPoint (x, y: Integer; cb: TGridQueryCB; tagmask: Integer=-1): ITP;
120 //WARNING: don't modify grid while any query is in progress (no checks are made!)
121 // you can set enabled/disabled flag, tho (but iterator can still return objects disabled inside it)
122 // cb with `(nil)` will be called before processing new tile
123 // no callback: return `true` on the nearest hit
124 function traceRay (x0, y0, x1, y1: Integer; cb: TGridRayQueryCB; tagmask: Integer=-1): ITP; overload;
125 function traceRay (out ex, ey: Integer; x0, y0, x1, y1: Integer; cb: TGridRayQueryCB; tagmask: Integer=-1): ITP;
127 procedure dumpStats ();
129 //WARNING! no sanity checks!
130 property proxyEnabled[pid: TBodyProxyId]: Boolean read getProxyEnabled write setProxyEnabled;
131 end;
134 implementation
136 uses
137 SysUtils, e_log;
140 // ////////////////////////////////////////////////////////////////////////// //
141 procedure TBodyGridBase.TBodyProxyRec.setup (aX, aY, aWidth, aHeight: Integer; aObj: ITP; aTag: Integer);
142 begin
143 mX := aX;
144 mY := aY;
145 mWidth := aWidth;
146 mHeight := aHeight;
147 mQueryMark := 0;
148 mObj := aObj;
149 mTag := aTag;
150 nextLink := -1;
151 end;
154 // ////////////////////////////////////////////////////////////////////////// //
155 constructor TBodyGridBase.Create (aMinPixX, aMinPixY, aPixWidth, aPixHeight: Integer{; aTileSize: Integer=GridDefaultTileSize});
156 var
157 idx: Integer;
158 begin
160 if aTileSize < 1 then aTileSize := 1;
161 if aTileSize > 8192 then aTileSize := 8192; // arbitrary limit
162 mTileSize := aTileSize;
164 if (aPixWidth < mTileSize) then aPixWidth := mTileSize;
165 if (aPixHeight < mTileSize) then aPixHeight := mTileSize;
166 mMinX := aMinPixX;
167 mMinY := aMinPixY;
168 mWidth := (aPixWidth+mTileSize-1) div mTileSize;
169 mHeight := (aPixHeight+mTileSize-1) div mTileSize;
170 SetLength(mGrid, mWidth*mHeight);
171 SetLength(mCells, mWidth*mHeight);
172 SetLength(mProxies, 8192);
173 mFreeCell := 0;
174 // init free list
175 for idx := 0 to High(mCells) do
176 begin
177 mCells[idx].bodies[0] := -1;
178 mCells[idx].next := idx+1;
179 end;
180 mCells[High(mCells)].next := -1; // last cell
181 // init grid
182 for idx := 0 to High(mGrid) do mGrid[idx] := -1;
183 // init proxies
184 for idx := 0 to High(mProxies) do mProxies[idx].nextLink := idx+1;
185 mProxies[High(mProxies)].nextLink := -1;
186 mLastQuery := 0;
187 mUsedCells := 0;
188 mProxyFree := 0;
189 mProxyCount := 0;
190 mProxyMaxCount := 0;
191 e_WriteLog(Format('created grid with size: %dx%d (tile size: %d); pix: %dx%d', [mWidth, mHeight, mTileSize, mWidth*mTileSize, mHeight*mTileSize]), MSG_NOTIFY);
192 end;
195 destructor TBodyGridBase.Destroy ();
196 begin
197 mCells := nil;
198 mGrid := nil;
199 mProxies := nil;
200 inherited;
201 end;
204 procedure TBodyGridBase.dumpStats ();
205 var
206 idx, mcb, cidx, cnt: Integer;
207 begin
208 mcb := 0;
209 for idx := 0 to High(mGrid) do
210 begin
211 cidx := mGrid[idx];
212 cnt := 0;
213 while cidx >= 0 do
214 begin
215 Inc(cnt);
216 cidx := mCells[cidx].next;
217 end;
218 if (mcb < cnt) then mcb := cnt;
219 end;
220 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);
221 end;
224 function TBodyGridBase.insideGrid (x, y: Integer): Boolean; inline;
225 begin
226 // fix coords
227 Dec(x, mMinX);
228 Dec(y, mMinY);
229 result := (x >= 0) and (y >= 0) and (x < mWidth*mTileSize) and (y < mHeight*mTileSize);
230 end;
233 function TBodyGridBase.getProxyEnabled (pid: TBodyProxyId): Boolean; inline;
234 begin
235 if (pid >= 0) then result := ((mProxies[pid].mTag and TagDisabled) = 0) else result := false;
236 end;
239 procedure TBodyGridBase.setProxyEnabled (pid: TBodyProxyId; val: Boolean); inline;
240 begin
241 if (pid >= 0) then
242 begin
243 if val then
244 begin
245 mProxies[pid].mTag := mProxies[pid].mTag and not TagDisabled;
246 end
247 else
248 begin
249 mProxies[pid].mTag := mProxies[pid].mTag or TagDisabled;
250 end;
251 end;
252 end;
255 function TBodyGridBase.allocCell: Integer;
256 var
257 idx: Integer;
258 begin
259 if (mFreeCell < 0) then
260 begin
261 // no free cells, want more
262 mFreeCell := Length(mCells);
263 SetLength(mCells, mFreeCell+32768); // arbitrary number
264 for idx := mFreeCell to High(mCells) do
265 begin
266 mCells[idx].bodies[0] := -1;
267 mCells[idx].next := idx+1;
268 end;
269 mCells[High(mCells)].next := -1; // last cell
270 end;
271 result := mFreeCell;
272 mFreeCell := mCells[result].next;
273 mCells[result].next := -1;
274 mCells[result].bodies[0] := -1;
275 Inc(mUsedCells);
276 //e_WriteLog(Format('grid: allocated new cell #%d (total: %d)', [result, mUsedCells]), MSG_NOTIFY);
277 end;
280 procedure TBodyGridBase.freeCell (idx: Integer);
281 begin
282 if (idx >= 0) and (idx < Length(mCells)) then
283 begin
284 //if mCells[idx].body = -1 then exit; // the thing that should not be
285 mCells[idx].bodies[0] := -1;
286 mCells[idx].next := mFreeCell;
287 mFreeCell := idx;
288 Dec(mUsedCells);
289 end;
290 end;
293 function TBodyGridBase.allocProxy (aX, aY, aWidth, aHeight: Integer; aObj: ITP; aTag: Integer): TBodyProxyId;
294 var
295 olen, idx: Integer;
296 px: PBodyProxyRec;
297 begin
298 if (mProxyFree = -1) then
299 begin
300 // no free proxies, resize list
301 olen := Length(mProxies);
302 SetLength(mProxies, olen+8192); // arbitrary number
303 for idx := olen to High(mProxies) do mProxies[idx].nextLink := idx+1;
304 mProxies[High(mProxies)].nextLink := -1;
305 mProxyFree := olen;
306 end;
307 // get one from list
308 result := mProxyFree;
309 px := @mProxies[result];
310 mProxyFree := px.nextLink;
311 px.setup(aX, aY, aWidth, aHeight, aObj, aTag);
312 // add to used list
313 px.nextLink := -1;
314 // statistics
315 Inc(mProxyCount);
316 if (mProxyMaxCount < mProxyCount) then mProxyMaxCount := mProxyCount;
317 end;
319 procedure TBodyGridBase.freeProxy (body: TBodyProxyId);
320 begin
321 if (body < 0) or (body > High(mProxies)) then exit; // just in case
322 if (mProxyCount = 0) then raise Exception.Create('wutafuuuuu in grid (no allocated proxies, what i should free now?)');
323 // add to free list
324 mProxies[body].mObj := nil;
325 mProxies[body].nextLink := mProxyFree;
326 mProxyFree := body;
327 Dec(mProxyCount);
328 end;
331 function TBodyGridBase.forGridRect (x, y, w, h: Integer; cb: TGridInternalCB; bodyId: TBodyProxyId): Boolean;
332 const
333 tsize = mTileSize;
334 var
335 gx, gy: Integer;
336 gw, gh: Integer;
337 begin
338 result := false;
339 if (w < 1) or (h < 1) or not assigned(cb) then exit;
340 // fix coords
341 Dec(x, mMinX);
342 Dec(y, mMinY);
343 // go on
344 if (x+w <= 0) or (y+h <= 0) then exit;
345 gw := mWidth;
346 gh := mHeight;
347 //tsize := mTileSize;
348 if (x >= gw*tsize) or (y >= gh*tsize) then exit;
349 for gy := y div tsize to (y+h-1) div tsize do
350 begin
351 if (gy < 0) then continue;
352 if (gy >= gh) then break;
353 for gx := x div tsize to (x+w-1) div tsize do
354 begin
355 if (gx < 0) then continue;
356 if (gx >= gw) then break;
357 result := cb(gy*gw+gx, bodyId);
358 if result then exit;
359 end;
360 end;
361 end;
364 function TBodyGridBase.inserter (grida: Integer; bodyId: TBodyProxyId): Boolean;
365 var
366 cidx: Integer;
367 pc: Integer;
368 pi: PGridCell;
369 f: Integer;
370 begin
371 result := false; // never stop
372 // add body to the given grid cell
373 pc := mGrid[grida];
374 if (pc <> -1) then
375 begin
376 pi := @mCells[pc];
377 f := 0;
378 for f := 0 to High(TGridCell.bodies) do
379 begin
380 if (pi.bodies[f] = -1) then
381 begin
382 // can add here
383 pi.bodies[f] := bodyId;
384 if (f+1 < Length(TGridCell.bodies)) then pi.bodies[f+1] := -1;
385 exit;
386 end;
387 end;
388 end;
389 // either no room, or no cell at all
390 cidx := allocCell();
391 mCells[cidx].bodies[0] := bodyId;
392 mCells[cidx].bodies[1] := -1;
393 mCells[cidx].next := pc;
394 mGrid[grida] := cidx;
395 end;
397 procedure TBodyGridBase.insertInternal (body: TBodyProxyId);
398 var
399 px: PBodyProxyRec;
400 begin
401 if (body < 0) or (body > High(mProxies)) then exit; // just in case
402 px := @mProxies[body];
403 forGridRect(px.mX, px.mY, px.mWidth, px.mHeight, inserter, body);
404 end;
407 // absolutely not tested
408 function TBodyGridBase.remover (grida: Integer; bodyId: TBodyProxyId): Boolean;
409 var
410 f: Integer;
411 pidx, idx, tmp: Integer;
412 pc: PGridCell;
413 begin
414 result := false; // never stop
415 // find and remove cell
416 pidx := -1;
417 idx := mGrid[grida];
418 while (idx >= 0) do
419 begin
420 tmp := mCells[idx].next;
421 pc := @mCells[idx];
422 f := 0;
423 while (f < High(TGridCell.bodies)) do
424 begin
425 if (pc.bodies[f] = bodyId) then
426 begin
427 // i found her!
428 if (f = 0) and (pc.bodies[1] = -1) then
429 begin
430 // this cell contains no elements, remove it
431 tmp := mCells[idx].next;
432 if (pidx = -1) then mGrid[grida] := tmp else mCells[pidx].next := tmp;
433 freeCell(idx);
434 end
435 else
436 begin
437 // remove element from bucket
438 Inc(f);
439 while (f < High(TGridCell.bodies)) do
440 begin
441 pc.bodies[f-1] := pc.bodies[f];
442 if (pc.bodies[f] = -1) then break;
443 Inc(f);
444 end;
445 pc.bodies[High(TGridCell.bodies)] := -1; // just in case
446 end;
447 exit; // assume that we cannot have one object added to bucket twice
448 end;
449 Inc(f);
450 end;
451 pidx := idx;
452 idx := tmp;
453 end;
454 end;
456 // absolutely not tested
457 procedure TBodyGridBase.removeInternal (body: TBodyProxyId);
458 var
459 px: PBodyProxyRec;
460 begin
461 if (body < 0) or (body > High(mProxies)) then exit; // just in case
462 px := @mProxies[body];
463 forGridRect(px.mX, px.mY, px.mWidth, px.mHeight, remover, body);
464 end;
467 function TBodyGridBase.insertBody (aObj: ITP; aX, aY, aWidth, aHeight: Integer; aTag: Integer=-1): TBodyProxyId;
468 begin
469 aTag := aTag and TagFullMask;
470 result := allocProxy(aX, aY, aWidth, aHeight, aObj, aTag);
471 insertInternal(result);
472 end;
475 procedure TBodyGridBase.removeBody (aObj: TBodyProxyId);
476 begin
477 if (aObj < 0) or (aObj > High(mProxies)) then exit; // just in case
478 removeInternal(aObj);
479 freeProxy(aObj);
480 end;
483 procedure TBodyGridBase.moveResizeBody (body: TBodyProxyId; dx, dy, sx, sy: Integer);
484 var
485 px: PBodyProxyRec;
486 begin
487 if (body < 0) or (body > High(mProxies)) then exit; // just in case
488 if ((dx = 0) and (dy = 0) and (sx = 0) and (sy = 0)) then exit;
489 removeInternal(body);
490 px := @mProxies[body];
491 Inc(px.mX, dx);
492 Inc(px.mY, dy);
493 Inc(px.mWidth, sx);
494 Inc(px.mHeight, sy);
495 insertInternal(body);
496 end;
498 procedure TBodyGridBase.moveBody (body: TBodyProxyId; dx, dy: Integer);
499 begin
500 moveResizeBody(body, dx, dy, 0, 0);
501 end;
503 procedure TBodyGridBase.resizeBody (body: TBodyProxyId; sx, sy: Integer);
504 begin
505 moveResizeBody(body, 0, 0, sx, sy);
506 end;
509 // ////////////////////////////////////////////////////////////////////////// //
510 // no callback: return `true` on the first hit
511 function TBodyGridBase.forEachAtPoint (x, y: Integer; cb: TGridQueryCB; tagmask: Integer=-1): ITP;
512 var
513 f: Integer;
514 idx, curci: Integer;
515 cc: PGridCell = nil;
516 px: PBodyProxyRec;
517 lq: LongWord;
518 ptag: Integer;
519 begin
520 result := Default(ITP);
521 tagmask := tagmask and TagFullMask;
522 if (tagmask = 0) then exit;
524 // make coords (0,0)-based
525 Dec(x, mMinX);
526 Dec(y, mMinY);
527 if (x < 0) or (y < 0) or (x >= mWidth*mTileSize) or (y >= mHeight*mTileSize) then exit;
529 curci := mGrid[(y div mTileSize)*mWidth+(x div mTileSize)];
530 // restore coords
531 Inc(x, mMinX);
532 Inc(y, mMinY);
534 // increase query counter
535 Inc(mLastQuery);
536 if (mLastQuery = 0) then
537 begin
538 // just in case of overflow
539 mLastQuery := 1;
540 for idx := 0 to High(mProxies) do mProxies[idx].mQueryMark := 0;
541 end;
542 lq := mLastQuery;
544 while (curci <> -1) do
545 begin
546 cc := @mCells[curci];
547 for f := 0 to High(TGridCell.bodies) do
548 begin
549 if (cc.bodies[f] = -1) then break;
550 px := @mProxies[cc.bodies[f]];
551 ptag := px.mTag;
552 if ((ptag and TagDisabled) = 0) and ((ptag and tagmask) <> 0) and (px.mQueryMark <> lq) then
553 begin
554 if (x >= px.mX) and (y >= px.mY) and (x < px.mX+px.mWidth) and (y < px.mY+px.mHeight) then
555 begin
556 px.mQueryMark := lq;
557 if assigned(cb) then
558 begin
559 if cb(px.mObj, ptag) then begin result := px.mObj; exit; end;
560 end
561 else
562 begin
563 result := px.mObj;
564 exit;
565 end;
566 end;
567 end;
568 end;
569 curci := cc.next;
570 end;
571 end;
574 // no callback: return `true` on the first hit
575 function TBodyGridBase.forEachInAABB (x, y, w, h: Integer; cb: TGridQueryCB; tagmask: Integer=-1; allowDisabled: Boolean=false): ITP;
576 const
577 tsize = mTileSize;
578 var
579 idx: Integer;
580 gx, gy: Integer;
581 curci: Integer;
582 f: Integer;
583 cc: PGridCell = nil;
584 px: PBodyProxyRec;
585 lq: LongWord;
586 gw: Integer;
587 x0, y0: Integer;
588 ptag: Integer;
589 begin
590 result := Default(ITP);
591 if (w < 1) or (h < 1) then exit;
592 tagmask := tagmask and TagFullMask;
593 if (tagmask = 0) then exit;
595 x0 := x;
596 y0 := y;
598 // fix coords
599 Dec(x, mMinX);
600 Dec(y, mMinY);
602 gw := mWidth;
603 //tsize := mTileSize;
605 if (x+w <= 0) or (y+h <= 0) then exit;
606 if (x >= gw*tsize) or (y >= mHeight*tsize) then exit;
608 // increase query counter
609 Inc(mLastQuery);
610 if (mLastQuery = 0) then
611 begin
612 // just in case of overflow
613 mLastQuery := 1;
614 for idx := 0 to High(mProxies) do mProxies[idx].mQueryMark := 0;
615 end;
616 //e_WriteLog(Format('grid: query #%d: (%d,%d)-(%dx%d)', [mLastQuery, minx, miny, maxx, maxy]), MSG_NOTIFY);
617 lq := mLastQuery;
619 // go on
620 for gy := y div tsize to (y+h-1) div tsize do
621 begin
622 if (gy < 0) then continue;
623 if (gy >= mHeight) then break;
624 for gx := x div tsize to (x+w-1) div tsize do
625 begin
626 if (gx < 0) then continue;
627 if (gx >= gw) then break;
628 // process cells
629 curci := mGrid[gy*gw+gx];
630 while (curci <> -1) do
631 begin
632 cc := @mCells[curci];
633 for f := 0 to High(TGridCell.bodies) do
634 begin
635 if (cc.bodies[f] = -1) then break;
636 px := @mProxies[cc.bodies[f]];
637 ptag := px.mTag;
638 if (not allowDisabled) and ((ptag and TagDisabled) <> 0) then continue;
639 if ((ptag and tagmask) <> 0) and (px.mQueryMark <> lq) then
640 //if ((ptag and TagDisabled) = 0) and ((ptag and tagmask) <> 0) and (px.mQueryMark <> lq) then
641 //if ( ((ptag and TagDisabled) = 0) = ignoreDisabled) and ((ptag and tagmask) <> 0) and (px.mQueryMark <> lq) then
642 begin
643 if (x0 >= px.mX+px.mWidth) or (y0 >= px.mY+px.mHeight) then continue;
644 if (x0+w <= px.mX) or (y0+h <= px.mY) then continue;
645 px.mQueryMark := lq;
646 if assigned(cb) then
647 begin
648 if cb(px.mObj, ptag) then begin result := px.mObj; exit; end;
649 end
650 else
651 begin
652 result := px.mObj;
653 exit;
654 end;
655 end;
656 end;
657 curci := cc.next;
658 end;
659 end;
660 end;
661 end;
664 // ////////////////////////////////////////////////////////////////////////// //
665 // no callback: return `true` on the nearest hit
666 function TBodyGridBase.traceRay (x0, y0, x1, y1: Integer; cb: TGridRayQueryCB; tagmask: Integer=-1): ITP;
667 var
668 ex, ey: Integer;
669 begin
670 result := traceRay(ex, ey, x0, y0, x1, y1, cb, tagmask);
671 end;
674 // no callback: return `true` on the nearest hit
675 function TBodyGridBase.traceRay (out ex, ey: Integer; x0, y0, x1, y1: Integer; cb: TGridRayQueryCB; tagmask: Integer=-1): ITP;
676 const
677 tsize = mTileSize;
678 var
679 i: Integer;
680 dx, dy, d: Integer;
681 xerr, yerr: Integer;
682 incx, incy: Integer;
683 stepx, stepy: Integer;
684 x, y: Integer;
685 maxx, maxy: Integer;
686 gw, gh: Integer;
687 ccidx: Integer;
688 curci: Integer;
689 cc: PGridCell;
690 hasUntried: Boolean;
691 px: PBodyProxyRec;
692 lq: LongWord;
693 prevX, prevY: Integer;
694 minx, miny: Integer;
695 ptag: Integer;
696 lastDistSq, distSq: Integer;
697 wasHit: Boolean = false;
698 lastObj: ITP;
699 lastWasInGrid: Boolean;
700 tbcross: Boolean;
701 f: Integer;
702 begin
703 result := Default(ITP);
704 lastObj := Default(ITP);
705 tagmask := tagmask and TagFullMask;
706 if (tagmask = 0) then begin ex := x0; ey := y0; exit; end;
708 minx := mMinX;
709 miny := mMinY;
711 dx := x1-x0;
712 dy := y1-y0;
714 if (dx > 0) then incx := 1 else if (dx < 0) then incx := -1 else incx := 0;
715 if (dy > 0) then incy := 1 else if (dy < 0) then incy := -1 else incy := 0;
717 dx := abs(dx);
718 dy := abs(dy);
720 if (dx > dy) then d := dx else d := dy;
722 // `x` and `y` will be in grid coords
723 x := x0-minx;
724 y := y0-miny;
726 // increase query counter
727 Inc(mLastQuery);
728 if (mLastQuery = 0) then
729 begin
730 // just in case of overflow
731 mLastQuery := 1;
732 for i := 0 to High(mProxies) do mProxies[i].mQueryMark := 0;
733 end;
734 lq := mLastQuery;
736 // cache various things
737 //tsize := mTileSize;
738 gw := mWidth;
739 gh := mHeight;
740 maxx := gw*tsize-1;
741 maxy := gh*tsize-1;
743 // setup distance and flags
744 lastDistSq := (x1-x0)*(x1-x0)+(y1-y0)*(y1-y0)+1;
745 lastWasInGrid := (x >= 0) and (y >= 0) and (x <= maxx) and (y <= maxy);
747 // setup starting tile ('cause we'll adjust tile vars only on tile edge crossing)
748 if lastWasInGrid then ccidx := mGrid[(y div tsize)*gw+(x div tsize)] else ccidx := -1;
750 // it is slightly faster this way
751 xerr := -d;
752 yerr := -d;
754 // now trace
755 for i := 1 to d do
756 begin
757 // prevs are always in map coords
758 prevX := x+minx;
759 prevY := y+miny;
760 // do one step
761 xerr += dx;
762 yerr += dy;
763 // invariant: one of those always changed
764 if (xerr < 0) and (yerr < 0) then raise Exception.Create('internal bug in grid raycaster (0)');
765 if (xerr >= 0) then begin xerr -= d; x += incx; stepx := incx; end else stepx := 0;
766 if (yerr >= 0) then begin yerr -= d; y += incy; stepy := incy; end else stepy := 0;
767 // invariant: we always doing a step
768 if ((stepx or stepy) = 0) then raise Exception.Create('internal bug in grid raycaster (1)');
769 begin
770 // check for crossing tile/grid boundary
771 if (x >= 0) and (y >= 0) and (x <= maxx) and (y <= maxy) then
772 begin
773 // we're still in grid
774 lastWasInGrid := true;
775 // check for tile edge crossing
776 if (stepx < 0) and ((x mod tsize) = tsize-1) then tbcross := true
777 else if (stepx > 0) and ((x mod tsize) = 0) then tbcross := true
778 else if (stepy < 0) and ((y mod tsize) = tsize-1) then tbcross := true
779 else if (stepy > 0) and ((y mod tsize) = 0) then tbcross := true
780 else tbcross := false;
781 // crossed tile edge?
782 if tbcross then
783 begin
784 // had something in the cell we're leaving?
785 if (ccidx <> -1) then
786 begin
787 // yes, signal cell completion
788 if assigned(cb) then
789 begin
790 if cb(nil, 0, x+minx, y+miny, prevX, prevY) then begin result := lastObj; exit; end;
791 end
792 else if wasHit then
793 begin
794 result := lastObj;
795 exit;
796 end;
797 end;
798 // setup new cell index
799 ccidx := mGrid[(y div tsize)*gw+(x div tsize)];
800 end;
801 end
802 else
803 begin
804 // out of grid, had something in the last processed cell?
805 if (ccidx <> -1) then
806 begin
807 // yes, signal cell completion
808 ccidx := -1;
809 if assigned(cb) then
810 begin
811 if cb(nil, 0, x+minx, y+miny, prevX, prevY) then begin result := lastObj; exit; end;
812 end
813 else if wasHit then
814 begin
815 result := lastObj;
816 exit;
817 end;
818 end;
819 if lastWasInGrid then exit; // oops, stepped out of the grid -- there is no way to return
820 end;
821 end;
823 // has something to process in the current cell?
824 if (ccidx <> -1) then
825 begin
826 // process cell
827 curci := ccidx;
828 hasUntried := false; // this will be set to `true` if we have some proxies we still want to process at the next step
829 // convert coords to map (to avoid ajdusting coords inside the loop)
830 Inc(x, minx);
831 Inc(y, miny);
832 // process cell list
833 while (curci <> -1) do
834 begin
835 cc := @mCells[curci];
836 for f := 0 to High(TGridCell.bodies) do
837 begin
838 if (cc.bodies[f] = -1) then break;
839 px := @mProxies[cc.bodies[f]];
840 ptag := px.mTag;
841 if ((ptag and TagDisabled) = 0) and ((ptag and tagmask) <> 0) and (px.mQueryMark <> lq) then
842 begin
843 // can we process this proxy?
844 if (x >= px.mX) and (y >= px.mY) and (x < px.mX+px.mWidth) and (y < px.mY+px.mHeight) then
845 begin
846 px.mQueryMark := lq; // mark as processed
847 if assigned(cb) then
848 begin
849 if cb(px.mObj, ptag, x, y, prevX, prevY) then
850 begin
851 result := lastObj;
852 ex := prevX;
853 ey := prevY;
854 exit;
855 end;
856 end
857 else
858 begin
859 // remember this hitpoint if it is nearer than an old one
860 distSq := (prevX-x0)*(prevX-x0)+(prevY-y0)*(prevY-y0);
861 if (distSq < lastDistSq) then
862 begin
863 wasHit := true;
864 lastDistSq := distSq;
865 ex := prevX;
866 ey := prevY;
867 lastObj := px.mObj;
868 end;
869 end;
870 end
871 else
872 begin
873 // this is possibly interesting proxy, set "has more to check" flag
874 hasUntried := true;
875 end;
876 end;
877 end;
878 // next cell
879 curci := cc.next;
880 end;
881 // still has something interesting in this cell?
882 if not hasUntried then
883 begin
884 // nope, don't process this cell anymore; signal cell completion
885 ccidx := -1;
886 if assigned(cb) then
887 begin
888 if cb(nil, 0, x, y, prevX, prevY) then begin result := lastObj; exit; end;
889 end
890 else if wasHit then
891 begin
892 result := lastObj;
893 exit;
894 end;
895 end;
896 // convert coords to grid
897 Dec(x, minx);
898 Dec(y, miny);
899 end;
900 end;
901 end;
904 end.