libGDXの開発でレイアウト調整を地味に効率化するダサいやつ

## はじめに

最近始めたレイアウト調整を少しだけ効率良くする自分なりのTipsを書いてみたいと思います。
新規性は無いので宜しくお願いします。
何かツッコミ or より良い方法があれば本記事のコメントなり、はてブコメントなりで反応してもらえるとうれしいです。


## 前提

以下のものを使っている開発

* Java
* libGDX
* Gradle

## 問題

Scene2dを使う場合、座標位置というのは大体Groupに閉じ込められたローカル座標となっているので、
画面のレイアウト上、位置の調整はそこまで大外れはしないものです。

ただし、細かく位置調整をする段階（例えば画面モックを作る段階）であると、画面UIの座標位置や大きさなどを何回も変える必要があります。つまり、単純にやるのであればその度にJavaのクラスを再コンパイルする必要があります。
最近ではJavaコンパイル速度に苦しまされることはなくなってきているとは言え（要出典）、単純な位置調整のために毎回毎回コンパイルしてrelaunchするのは馬鹿らしいですし、時間のかかる作業です。


## 解決策

PropertiesUtilsを使ってレイアウト関連の値をpropertiesファイルで持ちます。
このUtilについては以前にまとめています。
[libGDXのUtilsクラスのメモ](http://qiita.com/yyYank/items/4593a02620c5608e59b9)

## 実際にはどうするか


* 前準備

```java:Hoge.java

import com.badlogic.gdx.scenes.scene2d.Actor;
import com.badlogic.gdx.scenes.scene2d.Group;
import com.badlogic.gdx.utils.ObjectMap;
import com.badlogic.gdx.utils.PropertiesUtils;
import com.badlogic.gdx.utils.StreamUtils;

import java.io.IOException;
import java.io.InputStreamReader;

/**
 * Created by yy_yank on 2017/02/01.
 */
public class Hoge extends Group {


    ObjectMap<String, String> debugCache = new ObjectMap<>();

    public Hoge() {
        // 処理色々
        try{
            InputStreamReader reader = new InputStreamReader(getClass().getClassLoader().getResourceAsStream("debugCache.properties"));
            PropertiesUtils.load(debugCache, reader);
            StreamUtils.closeQuietly(reader);
        } catch (IOException e) {
            // 全力で握りつぶしてるけど良い子は真似しないでね
        }

        init();
    }
    
    private void init() {

        Actor a = new Actor();
        a.setScale(Float.parseFloat(debugCache.get("a.scale")));
        addActor(a);
        a.setPosition(Float.parseFloat(debugCache.get("a.x")), Float.parseFloat(debugCache.get("a.y")));
        Actor b = new Actor();
        b.setPosition(Float.parseFloat(debugCache.get("b.x")), Float.parseFloat(debugCache.get("b.y")));
        addActor(b);
        Actor c = new Actor();
        c.setPosition(Float.parseFloat(debugCache.get("c.x")), Float.parseFloat(debugCache.get("c.y")));
        addActor(c);
        Actor d = new Actor();
        d.setPosition(Float.parseFloat(debugCache.get("d.x")), Float.parseFloat(debugCache.get("d.y")));
        addActor(d);
    }
}

```

そして、android/assets的なところにプロパティファイルを置いておきます。

``` bash:debugCache.properties
a.x=100
a.y=200
a.scale=1
b.x=100
b.y=300
c.x=100
c.y=400
d.x=100
d.y=500
```
なんとも泥臭いですが、こうすることでプロパティファイルの値を用いてレイアウトの調整をすることが出来ます。


* ゲームアプリ起動

そして、ゲームアプリを起動します。デスクトップ版起動を想定しています。


* 気になるところの修正


さっき書いたソースコードのa,ｂ,c,dと言ったActorの位置調整をしていきます。
具体的にはdesktop/build/resources/main配下とかにdebugCache.propertiesというファイルが生成されているはずなのでそちらをいじっていきます。そうすることで、HogeクラスのActorのインスタンス生成がされるごとに位置を調整出来るわけです。なんともダサい。でも、毎回コンパイルするよりかは早いです。


## 位置調整が終わったあとはどうするか

この```ObjectMap<String, String>```型を毎回parseして数値型に変換する処理だけでは芸が無いので、
一応座標やscale、フォントサイズが決まったあとを想定して、ソースコードにプロパティファイルの値を適用するよう書き換えるコードを書いてみます。
ちょっとサボってJava SE 7相当になっているのは目をつぶってください。


```java
import com.badlogic.gdx.utils.ObjectMap;
import com.badlogic.gdx.utils.PropertiesUtils;
import com.badlogic.gdx.utils.StreamUtils;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.Charset;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.Arrays;
import java.util.List;


public class DebugPropertiesConverter implements InternalConverter{


    private ObjectMap<String, String> debugCache;
    
    CacheConfigurationFactory factory = new CacheConfigurationFactory() {
        @Override
        public String createCacheVariableName() {
            return "debugCache";
        }

        @Override
        public String createCacheFileName() {
            return "debugCache.properties";
        }

        @Override
        public List<Pair<String, String>> cretateTargetStringPair(String between, String value) {
            return Arrays.asList(
                    new Pair<>("Integer.parseInt("+createCacheVariableName()+".get(\""+ between +"\"))", value),
                    new Pair<>("Double.parseDouble("+createCacheVariableName()+".get(\""+ between +"\"))", value),
                    new Pair<>("Float.parseFloat("+createCacheVariableName()+".get(\""+ between +"\"))", value + "f")
            );
        }
    };

    
    public static void main(String[] args) throws Exception {
        new DebugPropertiesConverter().run();
    }

    public void run() throws Exception {
        FileVisitor<Path> visitor = new InternalFileVisitor(this);
        Files.walkFileTree(Paths.get("/Users/yy_yank/work/projects/hogehoge/core/src/com/github/yyYank"
                ,"your"
                ,"package"
                ,"dir"
        ), visitor);
    }


    @Override
    public ObjectMap<String, String> readBetweenString() throws IOException {
        if(debugCache != null) {
            return debugCache;
        }
        InputStreamReader reader = new InputStreamReader(getClass().getClassLoader().getResourceAsStream(factory.createCacheFileName()));
        this.debugCache = new ObjectMap<>();
        PropertiesUtils.load(debugCache, reader);
        System.out.println(debugCache);
        StreamUtils.closeQuietly(reader);
        return debugCache;
    }

    @Override
    public void executeOnFile(Path file, BasicFileAttributes attrs) throws IOException {
        ObjectMap<String, String> betweens = readBetweenString();
        List<String> lines = Files.readAllLines(file, Charset.forName("UTF-8"));

        boolean isRewrite = false;
        for (ObjectMap.Entry<String, String> between : betweens) {
            List<Pair<String, String>> targets = factory.cretateTargetStringPair(between.key, between.value);
            for (Pair<String, String> target : targets) {
                for (int i = 0; i < lines.size(); i++) {
                    String line = lines.get(i);
                    isRewrite = isRewrite ? isRewrite : line.contains(target.key);
                    lines.set(i, line.replace(target.key, target.value));
                }
            }
        }
        // rewrite
        if(isRewrite) {
            System.out.println("rewrite file.... -> " + file.getFileName());
            Files.write(file, lines, Charset.forName("UTF-8"));
        } else {
            System.out.println("[SKIP]" + file.getFileName());
        }
    }

    @Override
    public void executeOnDir(Path dir, BasicFileAttributes attrs) {
        // do nothing
    }

    @Override
    public boolean filterExtension(Path file) {
        return file.getFileName().toString().endsWith("java");
    }
}

interface CacheConfigurationFactory {
    String createCacheVariableName();
    String createCacheFileName();
    List<Pair<String, String>> cretateTargetStringPair(String between, String value);
}

interface InternalConverter {
    ObjectMap<String, String> readBetweenString() throws IOException;
    void executeOnFile(Path file, BasicFileAttributes attrs) throws IOException;
    void executeOnDir(Path dir, BasicFileAttributes attrs);
    boolean filterExtension(Path file);

}


class InternalFileVisitor implements FileVisitor<Path> {


    private final DebugPropertiesConverter converter;

    public InternalFileVisitor(DebugPropertiesConverter converter) {
        this.converter = converter;
    }

    @Override
    public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
        print("preVisitDirectory : " + dir.getFileName());
        return FileVisitResult.CONTINUE;
    }

    @Override
    public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
        if(converter.filterExtension(file)) {
            print("visitFile : " + file.getFileName());
            converter.executeOnFile(file, attrs);
        } else {
            print("[SKIP]visitFile : " + file.getFileName());
        }
        return FileVisitResult.CONTINUE;
    }

    @Override
    public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
        return FileVisitResult.CONTINUE;
    }

    @Override
    public FileVisitResult visitFileFailed(Path file, IOException exc) throws IOException {
        String error = String.format(" [exception=%s, message=%s]", exc.getClass(), exc.getMessage());
        print("visitFileFailed : " + file.getFileName() + error);
        return FileVisitResult.CONTINUE;
    }

    protected void print(String message) {
        System.out.println(message);
    }
}

class Pair<K, V>{
    public final K key;
    public final V value;
    Pair(K k, V v) {
        this.key = k;
        this.value = v;
    }
}
```


これを実行すると、ソースコードがプロパティファイルで設定した値に書き換わります。



## まとめ

静的型付けはありがたいけど、動的にしたい部分もある！
本当はもっとスマートな感じにやりたい。