2Dゲームを作る際、原点座標は左上であって欲しいのですが libGDXは右下が原点となっています。
3Dとの親和性としてはこちらのほうが自然なのかもしれませんが、長年の慣れで原点が左上でないと気持ち悪くて…。
というわけで、原点を左上にすべくがんばりました。だいぶ納得いく形になったので記事にします。
参考情報
「libGDX y-down」で検索すると左上原点派の仲間を探すのに便利!
stackoverflowでのQ&A
Changing the Coordinate System in LibGDX (Java)
badlogicからの回答もアリ。
原点左上派の熱い叫びに共感公式のy-downサンプル
YDownTest.java
公式サンプルにあるじゃん!
でもこれだと論理画面サイズが物理画面にフィットしないので不完全
解決済みの課題
基本的には OrthographicCamera.setToOrthoメソッドの第一引数をtrueにすればいいんですが、ほかにもいろいろ面倒見てあげないといけません。
以下については解決しました。
- スプライトが上下反転してしまう
- フォントが上下反転してしまう
- UIパーツのフォントが上下反転してしまう(スキンをどうにかしたい)
-
タッチ位置を論理座標に変換するとずれる(これは上下反転しなくてもおきているかも?)※勘違いでした
サンプルソース
以下が今回の成果です。
package com.dokokano.gdxydowntest;
import com.badlogic.gdx.Application;
import com.badlogic.gdx.ApplicationAdapter;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.files.FileHandle;
import com.badlogic.gdx.graphics.Color;
import com.badlogic.gdx.graphics.GL20;
import com.badlogic.gdx.graphics.OrthographicCamera;
import com.badlogic.gdx.graphics.Texture;
import com.badlogic.gdx.graphics.g2d.BitmapFont;
import com.badlogic.gdx.graphics.g2d.GlyphLayout;
import com.badlogic.gdx.graphics.g2d.Sprite;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
import com.badlogic.gdx.graphics.g2d.TextureAtlas;
import com.badlogic.gdx.graphics.g2d.TextureRegion;
import com.badlogic.gdx.graphics.glutils.ShapeRenderer;
import com.badlogic.gdx.graphics.glutils.ShapeRenderer.ShapeType;
import com.badlogic.gdx.math.Vector3;
import com.badlogic.gdx.scenes.scene2d.Actor;
import com.badlogic.gdx.scenes.scene2d.Event;
import com.badlogic.gdx.scenes.scene2d.EventListener;
import com.badlogic.gdx.scenes.scene2d.InputEvent;
import com.badlogic.gdx.scenes.scene2d.InputListener;
import com.badlogic.gdx.scenes.scene2d.Stage;
import com.badlogic.gdx.scenes.scene2d.ui.SelectBox;
import com.badlogic.gdx.scenes.scene2d.ui.Skin;
import com.badlogic.gdx.scenes.scene2d.ui.TextButton;
import com.badlogic.gdx.scenes.scene2d.ui.TextField;
import com.badlogic.gdx.scenes.scene2d.utils.ChangeListener;
import com.badlogic.gdx.utils.ObjectMap;
import com.badlogic.gdx.utils.viewport.FitViewport;
public class MyGdxGame extends ApplicationAdapter {
static final int LOGICAL_WIDTH = 640; // ゲーム内の論理座標 幅
static final int LOGICAL_HEIGHT = 400; // ゲーム内の論理座標 高さ
Stage stage_; // ViewportとCameraの管理をさせる
// 描画用
SpriteBatch batch_;
private ShapeRenderer renderer_;
Texture img_; // イメージ
private Sprite sprite_; // スプライト(キャラ表示用)
BitmapFont font_; // フォント
Skin skin_; // UIパーツ用スキン
@Override
public void create () {
Gdx.app.setLogLevel(Application.LOG_DEBUG); // デバッグログ出力開始
////////////////////////////////////////
// カメラ設定
OrthographicCamera camera = new OrthographicCamera();
camera.setToOrtho(true,LOGICAL_WIDTH,LOGICAL_HEIGHT); // ★第1引数をtrueにすることで上下反転する
// ViewportとStageの設定
FitViewport viewport = new FitViewport(LOGICAL_WIDTH,LOGICAL_HEIGHT,camera);
stage_ = new Stage(viewport);
////////////////////////////////////////
// 描画用オブジェクト生成
renderer_ = new ShapeRenderer(); // 図形描画用
batch_ = new SpriteBatch(); // スプライト描画用
renderer_.setProjectionMatrix(camera.combined); // カメラを設定する
batch_.setProjectionMatrix(camera.combined); // カメラを設定する
////////////////////////////////////////
// スプライト生成
img_ = new Texture("badlogic.jpg"); // イメージ読み込み(256x256 px)
// スプライト初期化
sprite_ = new Sprite( new TextureRegion(img_,0,0,256,256));
sprite_.flip(false, true); // ★第2引数をtrueにすることで上下反転する
sprite_.setSize(256, 256);
////////////////////////////////////////
// フォント読み込み(デフォルトフォント読み込む)
font_ = new BitmapFont(Gdx.files.internal("skin/default.fnt"), true);// ★第2引数をtrueにすることで上下反転する
////////////////////////////////////////
// スキン設定
skin_ = new Skin(); // 空のskinを生成
skin_.add("default-font", font_ , BitmapFont.class); // まずフォント(上下反転済み)をセットする
// skinのjsonファイルを読み込む ※jsonからはフォント設定を除外しておくこと
FileHandle fileHandle = Gdx.files.internal("skin/uiskin_mod.json"); // jsonのファイルハンドルを開く
FileHandle atlasFile = fileHandle.sibling("uiskin.atlas"); // jsonをlaodする前にskin用のテクスチャアトラスを読み込む
if (atlasFile.exists()) {
skin_.addRegions(new TextureAtlas(atlasFile));
}
skin_.load(fileHandle); // jsonをloadする
// フォントを先に読む方法:http://badlogicgames.com/forum/viewtopic.php?f=11&t=8485&p=38863&hilit=skin+font#p38863[link]
////////////////////////////////////////
// UIパーツ設置
Gdx.input.setInputProcessor(stage_); // タッチイベントをstageが受け取るようにする
// ボタンを追加
TextButton button = new TextButton("TestButton", skin_);
button.setWidth(200);
button.setHeight(50);
button.setPosition(400, 50);
button.addListener(new InputListener() {
@Override
public boolean touchDown(InputEvent event, float x, float y, int pointer, int button) {
Gdx.app.debug("ydown", "button clicked");
return true;
}
});
stage_.addActor(button); // stageに設置
TextField textfield = new TextField("Hello", skin_);
textfield.setWidth(200);
textfield.setHeight(50);
textfield.setPosition(400, 150);
textfield.setTextFieldListener(new TextField.TextFieldListener() {
@Override
public void keyTyped(TextField textField, char c) {
Gdx.app.debug("ydown", "key typed:" + c);
}
});
stage_.addActor(textfield); // stageに設置
// ドロップダウンリストを追加
final SelectBox<String> selectbox = new SelectBox<String>(skin_);
String[] menuItem ={"AAA","BBB","CCC","DDD"};
selectbox.setItems(menuItem);
selectbox.addListener(new ChangeListener() {
@Override
public void changed (ChangeEvent event, Actor actor) {
Gdx.app.debug("ydown", "selectbox selected:" + selectbox.getSelected());
}
});
selectbox.setWidth(200);
selectbox.setHeight(100);
selectbox.setPosition(400, 250);
stage_.addActor(selectbox); // stageに設置
}
@Override
public void render () {
////////////////////////////////////////
// 背景
// 背景塗りつぶし(全体を暗赤)
Gdx.gl.glClearColor(0.5f, 0, 0, 1);
Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);
// 論理表示領域を黒で塗りつぶし
renderer_.begin(ShapeType.Filled);
renderer_.setColor(0,0,0.5f,1);
renderer_.rect(0, 0,LOGICAL_WIDTH,LOGICAL_HEIGHT);
renderer_.end();
////////////////////////////////////////
// グリッド描画
// 100x100グリッド描画(50x50で補助グリッド)
renderer_.begin(ShapeType.Line);
for ( int i=0; i<=LOGICAL_WIDTH; i+=50 ) {
renderer_.setColor(i % 100 == 0 ? Color.GREEN : Color.DARK_GRAY);
renderer_.line(i, 0, i, LOGICAL_HEIGHT);
}
for ( int i=0; i<=LOGICAL_HEIGHT; i+=50 ) {
renderer_.setColor(i % 100 == 0 ? Color.GREEN : Color.DARK_GRAY);
renderer_.line(0, i, LOGICAL_WIDTH, i);
}
renderer_.end();
// 100x100グリッドにあわせて座標の数値描画
batch_.begin();
for ( int x=0; x<=LOGICAL_WIDTH; x+=100) {
for ( int y=0; y<LOGICAL_HEIGHT; y+=100) {
font_.getData().setScale(1f);
font_.setColor(Color.WHITE);
font_.draw(batch_, "("+x + ","+y +")" , x, y);
}
}
batch_.end();
////////////////////////////////////////
// 描画(スプライト、文字)
batch_.begin();
// スプライト描画
sprite_.setPosition(100,100); // Spriteの左上の座標を指定
sprite_.draw(batch_);
// 文字列描画
font_.getData().setScale(2f);
font_.setColor(Color.RED);
font_.draw(batch_,"Y-down is justice!",50,50);
batch_.end();
////////////////////////////////////////
// StageのUIパーツ処理
stage_.act(); // 動きのあるパーツの処理(これを忘れるとselectboxなどが動かない)
stage_.draw(); // UIパーツの描画
////////////////////////////////////////
// タッチ処理
if (Gdx.input.isTouched()) {
// タッチ位置を取得(スクリーン座標)
float touchX = Gdx.input.getX();
float touchY = Gdx.input.getY();
// Stageにあわせて論理座標に変換
Vector2 touchPos = new Vector2(touchX,touchY);
stage_.screenToStageCoordinates(touchPos);
Gdx.app.debug("ydown", " touch_cnv x:"+ touchPos.x + " y:" + touchPos.y); // タッチ位置をデバッグ出力
// タッチ位置に赤丸を描画
renderer_.begin(ShapeType.Filled);
renderer_.setColor(Color.RED);
renderer_.circle(touchPos.x, touchPos.y, 30);
renderer_.end();
}
}
@Override
public void resize(int width, int height) {
Gdx.app.debug("ydown", "width:"+ width + " height:" + height);
// リサイズ時Viewportを再設定する
stage_.getViewport().update(width, height,true);
}
}
アセットは assets.zip をダウンロードして以下のように配置してください。
基本的に公式サンプルのファイルですが、「uiskin_mod.json」は「uiskin.json」をもとに一部編集してあります。
2行目の「com.badlogic.gdx.graphics.g2d.BitmapFont: { default-font: { file: default.fnt } },」を削除してあります。
解説
前提
ゲーム内部で利用する座標系は論理座標と呼ぶこととし、1画面に相当するサイズを640x400とすることにします。
原点(0,0)は、左上です!top-left デース!
X座標の値は右側が大きくなります。
Y座標の値は下側が大きくなります。(・∀・)イイネ!!
また、libGDXのUIパーツを利用するため、StageクラスでVierportやCameraを管理することとります。
static final int LOGICAL_WIDTH = 640; // ゲーム内の論理座標 幅
static final int LOGICAL_HEIGHT = 400; // ゲーム内の論理座標 高さ
Stage stage_; // ViewportとCameraの管理をさせる
カメラの設定
カメラはパースを効かせず正投影するためOrthographicCamera を使います。
setToOrthoのメソッドの第1引数をtrueにすることで、上下反転されめでたく原点が左上になります。
Viewportとして、FitViewportを指定していますので、論理座標でのサイズと物理画面サイズとで縦横比率が違う場合は、縦横比を維持したまま内側に収まるように配置されます。(比率が違う部分は余白になります)
// カメラ設定
OrthographicCamera camera = new OrthographicCamera();
camera.setToOrtho(true,LOGICAL_WIDTH,LOGICAL_HEIGHT); // ★第1引数をtrueにすることで上下反転する
// ViewportとStageの設定
FitViewport viewport = new FitViewport(LOGICAL_WIDTH,LOGICAL_HEIGHT,camera);
stage_ = new Stage(viewport);
- 物理画面が縦長の場合、上下に余白が出来る
- 物理画面が横長の場合、左右に余白が出来る
描画用オブジェクト生成
これは普通ですね…
setProjectionMatrixには、cameraからマトリクスを取得して設定します。
cameraの設定を変更する場合は、その都度設定してください。
// 描画用オブジェクト生成
renderer_ = new ShapeRenderer(); // 図形描画用
batch_ = new SpriteBatch(); // スプライト描画用
renderer_.setProjectionMatrix(camera.combined); // カメラを設定する
batch_.setProjectionMatrix(camera.combined); // カメラを設定する
スプライト読み込み
スプライトはそのままでは、上下反転してしまいます。
flipメソッドで上下を反転する必要があります。
img_ = new Texture("badlogic.jpg"); // イメージ読み込み(256x256 px)
// スプライト初期化
sprite_ = new Sprite( new TextureRegion(img_,0,0,256,256));
sprite_.flip(false, true); // ★第2引数をtrueにすることで上下反転する
sprite_.setSize(256, 256);
フォント読み込み
フォントもそのままでは、上下反転してしまいます。
「new font()」で生成される内蔵フォントは反転できないので、フォントファイルを元に作成し、作成時に反転指定をします。
// フォント読み込み(デフォルトフォント読み込む)
font_ = new BitmapFont(Gdx.files.internal("skin/default.fnt"), true);// ★第2引数をtrueにすることで上下反転する
スキン読み込み
UIパーツのスキンをそのまま使うと、UIパーツの文字が全部上下反転してしまいます。
全部コードで生成すればどうとでもなるのですが、jsonファイルから読み込む方法ではfontだけを置き換えるのが難しくハマりしました。
以下の方法で成功しました。
・空のskinを作成する
・まずフォントを設定する(一つ上で読み込んだ、上下反転されたフォントを設定)
・その後、jsonファイルを読み込む
※jsonファイルからは、フォント設定を除外しておく必要があります。
// スキン設定
skin_ = new Skin(); // 空のskinを生成
skin_.add("default-font", font_ , BitmapFont.class); // まずフォント(上下反転済み)をセットする
// skinのjsonファイルを読み込む ※jsonからはフォント設定を除外しておくこと
FileHandle fileHandle = Gdx.files.internal("skin/uiskin_mod.json"); // jsonのファイルハンドルを開く
FileHandle atlasFile = fileHandle.sibling("uiskin.atlas"); // jsonをlaodする前にskin用のテクスチャアトラスを読み込む
if (atlasFile.exists()) {
skin_.addRegions(new TextureAtlas(atlasFile));
}
skin_.load(fileHandle); // jsonをloadする
フォントを先に読み込む方法は、以下のページが大変参考になりました。
Re: Skins and TrueTypeFont
UIパーツ設置
普通に設置できます。
UIパーツもちゃんと左上を原点として座標指定出来ますよ~
Gdx.input.setInputProcessor(stage_); // タッチイベントをstageが受け取るようにする
// ボタンを追加
TextButton button = new TextButton("TestButton", skin_);
button.setWidth(200);
button.setHeight(50);
button.setPosition(400, 50);
stage_.addActor(button); // stageに設置
ちょっと気になる点としてはselectboxの文字表示位置が下にずれてしまいます。
サンプルskinのjsonそのまま流用しているからか、テキストが下にずれます。skinを調整すればなんとかなるかな?
描画
render() メソッド中に記述していきます。
普通ですね。座標は左上が原点デス!
batch_.begin();
// スプライト描画
sprite_.setPosition(100,100); // Spriteの左上の座標を指定
sprite_.draw(batch_);
// 文字列描画
font_.getData().setScale(2f);
font_.setColor(Color.RED);
font_.draw(batch_,"Y-down is justice!",50,50);
batch_.end();
文字列描画も左上が原点となります。
ただし、指定座標より上に少しはみ出る感じです。
タッチ処理
タッチ処理も上記で設定されたCameraクラス(stage_.getViewport().getCamera()で取得してね)を使えば、上下方向も意図通り変換されます。
ただし、論理座標のサイズと物理画面の縦横比率が異なり上下か左右に余白がある場合、タッチ位置が物理画面サイズに引っ張られてずれてしまいます。
なんで?
これは、上下反転しないときもあったような…
以下のようにして補正しています。これで正しいかは謎。
【追加】「stage_.screenToStageCoordinates(touchPos);」で正しく変換できました。補正いらなかった…
if (Gdx.input.isTouched()) {
// タッチ位置を取得(スクリーン座標)
float touchX = Gdx.input.getX();
float touchY = Gdx.input.getY();
// タッチ位置を補正(画面に余白があると正しく論理座標に変換されないので補正する)
float viewportX = stage_.getViewport().getScreenX();
float viewportW = stage_.getViewport().getScreenWidth();
float screenW = Gdx.graphics.getWidth();
float viewportY = stage_.getViewport().getScreenY();
float viewportH = stage_.getViewport().getScreenHeight();
float screenH = Gdx.graphics.getHeight();
float coockedTouchX = ( touchX - viewportX ) * screenW / viewportW;
float coockedTouchY = ( touchY - viewportY ) * screenH / viewportH;
// Cameraにあわせて論理座標に変換
Vector3 touchPos = new Vector3();
touchPos.set(coockedTouchX,coockedTouchY, 0);
stage_.getViewport().getCamera().unproject(touchPos);
Gdx.app.debug("ydown", " touch_cnv x:"+ touchPos.x + " y:" + touchPos.y); // タッチ位置をデバッグ出力
// タッチ位置に赤丸を描画
renderer_.begin(ShapeType.Filled);
renderer_.setColor(Color.RED);
renderer_.circle(touchPos.x, touchPos.y, 30);
renderer_.end();
}
その他未確認事項
・TextureAltusの場合はどうなるか未確認。これについては、公式の公式のy-downサンプルを参照してください。
・Tiledマップ描画ライブラリでどうなるか未確認。
最後に
座標系の懸念が解決したので、これでバリバリゲーム開発できますね!