플레이어 설정 단점 - 백엔드 코드를 전방에서 실행하도록 합니다

본고는 최초로 InnoGames Techblog에 발표되었는데, here에서 찾을 수 있다.

소개하다.


게임을 개발할 때, 일반적으로 유저를 특정 상태에 두거나 유저의 진도를 바꾸어야 한다.만약 우리가 새로운 게임 기능이나 빈틈 복구를 시험하고 싶다면, 이것은 특히 필요한 것이다.수동 테스트든 자동 테스트를 작성하든 우리는 시종일관 모든 요구와 조건을 만족시키는 유저 계정을 가지고 이 기능을 테스트해야 한다.
수동 테스트를 할 때, 우리는 보통 게임에서 부정행위를 사용하는데, 이러한 부정행위는 단지 테스트 목적을 위해 실현된 것이다.예를 들어 자원을 분배하려면 즉시 타이머나 잠금 해제 기능을 완성하십시오.
우리의 자동화 전단 테스트(끝에서 끝까지 테스트)에서 테스트 운영자의 행동은 진정한 유저와 같다.예를 들어, 이것은 단추를 누르고 화면에 어떤 것이 나타나기를 기다리는 것을 시뮬레이션한다.그 밖에 실제 테스트를 진행하기 전에 우리는 보통 게임 내 부정행위를 사용하여 게이머의 진도를 바꾼다.
그리고 우리는 백엔드 시스템 통합 테스트를 진행한다.그들은 게임의 백엔드 코드에 완전히 접근할 수 있으며, 이렇게 하면 게임 코드 중의 실체와 저장소 종류를 다시 사용해서 게이머를 설정할 수 있다.이것은 데이터베이스를 직접 조작하기 때문에 여러 개의 사기 요청을 할 필요가 없기 때문에 가장 빠른 방법이다.플레이어 설정을 빌더와 유사한 API로 추상화하여 단일 객체를 구성하여 전체 플레이어 상태를 정의할 수 있습니다(이전 블로그에서 이 구조에 대한 더 많은 정보를 찾을 수 있습니다:).다음은 백엔드 테스트에서 플레이어 설정의 예입니다.
@Test
public void testCollectProduction() {
   buildScenario(scenario -> scenario
      .withPlayer(player -> player
         .withResource(ResourceConstants.COINS, 100)
         .withCity("MainCity", city -> city
            .withBuilding("SomeProductionBuilding", building -> building
               .withProduction(production -> production
                  .withResource(ResourceConstants.COINS)
                  .withAmount(20)
                  .withFinishNow()
               )
            )
         )
      )
   );

   // ... test action and assertions are here ...
}
어느 순간, 우리는 부정행위가 아니라, 부정행위가 매우 제한되어 있기 때문에, 우리가 전방 테스트에서 같은 것을 사용할 수 없는지 알고 싶다.그래서 우리는 전방 테스트에서 완전히 같은 플레이어 설정 코드를 사용할 수 있도록 하는 해결 방안을 생각해냈다. 별도의 노력이 필요 없다!플레이어 설정 끝.

플레이어 설정 끝


이 포트를 보여주기 위해서, 플레이어 설정 포트를 통해 플레이어의 이름, 등급, 장치를 설정할 수 있는 디스플레이 항목을 만들었습니다.찾을 수 있습니다full source code on GitHub.Spring Boot을 사용하여 Java로 작성됩니다.아주 간단한 예로부터 시작합시다.이것은 끝점에 대한 일반 HTTP 요청입니다.
POST /setup-player HTTP/1.1
Host: localhost:8080
Content-Type: text/plain
Accept: application/json

player
    .withName("Hero")
    .withLevel(2)
보시다시피 요청 주체는 실제로 코드를 포함합니다.이것은 우리가 백엔드 테스트에서 사용한 코드와 같다.우리는 유저 명칭을 "영웅"으로 설정하고 레벨을 2로 설정하여 유저 대상을 설정합니다.
또한 프리젠테이션 항목은 플레이어의 현재 상태를 SVG 이미지로 표시하는 끝점을 제공합니다.브라우저에서 열기만 하면 됩니다http://localhost:8080/player.우리의 2급 영웅에 대해 아래 그림을 보실 수 있습니다.

유저에게 무기를 하나 주려면 유저 대상에게 호출withWeapon()만 하면 됩니다.무기 대상에'with...'방법을 사용함으로써 무기 자체를 같은 방식으로 배치할 수 있다.
플레이어에 대한 게시 요청 설정:
player
    .withName("Hero")
    .withLevel(2)
    .withWeapon(weapon -> weapon
        .withType(Weapon.Type.SWORD)
        .withAttackPoints(10)
    )
/플레이어에 대한 요청 얻기:

이제 프레젠테이션 프로젝트가 지원하는 모든 내용을 구성합니다.
플레이어에 대한 게시 요청 설정:
player
    .withName("The Mighty")
    .withLevel(99)
    .withWeapon(weapon -> weapon
        .withType(Weapon.Type.AXE)
        .withAttackPoints(20)
        .withColor("#26639b")
        .entityRef(ref("weapon"))
    )
    .withHeadgear(headgear -> headgear
        .withType(Headgear.Type.HELMET)
        .withDefense(35)
        .withColor("#711284")
        .entityRef(ref("helmet"))
    )
/플레이어에 대한 요청 얻기:

알고 싶을 수도 있어요. entityRef() 전화.entityRef() 방법은 플레이어 설정 기간에 만들어진 실체에 인용된 대상을 받아들인다.우리는 백엔드 테스트에서 그것을 대량으로 사용해서 나중에 새로 만든 실체에 접근할 수 있도록 합니다. (당신은 더 많은 정보를 얻을 수 있습니다.)우리의 단점에서, 우리는 그것을 ref() 함수와 함께 사용합니다. 이것은 플레이어가 단점을 설정하는 특수한 기능입니다.이것은 인용 소유자 대상을 만들고 전달된 이름을 분배합니다.그리고 응답 주체는 이런 방식으로 만들어진 모든 인용을 포함합니다.상기 예에서 우리는 두 가지 참고를 정의했는데 그것이 바로'무기'와'투구'다.다음은 응답하는 모습.
{
    "weapon": {
        "id": "13aafbad-54be-425f-a27a-030935d7852a"
    },
    "helmet": {
        "id": "b9b5fa2d-0a00-40b3-b8c3-c4468943ceff"
    }
}
이것은 JSON 대상을 되돌려줍니다. 인용 이름은 속성이고 실체는 값입니다.우리의 예에서, 우리는 실체의 'id' 필드만 되돌려줍니다.물론 전체 엔티티로 돌아갈 수 있지만 대부분의 경우 ID로 충분할 것입니다.생성된 ID를 이해하는 것은 프런트엔드 테스트에서 새 엔티티에 대한 추가 작업을 수행하는 데 유용합니다.

엔진 덮개 아래의 작업 원리


단점을 통해 사용자 정의 코드를 실행하기 위해서 Groovy language를 사용할 수 있습니다.다행히도 Groovy는 자바 문법과 호환됩니다. 백엔드 테스트에서 코드 부분을 복사해서 요청체에 붙일 수 있습니다.이것은 요청 주체가 실제로는 Groovy 스크립트라는 것을 의미합니다!
응용 프로그램 패키지(675)에 Java를 통합합니다.Groovy 스크립트를 실행할 수 있을 뿐만 아니라 자바와 Groovy 스크립트 사이에서 대상을 공유할 수 있는 groovy 를 덧붙인다.다음 그림은 끝점의 작동 방법을 보여줍니다.
Groovy Shell
GivenLayer 객체는 생성기와 유사한 API를 포함하는 간단한 데이터 객체입니다.Groovy 스크립트가 접근할 수 있도록 Groovy 셸에 이 대상을 전달합니다.다음에 플레이어 대상을 설정하기 위해 요청 주체에서 스크립트를 실행합니다.스크립트 실행이 완료되면 플레이어를 최종적으로 설정하기 위해 같은 대상을 사용할 수 있습니다.
다음은 디렉터 메소드의 전체 Java 코드입니다.
@PostMapping(value = "/setup-player", produces = MediaType.APPLICATION_JSON_VALUE)
public String setUpPlayer(@RequestBody String request) throws JsonProcessingException {
   Player player = playerRepository.getCurrentPlayer();

   // Create sandboxed groovy shell
   var groovyShell = groovyShellFactory.createSandboxShell();

   // Set player property that can be configured inside the groovy script
   var givenPlayer = new GivenPlayer();
   givenPlayer.setEntity(player);
   groovyShell.setProperty("player", givenPlayer);

   // Run requested groovy script which configures the givenPlayer object
   groovyShell.evaluate(request);

   // Use givenPlayer object to set up the player
   playerSetup.setUp(givenPlayer);

   // Return entities that were referenced in the groovy script
   return objectMapper.writeValueAsString(groovyShell.getProperty("references"));
}
GivenLayer 객체 또는 playerSetup.setUp() 방법의 작동 원리에 대해서는 자세히 설명하지 않습니다.너는 내가 앞서 언급한 블로그 게시물에서 이 생각에 대한 더 많은 정보를 찾거나 를 볼 수 있다.일반적으로playerSetup.setUp() 방법은 지정된 도면층 객체를 읽어서 실제 솔리드를 작성합니다.
Groovy Shell이 어떻게 만들어졌는지 살펴보겠습니다.Groovy ShellFactory의 간략한 버전은 다음과 같습니다.
var sandboxClassLoader = new GroovySandboxClassLoader(getClass().getClassLoader());

var compilerConfig = new CompilerConfiguration();
compilerConfig.addCompilationCustomizers(new GroovySandboxImportCustomizer());
compilerConfig.setScriptBaseClass(PlayerSetupScript.class.getName());

var groovyShell = new GroovyShell(sandboxClassLoader, compilerConfig);
groovyShell.setProperty("references", new HashMap<>()); // used to store the references
Groovy 케이스의 설정은 세 개의 사용자 정의 구성 요소를 포함합니다.클래스 로더, ImportCustomizer 및 스크립트 기본 클래스

GitHub의 코드 클래스 로더


Groovy 스크립트의 클래스에 대한 접근을 제한하기 위해 사용자 정의 클래스 불러오기를 사용합니다.비록 단점은 테스트 환경에서만 사용해야 하지만 안전에 주목하는 것도 중요하다.그렇지 않으면 단점은 많은 다른 방식으로 게임을 깨뜨릴 수 있을 것이다.다음은 GroovySandboxClassLoader의 구현입니다.
public class GroovySandboxClassLoader extends ClassLoader {

   public GroovySandboxClassLoader(ClassLoader parent) {
      super(parent);
   }

   @Override
   protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
      if (name.startsWith("java.")
         || name.startsWith("groovy.")
         || name.startsWith("org.codehaus.groovy.")
         || name.equals(PlayerSetupScript.class.getName())
         || GroovySandboxClassWhitelist.getWhitelist().containsKey(name)) {
         return super.loadClass(name, resolve);
      }
      return null;
   }

}
Groovy Shell이 작동하도록 Groovy 패키지와 기본 스크립트 클래스에 접근할 수 있도록 해야 합니다.또한 모든 자바 클래스에서 기본 클래스, 예를 들어String만 사용할 수 있습니다.물론 더 안전하게 하고 싶다면 화이트리스트에 올려도 된다.우리 클래스의 실제 화이트리스트는 단독 Groovy Sandbox Class Whitelist 클래스에서 이루어진 것입니다. 왜냐하면 우리는 Import Customizer에서도 이 화이트리스트를 사용하기 때문입니다.다음은 화이트리스트 클래스의 간략한 버전입니다.
public class GroovySandboxClassWhitelist {

   // Mapping of "Class name" -> "Alias" (We need the alias for the ImportCustomizer)
   @Getter
   private static final Map<String, String> whitelist = new HashMap<>();

   static {
      addWithAlias(GivenPlayer.class);
      addWithAlias(GivenWeapon.class);
      // ...
   }

   private static void addWithAlias(Class<?> clazz) {
      whitelist.put(clazz.getName(), clazz.getSimpleName());
   }

}
프레젠테이션 항목에서, 우리는 단순함을 유지하기 위해 백명단을 하드코딩할 뿐이다.그러나 더 큰 응용 프로그램에서는 너무 많은 유지보수 작업이 될 것이다.우리의 실제 게임 프로그램에서, 우리는 검색을 사용하여 화이트리스트에 들어갈 클래스를 검색한다.

클래스 패키지 수입 맞춤형 기기


클래스를 사용하려면 Groovy 스크립트에 import 문장을 추가해야 합니다.이것이 바로 Import Customizer가 쓸모가 있는 곳이다.이것은 자동으로 클래스를 가져올 수 있도록 하기 때문에 가져오는 문장을 생략할 수 있습니다.이 예에서는 ClassLoader 화이트 리스트에서 별명을 정의하는 모든 클래스를 가져옵니다.
public class GroovySandboxImportCustomizer extends ImportCustomizer {

   public GroovySandboxImportCustomizer() {
      super();

      GroovySandboxClassWhitelist.getWhitelist().entrySet().stream()
         .filter(entry -> !entry.getValue().isBlank())
         .forEach(entry -> addImport(entry.getValue(), entry.getKey()));
   }

}
이로써GroovySandboxClassWhitelist는 클래스 마운트기와 ImportCustomizer의 실제 출처가 되었다.Groovy 스크립트에서 사용할 수 있도록 클래스를 추가하고, 자동으로 가져올 별명을 정의하기만 하면 됩니다.

스크립트 기본 클래스


스크립트 기본 클래스는 Groovy 스크립트에 추가 행동을 제공합니다. Groovy 스크립트는 앞에서 보신 ref() 함수일 뿐입니다.
public abstract class PlayerSetupScript extends Script {

   public AtomicReference<?> ref(String name) {
      var references = (Map<String, AtomicReference<?>>) getProperty("references");

      if (!references.containsKey(name)) {
         references.put(name, new AtomicReference<>());
      }

      return references.get(name);
   }

}
이것은 인용 소유자 대상을 만들고 Groovy 스크립트의'references'라는 속성에 저장합니다.스크립트가 실행된 후, 단점은 이 속성에 접근해서 응답의 모든 인용을 되돌려줍니다.
이렇게!현재, Groovy 스크립트를 실행하는 데 필요한 모든 종류를 만들었습니다. 의 전체 소스 코드를 찾을 수 있습니다.실제 플레이어 설정을 어떻게 실현하는지에 대한 상세한 정보는 프레젠테이션 항목을 보거나 보는 것을 권장합니다.

GitHub의 프레젠테이션 프로젝트 결론


Groovy의 강력한 기능을 빌려 우리는 클라이언트가 우리의 플레이어 설정 시스템에 접근할 수 있는 방법을 찾았다.
물론, 너는 반드시 안전 방면을 기억해야 한다.클래스 명단은 이미 매우 좋은 안전 요소이지만, 오류 코드를 실행할 때 프로그램을 파괴할 수도 있다.테스트 환경에서만 이 단점을 사용해야 합니다.단점은 이 단점을 사용하는 클라이언트가 자동으로 기능을 완성하지 못했다는 것이다.진정한 Groovy 스크립트나 백엔드 코드에서 설정을 준비하는 것을 권장합니다.그렇지 않으면 문법 오류가 발생하기 쉽다.
그러나 지금은 우리가 백엔드에서 새로운 게임 기능을 실현하는 유저 설정만으로도 충분하다.전방 테스트나 게임에서 부정행위를 하면 별도의 노력이 필요 없이 즉시 그것을 사용할 수 있다.
이노게임즈 모집 중! 를 살펴보고 햄버거에 합류한 우수 국제팀, 장소는certified Great Place to Work®입니다.

좋은 웹페이지 즐겨찾기