정보실

웹학교

정보실

javascript Voidcall – 13kb 자바 스크립트 실시간 전략 게임 제작

본문

Voidcall 


13kb의 JavaScript로 Voidcall – WebGL 실시간 전략 게임


최근에 나는 원래의 커맨드 앤 컨커 (나중에 "티 베리안 던"이라고 불렀습니다)를 다시 연주했습니다. 나는 어린 시절 내내 여름 내내 친구 지하실에서 게임을 탐험했습니다. 이후의 실시간 전략 게임과 비교하면 매우 간단하지만 매우 매력적입니다. 나는 js13k에 대한 게임의 본질을 포착하기 시작했습니다.


원본 : https://phoboslab.org/log/2019/09/voidcall-making-of


이 게임에 필요한 모든 것을 13kb의 작은 크기로 압축하는 것은 어려운 도전이었습니다. 작년보다 훨씬 더. 이 기술은 매우 야심적입니다. 3D 렌더링 높이 맵, 부드러운 키 프레임 보간 된 정점 애니메이션이 있는 완전 다각형 및 텍스처 모델, 정확한 피킹 및 견고한 경로 찾기 기능을 갖춘 포괄적 인 마우스 제어입니다.


이 기사에서는 퍼즐의 다양한 조각에 대해 자세히 설명합니다. 모든 것이 오픈 소스이므로 github을 따라갈 수 있습니다.


지형 생성 


처음에는 더 간단한 Canvas2D 렌더 뷰에서 게임을 구현하는 것에 대해 생각했지만 맵에 대해 다양한 그래픽이 필요하다는 것을 깨달았습니다. 3d 렌더링 된 높이 맵을 사용하는 것이 화면에서 보기 좋은 지형을 얻는 가장 쉬운 방법이었습니다.


Perlin Noise Collision Map 


펄린 노이즈 기능을 사용하여 초기 256x256 Canvas2D 이미지가 생성됩니다. 이 함수는 주변 픽셀에 부드러운 그라데이션으로 임의의 값을 생성합니다. 핵심은 서로 다른 주파수로 여러 펄린 노이즈 값을 계층화 하는 것입니다. 이 경우 각 픽셀은 합계 두 펄린 노이즈 값입니다. 낮은 주파수에서 하나의 값 만으로 지형의 광범위한 기능을 생성하고 높은 주파수에서 두 번째 값으로 미세한 디테일을 추가 할 수 있습니다.


for (let y = 0, i = 0; y < MAP_SIZE; y++) {
    for (let x = 0; x < MAP_SIZE; x++, i++) {
        let height = 
            noise(x/35, y/35) + // low frequency
            noise(x/50, y/50); // high frequency
    }
}

결과는 시각적으로 매우 만족스럽지만 빌드 하려는 게임에는 별로 유용하지 않습니다. 길 찾기가 제대로 작동하기 위해서는 걸을 수 있는 지형과 걸을 수 없는 지형을 구분해야 했습니다. 따라서 이 이미지는 신중하게 선택된 임계 값으로 1 비트로 변환됩니다.


이 1 비트 이미지는 게임 로직의 초기 충돌 맵이 됩니다. 그래픽에 사용되는 높이 맵의 경우 이 충돌 맵이 추가로 처리됩니다. 첫 번째 단계는 약간의 흐림 효과이므로 언덕 주위에 매끄러운 가장자리가 다시 생깁니다. 그 후 많은 주파수 펄린 노이즈가 맨 위에 쌓입니다.


Perlin Noise Height Map 


불행히도 Canvas2D API는 크로스 브라우저 블러 기능을 제공하지 않습니다. 컨볼 루션 필터를 사용하여 각 픽셀에 상자 흐림 효과를 적용하는 것은 계산 비용이 많이들 뿐만 아니라 여기에 쓰고 싶은 것보다 약간 더 많은 코드가 필요합니다. 내 솔루션은 높이 맵을 여러 번 그립니다.


// Cheesy blur
ctx.globalAlpha = 0.2;
for (let x = -2; x < 2; x++) {
    for (let y = -2; y < 2; y++) {
        // Draw the canvas onto itself with the x,y offset
        ctx.drawImage(canvas, x, y); 
    }
}

자세한 내용은 여러 가지 펄린 노이즈 값이 계산되어 올바르게 보일 때까지 함께 으깬다. 더 높은 주파수 잡음은 언덕에 더 강하게 적용되고 계곡에는 덜 적용됩니다.


for (let y = 0, i = 0; y < MAP_SIZE; y++) {
    for (let x = 0; x < MAP_SIZE; x++, i++) {
        let
            coarse = px[(y*MAP_SIZE+x)*4],
            mid = noise(x/12, y/12) * 8, 
            fine = 
                noise(x/30, y/30) * 4 +
                noise(x/10, y/10) * 2 +
                noise(x/5, y/5) + 
                noise(x/3, y/3),
            height = 
                coarse * 6 + // Basic map height from collision
                fine * 300 + // Details on everything
                mid * 5 + // Mids on everything
                (coarse * (mid+5)) * 2.5 + // Extra mids on hills
                (coarse * (fine+1)) * 1.5 + // Extra details on hills
                ((255-coarse) * mid*-1)*0.5; // Inverted mids in valleys
    }
}


이제 높이 맵이 생겼으므로 3D 형상을 만들 수 있습니다. 이 높이 맵의 각 픽셀에 대해 쿼드 (2 개의 삼각형)가 생성되어 렌더 버퍼로 푸시 됩니다. 각 쿼드는 서로 다른 두 개의 잔디 모양 타일 중 하나로 무작위로 질감이 나타납니다. 반복적으로 보이지 않을 정도로 충분합니다.


법선 벡터 (표면에 수직 인 벡터)는 각 쿼드마다 한 번씩 계산됩니다. 나중에 쉐이더에서 빛을 계산하는 데 사용됩니다. 쿼드의 4 개의 정점 각각에 대해 동일한 법선 벡터가 초기에 저장됩니다. 결과적으로 평평한 음영이 나타납니다.


높이 맵의 최종 버퍼에는 각각 8 Float32 값으로 설명되는 393k 정점으로 구성된 131k 삼각형이 포함됩니다. 총 12MB의 데이터는 많이 들릴지 모르지만 GPU는 이 숫자를 비웃습니다. 나중에 이것이 실제로 어떻게 렌더링 되는지에 대해 더 이야기하겠습니다.


Perlin Noise Terrain 


매끄러운 법선을 계산하는 것은 이 매우 제한적인 코드에서 해결하기에 특히 털이 문제가 되는 것으로 판명되었습니다. 내가 끝내었던 해결책은 그렇지 않으면 최종 버퍼를 걷고 인접한 정점에서 모든 법선을 모으고 평균화 한 다음 평균 법선을 다시 버퍼에 넣는 것입니다. 법선 벡터를 편집하는 것은 매우 간결한 코드를 생성하지만 모든 면에서 끔찍합니다.


이 모든 결과는 gl.STATIC_DRAW 버퍼에 저장됩니다.


마지막 세부 사항을 위해 수천 개의 바위와 나무 모델이 무작위로 맵에 뿌려집니다. 1 비트 충돌 맵이 사용되므로 이러한 모델은 밸리에만 배치됩니다.


마우스 선택이 제대로 작동하여 자체 버퍼에 트리가 없는 높이 맵만 있는 것이 중요했습니다. 따라서 결합 된 모든 상세 모델은 높이 맵 지오메트리와 별도로 두 번째 gl.STATIC_DRAW 버퍼에 저장됩니다.


Perlin Noise Terrain 


터 레인 생성은 임의성을 기반으로 하지만 게임을 로드 할 때마다 항상 동일한 터 레인으로 끝나는 것이 게임 플레이에 중요했습니다. 따라서 JavaScript의 내장 Math.random ()을 사용하는 것은 그렇지 않습니다. 대신, 게임은 매우 간단한 시드 가능한 난수 생성기를 사용합니다. 터 레인을 생성하기 전에 동일한 시드 값으로 초기화하면 항상 동일한 결과가 생성됩니다.


펄린 노이즈 기능에서 발생하는 재미있는 문제는 브라우저마다 다른 결과를 생성한다는 것입니다. 펄린 노이즈 함수를 초기화하기 위해 JavaScript 내장 Array.sort를 사용하여 순차 값의 배열을 섞습니다.

p.sort(function() {
    return 0.5 - random_float();
});


random_float ()는 항상 모든 브라우저에서 동일한 숫자의 시퀀스를 올바르게 생성하지만 브라우저는 여전히 정렬 알고리즘을 다르게 구현합니다. 따라서 Chrome에서는 Firefox와 다른 셔플 배열로 끝납니다. 물론 MDN은 이에 대해 경고합니다.


정렬의 시간 및 공간 복잡도는 구현에 따라 다르므로 보장 할 수 없습니다. 


어쨌든 무의미한 비교기 기능으로 배열을 섞는 것이 올바른 방법은 아닙니다. 교훈을 얻었습니다. (올바른 방법은 배열을 살펴보고 각 요소를 임의의 다른 요소로 바꾸는 것입니다).


마지막 게임에서는 게임 플레이에 흥미롭고 적합한 지형을 찾을 때까지 다른 시드 값을 연결했습니다. 이 방법으로 생성 된 모든 지형이 유용한 결과를 생성하는 것은 아닙니다. 때때로 지도의 절반이 다른 절반과 완전히 분리됩니다. 때때로 도달 할 수 없는 지형의 "섬"이 생성됩니다.


Perlin Noise Terrain 


처음에 정착 한 지형이 꽤 좋았지 만 나중에 도달 할 수 있는 지역이 실제로 없다는 것을 알게 되었습니다. 새로운 지형을 검색하고 모든 게임 오브젝트를 신중하게 다시 배치하고 싶지 않았습니다. 그래서 나는 통과 가능한 경로를 만들기 위해 지도의 특정 지점에 사각형을 그려서 퍼지했습니다. 게임 개발의 불쾌한 비밀입니다.


지형 생성을 위한 최종 코드는 모두 map_generate()에 있습니다.


3D 모델, 텍스처 & 버텍스 애니메이션 


애니메이션 모델을 만들고 적당한 크기로 압축하는 작업이 많이 있었습니다.


3D 모델링에 관해서는 여전히도 푸스이지만 놀랍도록 독창적 인 Wings3D는 다시 하루를 저장했습니다. 낮은 폴리 모델에서 잘 작동합니다. 저해상도 모델링은 저해상도 픽셀 아트를 만드는 것과 매우 흡사합니다. 제대로 보일 때까지 사물을 밀면 됩니다.


Low Poly Character Model 


주인공 모델은 설득력 있는 휴머노이드로 얻을 수 있는 만큼 낮은 폴리입니다. 모든 팔다리는 삼각형이고 단일 지점으로 끝납니다. 그러나 이전에 지형에서 보았 듯이 3D 데이터는 절대적으로 커질 수 있습니다. 이 모델의 경우에도 62 개의 각면을 3x3 Float32 값으로 저장하면 단일 애니메이션 프레임에 대해 2kb의 데이터가 생깁니다.


이제 캐릭터 모델에는 6 개의 애니메이션 프레임이 포함되어 있습니다. 골격 애니메이션을 살펴 봤지만 작동하려면 많은 코드가 필요했을 것입니다. 대신 가장 간단한 해결책 인 정점 애니메이션을 선택했습니다. 즉 애니메이션의 각 프레임에는 전체 모델이 다시 포함됩니다.


따라서 캐릭터 모델에는 6 개의 애니메이션 프레임 각각에 대한 정점 위치가 모두 포함되어 있지만 여전히 572 바이트로 압축됩니다.


Animation Frames 


내가 거기에 도착하는 데 사용한 두 가지 기술이 있습니다. 첫 번째는 상식이며 거의 모든 모델 데이터 형식에서 사용됩니다. 36 개의 정점 각각은 여러면에서 공유됩니다. 따라서 36 개의 정점 각각에 대해 x, y, z 위치를 한 번 저장하고 면의 3 개 모서리 각각에 대한 정점 위치에 색인을 사용합니다.


또한 각 애니메이션 프레임에서 모델의 토폴로지를 변경하지 않으면 꼭짓점 위치를 다시 저장하면 됩니다. 면의 인덱스 세트는 동일하게 유지됩니다.


정점을 최대 255 개로 제한하면 각 인덱스를 단일 바이트로 저장할 수 있습니다. 마찬가지로 Float32 x, y, z 정점 위치를 각각 단일 바이트로 변환하면 정점 당 9 바이트를 절약 할 수 있습니다. 불연속 값 (이 경우 0-255)까지 임의의 정밀 숫자를 "압축"하는 것을 양자화라고 합니다.


따라서 x, y, z 위치를 각각 1 바이트로 설정하면 각 정점을 저장하기 위해 3 바이트가 필요합니다. 마찬가지로 정점 데이터에 3 x 1 바이트 인덱스 (각 코너 당 하나씩)를 사용하는 각면에서 단일면을 저장하려면 3 바이트가 필요합니다.


36 vertices * 6 animation frames * 3 byte = 648 bytes
 +
62 faces * 3 byte = 186 byte
 =
834 bytes


나쁘지는 않지만 여전히 너무 많습니다.


이 간단한 모델에서는 실제로 x, y, z 위치에 대해 256 개의 이산 값의 1 바이트 "정밀도"가 필요하지 않습니다. 해상도를 훨씬 낮출 수 있습니다.


다음 논리적 단계는 3 개의 값을 모두 2 바이트 또는 16 비트로 묶는 것입니다. 이것은 우리에게 각각의 x, y, z 값에 대해 5 비트를 남겨 둡니다. (기술적으로 값당 5.333 비트. x, y, z 축 중 하나에 더 많은 해상도를 제공하기 위해 여분의 1 비트를 사용할 수 있지만 여기서는 그렇지 않습니다. 여분의 1 비트는 사용되지 않습니다.)


5 비트는 0–31의 값을 제공합니다. 따라서 각 정점 위치는 기본적으로 32x32x32 값의 격자에 스냅됩니다.


물론 얼굴 데이터를 비슷하게 압축하고 싶었지만 0-31 범위의 값으로 36 개의 정점을 모두 처리 할 수는 없었습니다. 운 좋게 Wings3D가 각 면의 첫 번째 구석에 대한 정점 인덱스와 함께 면 데이터를 오름차순으로 내보내는 것을 알았습니다.


f 1// 22// 2//
f 2// 3// 1//
f 2// 24// 18//
f 3// 4// 1//
f 3// 8// 6//
f 4// 23// 17//
f 5// 3// 6//
…
f 32// 35// 33//
f 33// 26// 32//
f 33// 34// 26//
f 33// 35// 34//
f 34// 32// 27//
f 36// 30// 29//


내 보낸 모델에는 면당 하나의 행, 정점 데이터에 대한 3 개의 인덱스가 포함됩니다. 첫 번째 인덱스가 오름차순으로 최대 2 증가하는 방법에 주목하십시오.


논리적 결론은 각 면의 첫 번째 정점 인덱스에 대해 2 비트 주소 증분을 저장하고 나머지 두 인덱스에 2 x 7 비트 숫자를 사용하는 것입니다. 이를 통해 최대 127 개의 정점이 있는 모델을 저장할 수 있습니다. 많은!


물론 각 모델마다 정점,면 및 애니메이션 프레임 수와 같은 약간의 메타 데이터를 저장해야 합니다. C-struct로 설명되는 최종 모델 형식은 다음과 같습니다.

// Retarded Model Format (.rmf):
struct {
    uint8_t num_frames;
    uint8_t num_verts; // per frame
    uint16_t num_indices;
    struct {
        uint8_t reserved : 1;
        int8_t x : 5;
        int8_t y : 5;
        int8_t z : 5;
    } vertices[num_frames * num_verts];
    struct {
        uint8_t a_address_inc : 2;
        uint8_t b_index : 7;
        uint8_t c_index : 7;
    } indices[num_indices];
} rmf_data;

물론 Wings3D에는 이 비전 형식에 대한 수출 업체가 없습니다. 대신 Wavefront OBJ 형식으로 모델을 내보냈습니다. 이 형식은 쉽게 파싱 가능한 일반 텍스트 데이터를 포함합니다. OBJ 형식을이 바이너리 형식으로 변환하는 데 간단한 PHP 스크립트가 사용됩니다.


이 게임에는 총 10 가지 모델이 있습니다. 총 1328 바이트로 쉽게 로드하고 더 나은 ZIP 압축을 위해 모든 파일을 단일 파일로 연결합니다. 모델 형식이 너무 간결하여 ZIP 압축 (1203 바이트 압축)이 거의 되지 않습니다.


All Voidcall Models 


그런 다음 모델은 각각 텍스처 이미지의 단일 타일로 텍스처됩니다.


Textured Model 


내 모델 형식에는 텍스처 좌표가 포함되어 있지 않기 때문에 모델 위에 텍스처를 배치하는 매우 간단한 해킹을 선택했습니다. 타일은 정면에서 모델로 투영됩니다. 기본적으로 각 정점의 x, y 좌표는 텍스처 좌표입니다.


Textured Model 


텍스처화 되지 않은 모델의 발과 손이 어떻게 매우 뾰족하고 단일 정점으로 끝나는 지 확인하십시오. 손과 발이 잘리는 모든 유닛 유형에 텍스처에 투명한 픽셀을 사용하면 검정색 텍스처가 총을 나타내는 군사 유닛이 필요합니다.


다른 애니메이션 프레임을 블렌딩 하기 위해 게임은 마지막 프레임과 현재 프레임의 정점 위치를 선형으로 보간합니다. 얼굴 인덱스를 정점 위치로 풀고 모델의 Y 회전뿐만 아니라 프레임 보간은 모두 JavaScript에서 처리됩니다.


모델을 로드하고 렌더링 하는 마지막 코드는 150 줄에 불과합니다. 완전히 최적화되지 않은 상태 (예 :로드 된 모델은 캐시되지 않음)이지만 이러한 종류의 폴리 카운트에는 문제가 되지 않습니다.


마우스 따기 


내 게임에는 정확한 마우스 컨트롤이 필요합니다. 화면 공간이 아니라 월드 공간에서 마우스 커서의 위치를 ​​정확히 알아야 합니다. 일반적으로 마우스 커서에서 화면을 통해 3D 지오메트리에 광선을 투사 합니다. 이것은 프로젝트 전체에서 지금까지 피할 수 있는 약간의 수학이 필요합니다. 또한 높이 맵에서 적중 테스트 기능이 필요합니다.


현대 게임에 유리하지 않은 또 다른 기술은 특별히 인코딩 된 게임 뷰를 렌더링하고 픽셀을 다시 읽는 것입니다. 예를 들어, 각기 다른 색상으로 칠할 수 있는 각 객체를 렌더링 한 다음 마우스 커서 아래의 픽셀 색상을 다시 읽고 칠한 객체를 확인할 수 있습니다.

내 게임에서 나는 특정 객체에 관심이 없었지만 마우스 커서가 높이 맵에 정확히 어디에 있는지. 내가 아는 경우 JavaScript에서 가까운 객체를 쉽게 확인할 수 있습니다.


Mouse picking 


이 게임은 x, y, z 좌표를 RGB 색상으로 인코딩하는 간단한 셰이더를 사용하여 높이 맵만 렌더링 합니다. 이것은 우리에게 각 차원에서 단지 0-255의 해상도를 남기지 만, 게임에서 낮은 해상도는 실제로 눈에 띄지 않습니다. 어쨌든 게임 공간은 타일 그리드로 나뉘어 있기 때문에 단일 타일을 칠 때의 정확도는 충분합니다.


셰이더는 다음과 같습니다.

void main(void) {
    gl_FragColor = vec4(
        vertex_pos.x / 512.0 + 0.5,
        vertex_pos.y / MAP_HEIGHT,
        vertex_pos.z / 512.0 + 1.2,
        1.0
    );
}

일반적인 카메라 거리에 대해 x, z 오프셋 및 분할이 신중하게 선택됩니다. Z 축은 지도의 최대 높이로 나뉩니다. 게임에서 축소 할 수 있는 경우 셰이더에서 이를 설명해야 합니다. 그러나 게임의 확대 / 축소 수준은 항상 동일하게 유지되므로 이러한 값을 하드 코딩 할 수 있습니다.


물론 이러한 RGB 색상은 현재 카메라 위치에 의해 오프셋되므로 마우스 위치를 결정할 때 이를 고려합니다.


// Read a single pixel at the current mouse position
let px = new Uint8Array(4);
gl.readPixels(
    mouse_x, 
    screen_height - mouse_y, // damn opengl and it's bottom left 0 position
    1, 1, gl.RGBA, gl.UNSIGNED_BYTE, px
);

// Calculate world coords
mouse_world_x = (px[0] / 255 - 0.5) * 512 - camera_x; // red = x
mouse_world_y = (px[1] / 255) * MAP_HEIGHT;           // green = y
mouse_world_z = (px[2] / 255 - 1.2) * 512 - camera_z; // blue = z

OpenGL 좌표 시스템에 대해 불평 할 수 있습니다. OpenGL은 화면의 왼쪽 아래 모서리에 0,0 위치입니다.이 방법은 더 과학적이기 때문입니다. 모든 것을 뒤집는 것은 사소한 것처럼 보이지만, 이것을 처리하는 데 걸린 시간과 시간은 나를 쓰러 뜨립니다. Ejecta, 내 WebGLImageFilter 또는이 게임과 같은 간단한 것들에 관계없이 화면과 좌표를 항상 뒤집어 야합니다.


다른 렌더 패스로 구현 된 이러한 마우스 선택 방법은 요즘에는 GPU가 작업을 수행하고 계산하도록 하는 대신 gl.readPixels()가 뷰를 렌더링하고 색상 버퍼를 반환하기를 기다리는 동안 프로그램을 정지 시키므로 권장하지 않습니다. 다음 프레임. 성능을 절대적으로 저하 시킵니다. 그러나 이 게임에 대한 다른 많은 것들과 마찬가지로, 그것은 중요하지 않습니다. 컴퓨터가 빠릅니다.


표현(Rendering) 


렌더러는 작년에 입력 한 Underrun에서 대부분의 코드를 빌립니다. Underun은 모든 것을 단일 버퍼로 푸시하고 단 한 번의 드로우 콜로 전체 게임을 렌더링 했습니다. Voidcall의 경우이를 여러 번의 draw call로 나누어야 했지만 전체 구조는 동일하게 유지됩니다.

터 레인 지오메트리는 별도의 정적 버퍼에 저장되고 위에서 설명한 마우스 선택을 위해 가짜 색상으로 먼저 렌더링 됩니다.


그런 다음 y 위치를 기준으로 텍스처 색상의 채도를 낮추는 특수 셰이더를 사용하여 지형을 다시 렌더링합니다. 이것은 잔디 질감을 언덕을 위한 회색 돌로 멋지게 "혼합"합니다. 또한 모든 장식 지오메트리, 트리 및 굵은 체가 있는 또 다른 정적 버퍼가 이어집니다.


동적 버퍼는 모든 모델을 렌더링 하는 데 사용되고 그림자의 다른 패스에 플러시 및 재사용 됩니다.


오브젝트 그림자는 텍스처로 그림자 흐리게 그림자 얼룩이 있는 사각형입니다. 그러나 투명하기 때문에 깊이 버퍼를 비활성화 한 상태에서 다른 모든 객체 위에 렌더링 해야 합니다. 그렇지 않으면 그림자 텍스처의 반투명 픽셀이 그 아래의 모든 객체를 숨길 것입니다.


Voidcall render passes 


이 게임은 최대 16 개의 동적 조명을 허용합니다. 기본적으로 언더런에서와 동일하게 작동하지만 이번에는 라이트 계산이 버텍스 쉐이더 대신 픽셀 쉐이더에서 수행됩니다. 이것은 작년 게임의 저화질 영상과 비교하여 더 부드러운 모양을 제공합니다.


유닛의 게임 내 "채팅"은 캔버스 위에 오버레이 된 HTML 요소일 뿐입니다. WebGL 내부의 텍스트 렌더링을 다루고 싶지 않았습니다. 결국 우리는 여기 브라우저에서 실행 중이므로 왜 사용하지 않습니까?


렌더러의 코드는 적절하게 이름이 지정된 renderer.js에서 찾을 수 있습니다.


Pathfinding(길찾기) 


Voidcall은 길 찾기에 표준 A-Star 알고리즘을 사용합니다. 약간의 성능을 최적화 하기 위해 A-Star 구현은 필요한 모든 데이터를 사전에 할당합니다. 런타임 중에 데이터를 할당 할 필요가 없습니다.


방문한 노드 세트와 그 비용을 일반 JavaScript 배열로 푸시 된 오브젝트에 저장하는 대신 모든 상태 정보가 여러 개의 개별 TypedArray에 저장됩니다. 이 작업을 수행하기 위해 구현은 x, z 위치 대신 노드의 "주소"와 직접 작동합니다. 주소는 z * MAP_SIZE + x – 즉 이러한 유형의 배열에 대한 색인입니다. 결과적으로 이것은 또한 코드 크기를 상당히 줄였습니다.

let 
    nodes_parent = new Uint16Array(MAP_SIZE * MAP_SIZE),
    nodes_state = new Uint8Array(MAP_SIZE * MAP_SIZE),
    nodes_g = new Float32Array(MAP_SIZE * MAP_SIZE),
    nodes_f = new Float32Array(MAP_SIZE * MAP_SIZE),
    open_nodes = new Uint16Array(MAP_SIZE * 4), // Should be enough!

    NEIGHBORS = [
        -1-MAP_SIZE, -MAP_SIZE, 1-MAP_SIZE,
        -1,                     1,
        -1+MAP_SIZE,  MAP_SIZE, 1+MAP_SIZE
    ],

    STATE_UNKNOWN = 0,
    STATE_OPEN = 1,
    STATE_CLOSED = 2;

while (num_open_nodes--) {
    let current_addr = open_nodes[num_open_nodes];for (let i = 0; i < NEIGHBORS.length; i++) {
        let neighbor_addr = current_addr + NEIGHBORS[i],

        // Compute cost: f, g.
// Store node data
        nodes_parent[neighbor_addr] = current_addr;
        nodes_state[neighbor_addr] = STATE_OPEN;
        nodes_g[neighbor_addr] = g;
        nodes_f[neighbor_addr] = f;
        num_open_nodes++;

        // Insert into open_nodes
}
}

물론 각 노드의 비용을 계산하려면 주소에서 x, z 위치를 다시 가져와야 합니다.


let x = address % MAP_SIZE,
    z = (address / MAP_SIZE)|0;

또는 MAP_SIZE가 256이라는 것을 알았으므로 값 비싼 부서 대신 비트 트위들 링을 사용할 수 있습니다.


let x = address & 0xff, // low byte
    z = address > 8;    // high byte


AStar Pathfinding 


마지막 단계에서 찾은 경로는 최소 웨이 포인트 수로 압축됩니다. 각 웨이 포인트마다 이후의 모든 웨이 포인트가 보이지 않는 곳을 찾을 때까지 맵에서 볼 수 있으면 제거됩니다. 이는 바둑판 방식으로 뻣뻣한 움직임을 초래할 수있는 불필요한 웨이 포인트를 제거합니다.

AStar Pathfinding 


이 모든 것을 통해 전체 지도에 경로를 그리는 데 약 1.5ms가 걸립니다. 여전히 비용이 많이 들지만 충분합니다. 완전한 구현은 단 120 줄의 코드에 적합합니다.


소리와 음악 


작년과 마찬가지로, 나는 훌륭한 Sonant-X 라이브러리를 사운드와 음악에 사용했습니다. no-fate.net의 친구 인 Andreas Lösch가 Sonant-X Live에서 다시 한 번 놀라운 사운드 트랙을 제작했습니다. 마찬가지로, 몇 가지 음향 효과도 이 라이브러리를 사용합니다.


그러나 나는 도서관을 점검했다. 거의 다시 작성합니다. 사인, 정사각형, 톱니 및 삼각형 발진기에 발전기 기능을 사용하는 대신 모든 것이 단일 룩업 테이블로 사전 계산됩니다. 이로 인해 사운드 트랙을 렌더링하는 데 필요한 시간이 엄청나게 빨라졌습니다. 로드 하는 동안 페이지의 응답 성을 높이기 위해 원래 구현 된 모든 비동기 setTimeout() 호출을 제거 할 수있었습니다.


전체 라이브러리는 이제 약 250 줄의 코드이며 원래 라이브러리의 경우 5900ms에서 단 900ms로 120 초의 음악을 생성합니다.


게임 플레이 


분명히 게임 플레이는 약간 약점입니다. 모든 기술이 구현되었으므로 실제 게임 메커니즘을 조정할 시간이 거의 없었습니다. 당신이 무엇을 해야 할 지 정확히 알고 있더라도 게임은 아마도 너무 어려울 것입니다. 더구나, 대부분의 플레이어는 처음에 완전히 잃어 버렸습니다. 이 게임은 실제로 약간의 튜토리얼을 사용했거나 플레이어가 더 익숙해지기 위해 더 많은 시간을 할애하기 시작했을 수도 있습니다.


https://phoboslab.org/content/assets/voidcall-pathfinding.mp4


마우스 컨트롤이 어떻게 선택되어 있는지 자랑스럽게 생각합니다. 단일 유닛 선택, 선택 영역 드래그, 주변 정렬, 터릿 및 수확기 제작은 다른 완전한 RTS와 거의 비슷합니다. 그러나 이 작업을 수행하는 코드는 스파게티의 큰 혼란입니다. 그리고 기능도 완벽하지 않습니다.


그런 포스트 중 하나를 주문하여 특정 적을 공격 할 수는 없습니다. 대신 가장 가까운 공격 만 공격합니다. 마찬가지로, 의료진은 자신에게 가장 가까운 유닛 만 치료할 수 있습니다. 해당 유닛이 이미 건강 상태가 양호하고 치유가 필요한 다른 유닛이 범위 내에 있더라도 마찬가지입니다. 이러한 것들을 구현할 시간과 공간이 부족합니다.


적과 인공 지능을 생성하는 논리도 매우 중요합니다. 적들은 생성하는 총 에너지 량에 따라 시간 간격이 줄어 듭니다. 스폰 지점에 대해 맵 외부의 임의의 위치가 선택됩니다. 그러면 적은 언덕을 내려 계곡으로 내려 가서 가장 가까운 플레이어 유닛이나 건물을 찾습니다. 적을 공격하면 적의 초기 무작위 대상을 무시할 수 있습니다. 그런 다음 공격자를 향해 이동합니다. 


게임의 파워 레벨을 성공적으로 높이고 모든 (남은) 유닛을 함선으로 되돌릴 때 약간의 애니메이션이 나타납니다. 많은 사람들이 그것을 보았을 것입니다.


축소 


언더런에서와 같은 많은 기술이 게임을 압축하는 데 사용되었습니다. 그러나 이번에는 게임 크기를 13kb 제한 아래로 유지하기 위해 더 열심히 노력해야 했습니다. 압축되지 않은 코드의 무게는 약 96kb이므로 축소는 절대적으로 중요했습니다.


무작위 관찰 :

  • 긴 JavaScript 함수 이름이나 문서 및 창을 짧은 이름으로 별칭을 지정해도 코드를 ZIP 압축 할 때는 도움이 되지 않습니다.
  • ZIP의 모든 파일은 약 100 바이트의 상당한 오버 헤드를 전달합니다.
  • 소스 파일을 연결하는 순서는 ZIP 압축에 중요합니다. 비슷한 코드를 그룹화 하면 100-200 바이트를 더 줄 수 있습니다

마지막 게임은 텍스처 PNG, 이진 형식의 모델 및 모든 코드가 포함 된 index.html의 세 파일로 구성됩니다. PNG와 모델은 약 2kb입니다. 나머지 약 11kb는 모든 코드입니다.


Voidcall code 


Voidcall의 압축되지 않은 전체 소스는 github에 있습니다


재미있는 사실 :이 기사에는 게임에 허용 된 것보다 두 배 이상인 28000자가 포함되어 있습니다. 그러나 ZIP 압축은 10kb에 불과합니다.

페이지 정보

조회 71회 ]  작성일19-10-08 11:35

웹학교