Raycaster z PixiJS i kolizjami
Oto jak zaimplementowałem mój system Raycastingu w PixiJS:
przez: jonathan.lepage
11.11.2018
Obrazek autorstwa: jonathan.lepage
ECSGAMEDEVMATHPIXIJSTS
Implementacja Raycastera w PixiJS z Wykrywaniem Kolizji Vertex

Wprowadzenie
Podjąłem się sporego wyzwania: zaimplementowania kluczowej funkcji do prawidłowego zarządzania kolizjami w moim silniku gry.W przeciwieństwie do większości silników gier, które używają wektorów,
PixiJS obsługuje jedynie bounds w postaci prostokątnych rzutów.O ile nie stanowi to problemu w 2D, o tyle to podejście jest bardzo ograniczone w kontekście potrzeb gry.
Powyższy obrazek ilustruje zachowanie
raycasta na bounds naszych obiektów.Problem
Jednakże w 3D to podejście stanowi znaczący problem.Gdy
DisplayObject obraca się, jego bounds pozostaje prostym, statycznym prostokątem.Oto przykład:
Dlatego musiałem poświęcić kilka dni na studiowanie matematyki i trygonometrii, aby zaimplementować tę funkcjonalność.
Ta funkcja będzie kluczowa, aby aktor mógł postrzegać swoje otoczenie i wchodzić w interakcje z obiektami w zależności od ich odległości i wysokości.
Ten
raycast posłuży również systemowi FOW (Fog of War), wyświetlając określone części mapy i elementy w zależności od wykrytych kolizji.Jest to temat, który poruszam jedynie pobieżnie, ponieważ jest bardzo złożony.
Mam jednak nadzieję, że ten artykuł zainspiruje Cię do własnego projektu.
Integracje
Aby uprościć moje podejście, stworzyłemClass Helper w ramach mojej architektury.Te
Class Helper są przechowywane w plikach `.h.ts` i eksportują wyłącznie obiekty Pixi.tsexport class RayCasterHelper extends Container3d { public rays:Sprite3d[] = []; public raysVirtual:Graphics[] = []; public raysDistance:number[] = []; public boundsDebugs:Graphics[] = []; public raysTextDistance:Text3d[] = []; public rayLength:number = 900; public raySize:number = 12; public inCircleRadius:number = 10; public quadrans:number = 8; public shapes:CollisionShape[] = []; }
Inicjalizacja tworzy liczbę segmentów wskazaną w parametrze.
W moim przypadku kwadrant składający się z 8 segmentów okazał się więcej niż wystarczający.
Taka konfiguracja ułatwia również debugowanie, ponieważ pozwala na przypisanie 8 różnych kolorów do tych segmentów.
tspublic initialize( renderer:Renderer ) { this.clear(); const colors = [0x00ff00, 0xff0000, 0x0000ff, 0xffff00, 0x00ffff, 0xff00ff, 0x333444, 0xffffff]; for ( let i = 0, l = this.quadrans; i < l; i++ ) { const color = colors[i % colors.length]; const edges = [ new Point( this.inCircleRadius, 0 ), new Point( this.inCircleRadius + this.rayLength, 0 ), ]; const ray = new Graphics(); ray.lineStyle( this.raySize, color, 1 ); ray.moveTo( this.inCircleRadius, 1 ); ray.drawPolygon( edges ); ray.endFill(); const raySprite = new Sprite3d( renderer.generateTexture( ray ) ); this.rays.push( raySprite ); this.shapes[i] = new CollisionShape( raySprite, edges ); raySprite.proj.euler.z = ( Math.PI * 2 / this.quadrans ) * i; this.raysDistance.push( 0 );
Warto zauważyć tworzenie punktów, które służą jako segmenty.
Normalnie, potrzebne byłyby tylko punkty przekątnych, ponieważ prostokąt nie ma martwego punktu na osiach poziomej i pionowej.
Możliwe jest więc ich ignorowanie w zależności od procentu kwadrantu, co pozwala pominąć promienie pionowe i poziome, aby generować poligony tylko na przekątnych.
tsfor ( let i = 0, l = this.quadrans; i < l; i++ ) { const color = colors[i % colors.length]; // continue if is horizontal or vertical if( i % 2 === 0 ) continue; const edges = [...
Następnie pojawia się metoda
intersect, która akceptuje w parametrach wyłącznie DisplayObject.Dostarczamy jej wszystkie
DisplayObject, na których raycast będzie musiał iterować.tspublic intersects( worlds:Container3d[] ) { const { rays, quadrans, intersectedMap, intersectedObjectsMap, closestMap } = this; intersectedMap.clear(); intersectedObjectsMap.clear(); closestMap.clear(); for ( let i = 0, l = quadrans; i < l; i++ ) { const ray = rays[i]; const b1 = ray.getBounds( true, this._cacheRectangle1 ); for ( const object of worlds ) { const b2 = object.getBounds( true, this._cacheRectangle2 ); const intersect = intersects( b1, b2 ); if ( intersect ) { const rayshape = this.shapes[i]; if ( rayshape.intersectsShape( b2 ) ) { intersectedMap.set( i, ( intersectedMap.get( i ) ?? [] ).concat( object ) ); intersectedObjectsMap.set( object, ( intersectedObjectsMap.get( object ) ?? [] ).concat( i ) ); } } } }
Musiałem również dostosować funkcję z
PixiJS, aby wykrywać przecięcia i zbierać dodatkowe dane.Kod przedstawiono poniżej; nie różni się on znacząco od oryginalnej metody.
ts/** return the intersection between two rectangles if any */ function intersects( source:Rectangle, other:Rectangle ) { let x0_1 = source.x < other.x ? other.x : source.x; let x1_1 = source.right > other.right ? other.right : source.right; if ( x1_1 <= x0_1 ) { return null; } let y0_1 = source.y < other.y ? other.y : source.y; let y1_1 = source.bottom > other.bottom ? other.bottom : source.bottom; if ( y1_1 <= y0_1 ) { return null; } // Compute the intersection distance const dx = x0_1 - x1_1; const dy = y0_1 - y1_1; const distance = Math.sqrt( dx * dx + dy * dy ); // Return the intersection details return { x: x0_1, y: y0_1, x2: x1_1, width: x1_1 - x0_1, height: y1_1 - y0_1, distance: distance, inRatioXY: Math.abs( dx / dy ), }; }
Wreszcie, gdy
Pixi wykryje przecięcia między dwoma obiektami, możliwe jest dopracowanie lokalnego wykrywania, aby sprawdzić, czy nasze ukośne segmenty dotykają obiektów.Notatka dotycząca optymalizacji:
Obiekty środowiskowe wykorzystują klasyczne kwadratowebounds.Rzeczywiście, przy wielu tysiącach obiektów, wykrywanie kolizji oparte na wszystkich segmentach bez zoptymalizowanej struktury prowadziłoby do bardzo poważnych problemów z wydajnością.
Prawdopodobnie lepiej byłoby użyć
web workers lub shaders, aby zoptymalizować ten typ obliczeń, zamiast zlecać je do zarządzania CPU.Dlatego tylko przekątne
DisplayObject raycastera będą obsługiwać wykrywanie kolizji poligonalnych, oferując w ten sposób 50% wzrost precyzji.Aby osiągnąć 100% precyzji, obiekty środowiskowe musiałyby również być poligonami, co nie jest niezbędne dla mojego obecnego silnika.
Z wyjątkiem być może ukośnych ogrodzeń, które potencjalnie mogłyby stanowić problem...
przyszłość pokaże.
Ta metoda
IntersectsShape będzie musiała więc iterować po każdym z punktów vertex, aby sprawdzić, czy te punkty znajdują się wewnątrz bounds DisplayObject.tsintersectsShape( rec:Rectangle ) { const edges = this.edges; for ( let i = 0; i < edges.length; i++ ) { const edge = edges[i]; const point = edge.intersectsRectangle( rec ); if ( point ) { return true; } } return false; } class Segment { public p1: Point; public p2: Point; constructor( p1 = new Point(), p2 = new Point() ) { this.p1 = p1; this.p2 = p2; } intersects( edge: Segment, asSegment = true, point = new Point() ) { const a = this.p1; const b = this.p2; const e = edge.p1; const f = edge.p2; const a1 = b.y - a.y; const a2 = f.y - e.y; const b1 = a.x - b.x; const b2 = e.x - f.x; const c1 = ( b.x * a.y ) - ( a.x * b.y ); const c2 = ( f.x * e.y ) - ( e.x * f.y ); const denom = ( a1 * b2 ) - ( a2 * b1 ); if ( denom === 0 ) { return null; } point.x = ( ( b1 * c2 ) - ( b2 * c1 ) ) / denom; point.y = ( ( a2 * c1 ) - ( a1 * c2 ) ) / denom; if ( asSegment ) { const uc = ( ( f.y - e.y ) * ( b.x - a.x ) - ( f.x - e.x ) * ( b.y - a.y ) ); const ua = ( ( ( f.x - e.x ) * ( a.y - e.y ) ) - ( f.y - e.y ) * ( a.x - e.x ) ) / uc; const ub = ( ( ( b.x - a.x ) * ( a.y - e.y ) ) - ( ( b.y - a.y ) * ( a.x - e.x ) ) ) / uc; if ( ua >= 0 && ua <= 1 && ub >= 0 && ub <= 1 ) { return point; } else { return null; } } return point; } intersectsRectangle( rect: Rectangle ) { const rectEdges = [ //TODO: will need optimize with a cache pool of points new Segment( new Point( rect.left, rect.top ), new Point( rect.right, rect.top ) ), new Segment( new Point( rect.right, rect.top ), new Point( rect.right, rect.bottom ) ), new Segment( new Point( rect.right, rect.bottom ), new Point( rect.left, rect.bottom ) ), new Segment( new Point( rect.left, rect.bottom ), new Point( rect.left, rect.top ) ) ]; for ( const rectEdge of rectEdges ) { if ( this.intersects( rectEdge, true ) ) { return true; } } return false; } }
Końcowy render
Metoda
segmentIntersects to ta, w której wykonywane są najbardziej złożone obliczenia.Uruchomienie jej zajęło mi dwa dni intensywnej pracy, ale wynik jest bardzo zadowalający.
Jak widać na tym
gifie, mogę teraz wchodzić w interakcje z moim otoczeniem i przypisywać tagi oraz informacje dzięki Raycastingowi.To proste narzędzie oferuje wiele interesujących funkcji do wykorzystania.