Phaser와 같은 상태 저장 프레임 워크?


9

TL; DR 상태 저장 프레임 워크 내에서 작업 할 때 자동화 된 단위 테스트를 단순화하는 기술을 식별하는 데 도움이 필요합니다.


배경:

저는 현재 TypeScript 및 Phaser 프레임 워크 에서 게임을 작성하고 있습니다. Phaser는 코드 구조를 제한하기 위해 가능한 한 적은 노력을 기울이는 HTML5 게임 프레임 워크라고 설명합니다. 여기에는 캐시, 물리, 게임 상태 등 모든 것에 액세스 할 수 있는 God-object Phaser.Game 이 있다는 몇 가지 장단점이 있습니다 .

이 상태 저장으로 인해 내 Tilemap과 같은 많은 기능을 테스트하기가 실제로 어려워집니다. 예를 보자.

여기에서는 타일 레이어가 올바르게 설정되어 있는지 테스트하고 타일 맵 내에서 벽과 생물을 식별 할 수 있습니다.

export class TilemapTest extends tsUnit.TestClass {
    constructor() {
        super();

        this.map = this.mapLoader.load("maze", this.manifest, this.mazeMapDefinition);

        this.parameterizeUnitTest(this.isWall,
            [
                [{ x: 0, y: 0 }, true],
                [{ x: 1, y: 1 }, false],
                [{ x: 1, y: 0 }, true],
                [{ x: 0, y: 1 }, true],
                [{ x: 2, y: 0 }, false],
                [{ x: 1, y: 3 }, false],
                [{ x: 6, y: 3 }, false]
            ]);

        this.parameterizeUnitTest(this.isCreature,
            [
                [{ x: 0, y: 0 }, false],
                [{ x: 2, y: 0 }, false],
                [{ x: 1, y: 3 }, true],
                [{ x: 4, y: 1 }, false],
                [{ x: 8, y: 1 }, true],
                [{ x: 11, y: 2 }, false],
                [{ x: 6, y: 3 }, false]
            ]);

내가 무엇을하든 맵을 만들려고하자마자 Phaser는 내부적으로 캐시를 호출하며 런타임 중에 만 채워집니다.

전체 게임을로드하지 않으면이 테스트를 호출 할 수 없습니다.

복잡한 해결책은 화면에지도를 표시해야 할 때만지도를 작성하는 어댑터 또는 프록시를 작성하는 것입니다. 또는 필요한 자산 만 수동으로로드 한 다음 특정 테스트 클래스 또는 모듈에 대해서만 사용하여 게임을 직접 채울 수 있습니다.

나는 더 실용적이라고 생각하지만 이것에 대한 외국 솔루션을 선택했습니다. 내 게임 로딩과 실제 재생 사이에 TestState이미로드 된 모든 자산과 캐시 된 데이터로 테스트를 실행 하는를 시작했습니다 .

이것은 내가 원하는 모든 기능을 테스트 할 수 있기 때문에 멋지다. 또한 기술적 인 통합 테스트이기 때문에 화면을보고 적을 표시 할 수 없는지 의아해하기 때문이다. 실제로, 아니, 그들은 시험에서 한 항목 (이미 한 번 일어난)으로 잘못 인식되었을 수도 있고 나중에 시험에서 그들의 죽음과 관련된 사건을받지 않았을 수도 있습니다.

내 질문 -shimming이 이와 같은 테스트 상태입니까? 특히 JavaScript 환경에서 잘 모르는 더 나은 접근 방법이 있습니까?


또 다른 예:

다음은 현재 상황을 설명하는 데 도움이되는보다 구체적인 예입니다.

export class Tilemap extends Phaser.Tilemap {
    // layers is already defined in Phaser.Tilemap, so we use tilemapLayers instead.
    private tilemapLayers: TilemapLayers = {};

    // A TileMap can have any number of layers, but
    // we're only concerned about the existence of two.
    // The collidables layer has the information about where
    // a Player or Enemy can move to, and where he cannot.
    private CollidablesLayer = "Collidables";
    // Triggers are map events, anything from loading
    // an item, enemy, or object, to triggers that are activated
    // when the player moves toward it.
    private TriggersLayer    = "Triggers";

    private items: Array<Phaser.Sprite> = [];
    private creatures: Array<Phaser.Sprite> = [];
    private interactables: Array<ActivatableObject> = [];
    private triggers: Array<Trigger> = [];

    constructor(json: TilemapData) {
        // First
        super(json.game, json.key);

        // Second
        json.tilesets.forEach((tileset) => this.addTilesetImage(tileset.name, tileset.key), this);
        json.tileLayers.forEach((layer) => {
            this.tilemapLayers[layer.name] = this.createLayer(layer.name);
        }, this);

        // Third
        this.identifyTriggers();

        this.tilemapLayers[this.CollidablesLayer].resizeWorld();
        this.setCollisionBetween(1, 2, true, this.CollidablesLayer);
    }

세 부분으로 타일 맵을 만듭니다.

  • 지도 key
  • manifest지도에 필요한 상세하게 모든 자산 (tilesheets 및 spritesheets)
  • mapDefinitiontilemap의 층 구조를 설명한다.

먼저 Phaser 내에서 Tilemap을 구성하려면 super를 호출해야합니다. 이것은에 정의 된 키뿐만 아니라 실제 자산을 검색하려고 할 때 캐시에 대한 모든 호출을 호출하는 부분입니다 manifest.

둘째, 타일 시트와 타일 레이어를 타일 맵과 연결합니다. 이제 맵을 렌더링 할 수 있습니다.

셋째, 나는 반복 처리 내 레이어를 통해 내가지도에서 밀어 내기를 원하는 특별한 개체를 찾을 : Creatures, Items, Interactables등. 나중에 사용할 수 있도록 이러한 개체를 만들어 저장합니다.

나는 여전히이 엔티티를 찾고 제거하고 업데이트 할 수있는 비교적 간단한 API를 여전히 가지고 있습니다.

    wallAt(at: TileCoordinates) {
        var tile = this.getTile(at.x, at.y, this.CollidablesLayer);
        return tile && tile.index != 0;
    }

    itemAt(at: TileCoordinates) {
        return _.find(this.items, (item: Phaser.Sprite) => _.isEqual(this.toTileCoordinates(item), at));
    }

    interactableAt(at: TileCoordinates) {
        return _.find(this.interactables, (object: ActivatableObject) => _.isEqual(this.toTileCoordinates(object), at));
    }

    creatureAt(at: TileCoordinates) {
        return _.find(this.creatures, (creature: Phaser.Sprite) => _.isEqual(this.toTileCoordinates(creature), at));
    }

    triggerAt(at: TileCoordinates) {
        return _.find(this.triggers, (trigger: Trigger) => _.isEqual(this.toTileCoordinates(trigger), at));
    }

    getTrigger(name: string) {
        return _.find(this.triggers, { name: name });
    }

확인하고 싶은 기능입니다. 타일 ​​레이어 또는 타일 세트를 추가하지 않으면 맵이 렌더링되지 않지만 테스트 할 수 있습니다. 그러나 super (...) 호출조차도 테스트에서 분리 할 수없는 컨텍스트 특정 또는 상태 저장 논리를 호출합니다.


2
혼란 스러워요. Phaser가 타일 맵로드 작업을 수행하고 있는지 테스트하려고합니까? 아니면 타일 맵 자체의 내용을 테스트하려고합니까? 전자의 경우, 일반적으로 종속성이 작업을 수행하는지 테스트하지 않습니다. 그것이 도서관 관리자의 일입니다. 후자의 경우 게임 로직이 프레임 워크에 너무 밀접하게 연결되어 있습니다. 성능이 허용하는 한도 내에서 게임의 내부 작업을 순수하게 유지하고 이러한 종류의 혼란을 피하기 위해 부작용을 프로그램의 최상위 계층에 유지하려고합니다.
Doval

아니요, 내 기능을 테스트하고 있습니다. 테스트가 그렇게 보이지 않으면 미안하지만 덮개 아래에 약간의 문제가 있습니다. 본질적으로, 나는 타일 맵을 살펴보고 아이템, 생물 등과 같은 게임 엔티티로 변환하는 특수 타일을 발견하고 있습니다. 이 논리는 모두 내 것이므로 반드시 테스트해야합니다.
IAE

1
그렇다면 Phaser가 정확히 어떻게 관련되어 있는지 설명 할 수 있습니까? Phaser가 어디에서 호출되는지 왜 그런지 명확하지 않습니다. 지도는 어디에서 오나요?
Doval

혼란을 드려 죄송합니다. 테스트하려는 기능 단위의 예로 Tilemap 코드를 추가했습니다. Tilemap은 확장 (또는 선택적으로 has-a) Phaser.Tilemap으로, 사용하고 싶은 여러 가지 추가 기능으로 타일 맵을 렌더링 할 수 있습니다. 마지막 단락은 왜 그것을 따로 테스트 할 수 없는지 강조합니다. 구성 요소라고해도 new Tilemap(...)Phaser가 캐시에서 파기 시작 하는 순간 . 나는 그것을 연기해야하지만, 내 Tilemap은 두 가지 상태, 즉 제대로 렌더링 할 수없는 상태와 완전히 구성된 상태라는 것을 의미합니다.
IAE

첫 번째 의견에서 말했듯이 게임 로직이 프레임 워크에 너무 결합되어있는 것 같습니다. 프레임 워크를 전혀 가져 오지 않고도 게임 로직을 실행할 수 있어야합니다. 타일 ​​맵을 화면에 그리는 데 사용 된 에셋에 연결하는 것이 방해가되었습니다.
Doval

답변:


2

Phaser 또는 Typescipt를 모르는 경우에도 여전히 문제를 해결하려고 노력합니다. 직면 한 문제는 다른 많은 프레임 워크에서도 볼 수있는 문제이기 때문입니다. 문제는 구성 요소가 단단히 결합되어야한다는 것입니다 (모든 것이 신의 대상을 가리키고 신의 대상이 모든 것을 소유합니다 ...). 프레임 워크 제작자가 자체 단위 테스트를 만든 경우에는 일어날 수없는 일입니다.

기본적으로 네 가지 옵션이 있습니다.

  1. 단위 테스트를 중지하십시오.
    다른 모든 옵션이 실패하지 않으면이 옵션을 선택하지 않아야합니다.
  2. 다른 프레임 워크를 선택하거나 직접 작성하십시오.
    단위 테스트를 사용하고 결합을 잃은 다른 프레임 워크를 선택하면 인생이 훨씬 쉬워집니다. 그러나 아마도 당신이 좋아하는 것이 없기 때문에 현재 가지고있는 프레임 워크에 붙어 있습니다. 직접 작성하는 데 많은 시간이 걸릴 수 있습니다.
  3. 프레임 워크에 기여하고 테스트를 친숙하게하십시오.
    아마도 가장 쉬운 방법 일지 모르지만 실제로는 시간이 얼마나 걸리고 프레임 워크 작성자가 풀 요청을 기꺼이 받아 들일 것인지에 달려 있습니다.
  4. 프레임 워크를 감싸십시오.
    이 옵션은 아마도 유닛 테스트를 시작하기에 가장 좋은 옵션 일 것입니다. 단위 테스트에서 실제로 필요한 특정 객체를 감싸고 나머지를 위해 가짜 객체를 만듭니다.

2

David와 마찬가지로 Phaser 또는 Typescript에 익숙하지 않지만 프레임 워크 및 라이브러리를 사용한 단위 테스트에 공통적 인 문제로 인식하고 있습니다.

짧은 대답은 그렇습니다. shimming은 단위 테스트로 이것을 처리하는 정확하고 일반적인 방법 입니다. 분리가 단위 테스트와 기능 테스트의 차이점을 이해하고 있다고 생각합니다.

단위 테스트 는 코드의 작은 섹션이 올바른 결과를 생성 함을 증명합니다. 단위 테스트의 목표에는 타사 코드 테스트가 포함되지 않습니다. 코드는 이미 제 3자가 예상 한대로 작동하도록 테스트되었다고 가정합니다. 프레임 워크에 의존하는 코드에 대한 단위 테스트를 작성할 때 특정 종속성을 코드에 특정 상태로 보이게 준비하거나 프레임 워크 / 라이브러리를 완전히 shim하는 것이 일반적입니다. 간단한 예는 웹 사이트의 세션 관리입니다. 아마도 shim은 저장소에서 읽는 대신 항상 유효하고 일관된 상태를 반환합니다. 또 다른 일반적인 예는 메모리에서 데이터를 숨기고 데이터베이스를 쿼리하는 라이브러리를 무시하는 것입니다. 목표는 데이터베이스 또는 데이터베이스를 연결하는 데 사용하는 라이브러리를 테스트하는 것이 아니라 코드가 데이터를 올바르게 처리하는 것입니다.

그러나 단위 테스트 양호하다고 해서 최종 사용자가 원하는 것을 정확하게 볼 수 는 없습니다 . 기능 테스트 는 전체 기능, 프레임 워크 및 모든 기능이 제대로 작동한다는 더 높은 수준의 관점을 취합니다. 간단한 웹 사이트의 예로 돌아가서 기능 테스트는 코드에 웹 요청을하고 유효한 결과에 대한 응답을 확인할 수 있습니다. 결과를 생성하는 데 필요한 모든 코드에 걸쳐 있습니다. 테스트는 특정 코드 정확성 이상의 기능성에 대한 것입니다.

따라서 단위 테스트를 통해 올바른 길을 가고 있다고 생각합니다. 전체 시스템의 기능 테스트를 추가하려면 Phaser 런타임을 호출하고 결과를 확인하는 별도의 테스트를 작성합니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.