【JavaFX】ゲーム用フルスクリーンの実装方法を考えてみる
本稿はJavaFXでゲーム用フルスクリーンモードの実装について考えてみたいと思います。(ディスプレイ解像度変更も含む)
フルスクリーンモードへの移行
JavaFXでフルスクリーンモードへ移行する方法は非常に簡単です。
メソッドを呼び出すだけで実装できてしまいますので何も説明することはありませんね。
Stageオブジェクト.setFullScreen(フルスクリーンにするかどうか);
解像度の変更
フルスクリーンは問題ないんですが、解像度が問題です。適当にネットで20分くらい調べていたのですが、いい情報を見つけられませんでした。もっと調べたら見つけられたかもしませんが・・・
ということで今回はJavaFXアプリケーションで無理やり解像度を変更する方法を考えてみます。
実装案その1.Swing連携でJFrameにJavaFXのSceneをはめ込む
Javaで解像度を変更する方法は、AWTのAPIを使用すればできることは以前の記事で紹介しました。
・・・ということはJFXPanelを使用してJFrameでJavaFXのSceneを表示すればフルスクリーン+解像度変更が実装できそうですね。
SwingとJavaFXの連携は次の記事で紹介しました。
この方法を試した結果、私の環境では、純粋なJavaFXよりヌルヌルアニメーションしなくなってしまいました。
この方法のメリットとデメリット
メリット
・とくになし
デメリット
・描画処理が純粋なJavaFXより遅い
・連携処理による動作を考慮するのが面倒
それではサンプルコードをご覧ください。
import java.awt.DisplayMode; import java.awt.GraphicsDevice; import java.awt.GraphicsEnvironment; import java.awt.event.KeyAdapter; import java.awt.event.KeyEvent; import javax.swing.JFrame; import javax.swing.WindowConstants; import javafx.animation.Animation; import javafx.animation.PathTransition; import javafx.application.Application; import javafx.application.Platform; import javafx.embed.swing.JFXPanel; import javafx.scene.Group; import javafx.scene.Scene; import javafx.scene.paint.Color; import javafx.scene.shape.Rectangle; import javafx.stage.Stage; import javafx.util.Duration; public class Test extends Application{ public static void main(String[] args) { launch(args); } //表示領域 double w=640; double h=480; JFXPanel p; @Override public void start(Stage stg) throws Exception { //アニメーション対象である四角オブジェクト Rectangle rect = new Rectangle(100,100); rect.setFill(Color.BLUE); //背景 Rectangle back = new Rectangle(w,h); back.setFill(Color.WHITE); //表示領域 Group root = new Group(back, rect); //Sceneに表示領域セット Scene scene = new Scene(root, w, h); stg.setScene(scene); //ウィンドウ表示 JFrame f = new JFrame(); p = new JFXPanel(); p.setScene(scene); f.add(p); f.pack(); f.setVisible(true); f.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE); //エンターでフルスクリーン有効と解除 scene.setOnKeyReleased(e -> { switch(e.getCode()) { case ENTER: change(f); break; default: break; } }); //エンターでフルスクリーン有効と解除(Swing側にフォーカスがあたるとこっちが呼び出される) f.addKeyListener(new KeyAdapter() { public void keyReleased(KeyEvent e) { switch(e.getKeyCode()) { case KeyEvent.VK_ENTER: Platform.runLater(()->{ change(f); }); break; default: break; } } }); //アニメーション PathTransition pt = new PathTransition(Duration.seconds(3), new Rectangle(50,50,540,380), rect); pt.setAutoReverse(true); pt.setCycleCount(Animation.INDEFINITE); pt.play(); } boolean isActive; public void change(JFrame f) { this.isActive = !isActive; //画面デバイスを取得 GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment(); GraphicsDevice gd = ge.getDefaultScreenDevice(); f.remove(p); if ( isActive ) { f.dispose(); f.setUndecorated(true); f.setVisible(true); //JFrameをフルスクリーンに gd.setFullScreenWindow(f); gd.setDisplayMode(new DisplayMode((int)w, (int)h, 32, 60)); } else { //全ての設定を解除 gd.setFullScreenWindow(null); f.dispose(); f.setUndecorated(false); f.setVisible(true); } f.add(p); } }
フルスクリーン前に一度JFXPanelを外さないとなぜかバグってしまったので、removeを呼び出しています。またSwingのスレッドで処理させようとするとバグるのでSwingのキーイベントはPlatform.runLater経由で実行させています。
この程度の処理でこれらの特殊な動作を考慮する羽目になるとは思いませんでした・・・
普通にアプリケーションを作成する時でも、JavaFXとSwingの連携部分はあまり増やさないほうが得策ですね。
・・・実行結果貼り付けたものの、ちゃんとできてるかわからないと思うのでコピペして実際にENTERを押してみてください。本当は動画にしようと思ったけどうまく取れなかった。。。
※以降のサンプルは実行結果は同じような結果となりますので、省略します。
実装案その2.表示領域をディスプレイの大きさに拡大する
この方法はディスプレイの解像度ではなく、表示領域のほうのサイズを拡大して、実装してやろうという考えです。
この方法を利用すれば、どんなサイズでゲームを制作していたとしても、キッチリフルスクリーン化できます。さらにフルスクリーンへの移行処理も速いです。・・・が問題も多く、領域を広げるということになるため、座標感覚も変わります。
例えば、Windowイベント経由でマウス座標を判定している部分がずれたりします。
さらに、毎秒60回ほど描画するゲームで、スケーリング処理を実行するのはPCへの負荷もかかってしまいそうです。
実際にこの方法を試した結果、私の環境では、毎フレームのスケーリング処理で描画負荷がかかってしまっているのかわかりませんが、PCからめっちゃ頑張ってるような音が聞こえてきました。
メリット
・どんなサイズでもディスプレイいっぱいに表示できる
・フルスクリーンへの移行速度が速い
デメリット
・毎フレームスケーリングが必要なため、ゲーム処理が遅くなる
・ゲームの組み方によっては、座標計算系の処理がずれてしまう
実装方法
JavaFXでディスプレイの解像度を取得するためのScreenクラスがあります。ここからディスプレイ解像度を取得し、その大きさに合わせて画面のリサイズを実装します。
//プライマリディスプレイのサイズを持つオブジェクトを取得 Screen screen = Screen.getPrimary(); Rectangle2D b = screen.getBounds();
計算は横幅と縦幅に対して、ディスプレイ幅/オリジナルのウィンドウ幅を計算し、倍率の小さいほうを拡大率として処理します。あとは余った領域を座標調整するだけです。
それではサンプルをご覧ください。
import javafx.animation.Animation; import javafx.animation.PathTransition; import javafx.application.Application; import javafx.beans.value.ObservableValue; import javafx.geometry.Rectangle2D; import javafx.scene.Group; import javafx.scene.Parent; import javafx.scene.Scene; import javafx.scene.layout.Background; import javafx.scene.layout.BackgroundFill; import javafx.scene.layout.Pane; import javafx.scene.paint.Color; import javafx.scene.shape.Rectangle; import javafx.scene.transform.Scale; import javafx.scene.transform.Translate; import javafx.stage.Screen; import javafx.stage.Stage; import javafx.util.Duration; public class Test extends Application{ public static void main(String[] args) throws InterruptedException { launch(args); } //表示領域 double w=640; double h=480; Parent root; @Override public void start(Stage stg) throws Exception { //アニメーション対象である四角オブジェクト Rectangle rect = new Rectangle(100,100); rect.setFill(Color.BLUE); //背景 Rectangle back = new Rectangle(w,h); back.setFill(Color.WHITE); //表示領域 root = new Group(back, rect); //表示領域の裏側 Pane pane = new Pane(root); pane.setBackground(new Background(new BackgroundFill(Color.BLACK, null, null))); //Sceneに表示領域セット Scene scene = new Scene(pane, w, h); stg.setScene(scene); //エンターでフルスクリーン有効と解除 scene.setOnKeyReleased(e -> { switch(e.getCode()) { case ENTER: setActive(stg, !this.isActive); break; default: break; } }); //デフォルト処理のESC解除も対応しとく stg.fullScreenProperty().addListener((ObservableValue<? extends Boolean> ob, Boolean o, Boolean n)->{ //解除された時 if ( o && !n ) { setActive(stg,false); } }); //ウィンドウ表示 stg.show(); //アニメーション PathTransition pt = new PathTransition(Duration.seconds(3), new Rectangle(50,50,540,380), rect); pt.setAutoReverse(true); pt.setCycleCount(Animation.INDEFINITE); pt.play(); } boolean isActive; Translate t; Scale s; public void setActive(Stage stg, boolean isActive) { this.isActive = isActive; if ( isActive != stg.isFullScreen() ) stg.setFullScreen(isActive); if ( isActive ) { //ディスプレイ領域取得 Screen screen = Screen.getPrimary(); Rectangle2D b = screen.getBounds(); //拡大して中央寄せ double w2 = b.getWidth(); double h2 = b.getHeight(); double scw = w2 / w; double sch = h2 / h; double sc = Math.min(scw, sch); t = new Translate(w2/2-w*sc/2, h2/2-h*sc/2); s = new Scale(sc,sc); root.getTransforms().add(t); root.getTransforms().add(s); } else { //全ての設定を解除 root.getTransforms().remove(t); root.getTransforms().remove(s); } } }
このサンプルはENTERで移行し、ESCで解除になっています。
この方法であれば純粋なJavaFXのみの処理で完結できますね。他APIの動作を考慮せずに済みます。
実装案その3.ダミーのWindowオブジェクトを用意してAWTAPIを利用する
この方法はAWTAPIのWindowを設定しなければならない制約を回避するために、ダミーのWindowオブジェクトを設定し、非表示にしておいて解像度変更を実現してやろうという方法です。
AWTAPIを利用した解像度変更は下記の記事で紹介しました。
1の方法とは違って連携しているわけではなく、個々のオブジェクトとして処理をさせます。
OS側は設定したダミーウィンドウを優先的に表示しようとするので、それを必死で抑え込むプログラムを書く必要があります。環境依存しやすい処理なので、動かしたいプラットフォームでそれぞれテストしなければなりません。
メリット
・処理速度への影響がほとんどない
デメリット
・ダミーのウィンドウが裏に存在することになる
・OS側がダミーウィンドウを表示させようとするので、それの対処が必要
それではこの方法を実現したサンプルをご覧ください。
import java.awt.DisplayMode; import java.awt.GraphicsDevice; import java.awt.GraphicsEnvironment; import javax.swing.JFrame; import javafx.animation.Animation; import javafx.animation.PathTransition; import javafx.application.Application; import javafx.application.Platform; import javafx.beans.value.ObservableValue; import javafx.scene.Group; import javafx.scene.Scene; import javafx.scene.paint.Color; import javafx.scene.shape.Rectangle; import javafx.stage.Stage; import javafx.util.Duration; public class Test extends Application{ public static void main(String[] args) { launch(args); } //表示領域 int w=640; int h=480; JFrame f = new JFrame(); Stage stg; @Override public void start(Stage stg) throws Exception { this.stg = stg; //アニメーション対象である四角オブジェクト Rectangle rect = new Rectangle(100,100); rect.setFill(Color.RED); //背景 Rectangle back = new Rectangle(w,h); back.setFill(Color.WHITE); //表示領域 Group root = new Group(back, rect); //Sceneに表示領域セット Scene scene = new Scene(root, w, h); stg.setScene(scene); //エンターでフルスクリーン有効と解除 scene.setOnKeyReleased(e -> { switch(e.getCode()) { case ENTER: change(!isActive); break; default: break; } }); //デフォルト処理のESC解除も対応しとく stg.fullScreenProperty().addListener((ObservableValue<? extends Boolean> ob, Boolean o, Boolean n)->{ //解除された時 if ( o && !n ) { change(false); } }); stg.show(); //アニメーション PathTransition pt = new PathTransition(Duration.seconds(3), new Rectangle(50,50,540,380), rect); pt.setAutoReverse(true); pt.setCycleCount(Animation.INDEFINITE); pt.play(); } boolean isActive; public void change(boolean isActive) { this.isActive = isActive; //画面デバイスを取得 GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment(); GraphicsDevice gd = ge.getDefaultScreenDevice(); if ( isActive ) { gd.setFullScreenWindow(f); gd.setDisplayMode(new DisplayMode(w, h, 32, 60)); f.setVisible(false); Platform.runLater(()->{ if ( !stg.isFullScreen() ) stg.setFullScreen(true); stg.setAlwaysOnTop(true); stg.requestFocus(); }); } else { //全ての設定を解除 gd.setFullScreenWindow(null); stg.setFullScreen(false); stg.requestFocus(); stg.sizeToScene(); } } }
試した結果ゲームの処理速度への影響が少ない感じがしました。
実装案その4.JNIを利用して各プラットフォーム用の解像度変更処理を実装する
この方法はJNIを利用して各プラットフォーム用の処理をC言語等で実装し、実現する方法です。
JNIの利用方法に関しては下記の記事で紹介しました。
無駄なものが作成されず、ある意味一番理想的な処理を実装可能なのですが、各プラットフォーム用の実装を用意し、テストも行うのは正直キツイです。
かといってWindows版だけ実装するのもなんかなぁ・・・て感じです。
メリット
・処理速度への影響がほとんどない
・その他にも制約なしで解像度変更できる
デメリット
・各プラットフォーム用にそれぞれ実装しなければならない
・テストが面倒くさい
・とにかく面倒くさい
とりあえず、自己学習兼ねて、WindowsとMacの環境だけ解像度変更機能を実装してみましたので、紹介します。バグがあったらごめんなさいm(_ _)m
まずは、Java側でnativeメソッドの作成します。下記の二種類のメソッドを実装します。
・DisplayChange(横幅,縦幅)
・End()
DisplayChangeは指定サイズにディスプレイ解像度を指定して呼び出すだけで切り替えできるようにします。
Endメソッドで元の解像度に戻すように実装します。
それではコードを書いていきましょう。
import javafx.animation.Animation; import javafx.animation.PathTransition; import javafx.application.Application; import javafx.beans.value.ObservableValue; import javafx.scene.Group; import javafx.scene.Scene; import javafx.scene.paint.Color; import javafx.scene.shape.Rectangle; import javafx.stage.Stage; import javafx.util.Duration; public class Test extends Application{ public static void main(String[] args) { launch(args); } //描画領域サイズ int w=800; int h=600; @Override public void start(Stage stg) throws Exception { //プラットフォームによって読み込み処理を変更 String name = System.getProperty("os.name").toLowerCase(); if ( name.contains("mac") ) { System.load(new java.io.File("Test.dylib").getAbsolutePath()); } else { System.loadLibrary("Test"); } //アニメーション対象である四角オブジェクト Rectangle rect = new Rectangle(100,100); rect.setFill(Color.GOLD); //背景 Rectangle back = new Rectangle(w,h); back.setFill(Color.WHITE); //表示領域 Group root = new Group(back, rect); //Sceneに表示領域セット Scene scene = new Scene(root, w, h); stg.setScene(scene); //ENTERでフルスクリーンに移行 scene.setOnKeyReleased(e -> { switch(e.getCode()) { case ENTER: if ( !stg.isFullScreen() ) { change(w, h); stg.setFullScreen(true); } break; default: break; } }); //ESCでフルスクリーン解除されたら、解像度も元に戻す stg.fullScreenProperty().addListener((ObservableValue<? extends Boolean> ob, Boolean o, Boolean n)->{ //解除時は元の解像度に戻す if ( o && !n ) { end(); stg.sizeToScene(); } }); //閉じるでアプリケーション終了 stg.setOnCloseRequest(e ->{System.exit(0);}); stg.show(); //アニメーション処理 PathTransition pt = new PathTransition(Duration.seconds(3), new Rectangle(50,50,w-100,h-100), rect); pt.setAutoReverse(true); pt.setCycleCount(Animation.INDEFINITE); pt.play(); } public native void change(int width, int height); public native void end(); }
上記のJavaファイルをコマンド「javac Test.java -h .」でJNI用ヘッダファイルを作成しましょう。下記が作成されたヘッダファイルです。
/* DO NOT EDIT THIS FILE - it is machine generated */ #include <jni.h> /* Header for class Test */ #ifndef _Included_Test #define _Included_Test #ifdef __cplusplus extern "C" { #endif /* * Class: Test * Method: change * Signature: (II)V */ JNIEXPORT void JNICALL Java_Test_change (JNIEnv *, jobject, jint, jint); /* * Class: Test * Method: end * Signature: ()V */ JNIEXPORT void JNICALL Java_Test_end (JNIEnv *, jobject); #ifdef __cplusplus } #endif #endif
続いて作成されたヘッダファイルの実装を行います。今回は下記のようにwindowsとmacで動作するnativeメソッドをC++で実装します。
WindowsとMac環境による、ディスプレイ解像度変更は下記の記事で紹介しています。
Windows
Mac
これらの記事で紹介した内容で必要な部分のみを抜き出してメソッド化したものが下記です。
/* DO NOT EDIT THIS FILE - it is machine generated */ #include "Test.h" //-----------------------------WinSource------------------------------- #ifdef _WIN64 #include <windows.h> bool isActive = false; DEVMODE* defaultMode = nullptr; JNIEXPORT void JNICALL Java_Test_change (JNIEnv *, jobject, jint w, jint h) { if (isActive) return; if (defaultMode == nullptr) { defaultMode = new DEVMODE(); EnumDisplaySettings(nullptr, ENUM_CURRENT_SETTINGS, defaultMode); } DEVMODE dm; EnumDisplaySettings(nullptr, ENUM_CURRENT_SETTINGS, &dm); dm.dmPelsWidth = w; dm.dmPelsHeight = h; isActive = ChangeDisplaySettings(&dm, CDS_FULLSCREEN) == DISP_CHANGE_SUCCESSFUL; } JNIEXPORT void JNICALL Java_Test_end (JNIEnv *, jobject) { if (!isActive) return; isActive = ChangeDisplaySettings(defaultMode, CDS_FULLSCREEN) != DISP_CHANGE_SUCCESSFUL; } #endif // !_WIN64 //-----------------------------MacSource------------------------------- #ifdef __APPLE__ #include <CoreGraphics/CoreGraphics.h> bool isActive = false; CGDisplayModeRef defaultMode = nullptr; JNIEXPORT void JNICALL Java_Test_change (JNIEnv *, jobject, jint w, jint h) { if (isActive) return; CGDirectDisplayID displayId = CGMainDisplayID(); if ( defaultMode == nullptr ) defaultMode = CGDisplayCopyDisplayMode(displayId); CFArrayRef arr = CGDisplayCopyAllDisplayModes(displayId, NULL); CFIndex len = CFArrayGetCount(arr); int ref = INT_MAX; CGDisplayModeRef target = nullptr; for ( int i = 0;i < len;i++ ) { CGDisplayModeRef modePtr = (CGDisplayModeRef)(CFArrayGetValueAtIndex(arr, i)); if ( CGDisplayModeGetWidth(modePtr) == w && CGDisplayModeGetHeight(modePtr) == h && abs(60 - CGDisplayModeGetRefreshRate(modePtr)) < ref ) { ref = abs(60 - CGDisplayModeGetRefreshRate(modePtr)); target =modePtr; } } if ( target != nullptr ) { isActive = CGDisplaySetDisplayMode(displayId, target, nullptr) == kCGErrorSuccess; } CFRelease(arr); } JNIEXPORT void JNICALL Java_Test_end (JNIEnv *, jobject) { if (!isActive) return; CGDirectDisplayID displayId = CGMainDisplayID(); isActive = CGDisplaySetDisplayMode(displayId, defaultMode, nullptr) != kCGErrorSuccess; } #endif // !__APPLE_
※2018/7/11
Mac側のプログラムの解放忘れ修正。一部バグも修正。m(_ _)mどうやら配列を解放すると中身も解放されるくさいです。gitのdylibとcpp、nompor-lib全て更新完了済。
見にくいかもしれませんが、windowsとmacの切り替えはマクロでやっちゃいました。ごめんなさい。
プログラムができたら、DLL、dylibファイルをそれぞれ作成し、Javaの起動階層のパスに配置しておきましょう。DLLとdylibの作成方法は、リンクを貼っておくので他のサイトを参考にしてください。
VC++でWindows用DLLの作成方法
xcodeでMac用dylibの作成方法
DLL作成は、上記で貼り付けたJNIの記事でも紹介しておりますので、もしかしたら参考になるかもしれません。
この実装を動作確認したい場合、ファイル作るのも面倒だと思うので、既に動くファイルをまとめたものをgitにアップしておきました。煮るなり焼くなり好きにすればいいと思うの。
https://github.com/nompor/jni_resolution_test
まとめ
今回紹介した方法はそれぞれメリットやデメリットがあると思うので、用途に合わせて・・・使いわけることもなさそうか・・・
とりあえず私は3の方法が手軽で速度も速いので良いかなぁと思いますね。
Macだと3と4の方法はフルスクリーンモードへの移行にかなり時間がかかります。(※1の方法はテストしてません。)
もしMac使用者でアプリが固まった場合はcommand+option+escでアプリケーションを終了できるので覚えておきましょう。
一応、今回紹介した4種類のフルスクリーン実装のうち、2,3,4の方法はnompor-lib-alpha-1.1.2.jarから利用できるようにしておきます。
需要はないかもしれませんが、使用方法のサンプルも載せておきます。
import com.nompor.gtk.fx.FullScreenControllerFX; import com.nompor.gtk.fx.FullScreenResizer; import com.nompor.gtk.fx.GTKManagerFX; import com.nompor.gtk.fx.GameViewFX; import javafx.animation.Animation; import javafx.animation.PathTransition; import javafx.application.Application; import javafx.scene.paint.Color; import javafx.scene.shape.Rectangle; import javafx.stage.Stage; import javafx.util.Duration; public class Test extends Application{ public static void main(String[] args) { launch(args); } @Override public void start(Stage stage) throws Exception { GameViewFX view = new GameViewFX(); GTKManagerFX.start(stage, 800, 600, view); //アニメーションオブジェクト Rectangle rect = new Rectangle(100,100); rect.setFill(Color.BLUE); //要素の追加 view.getChildren().add(rect); //2,3,4のモードの中から好きなモードを適用する modeV2();//拡大縮小 //modeV3();//AWT解像度操作 //modeV4();//ネイティブ解像度操作 //アニメーション PathTransition pt = new PathTransition(Duration.seconds(3), new Rectangle(50,50,700,500), rect); pt.setAutoReverse(true); pt.setCycleCount(Animation.INDEFINITE); pt.play(); } //2の方法を適用するメソッド private void modeV2() { //エンターでフルスクリーン有効、無効 FullScreenResizer resizer = new FullScreenResizer(GTKManagerFX.getWindow()); resizer.setAutoChangeEvent(true); GTKManagerFX.setOnKeyReleased(e -> { switch(e.getCode()) { case ENTER: resizer.setActive(!resizer.isActive()); break; default: break; } }); } //3の方法を適用するメソッド private void modeV3() { //エンターでフルスクリーン有効、無効 GTKManagerFX.setOnKeyReleased(e -> { switch(e.getCode()) { case ENTER: GTKManagerFX.setFullScreenWithResolution(!GTKManagerFX.isFullScreenWithResolution()); break; default: break; } }); } //4の方法を適用するメソッド private void modeV4() { //エンターでフルスクリーン有効、無効 GTKManagerFX.initFullScreenWithResolution(FullScreenControllerFX.ResolutionType.NATIVE, true); GTKManagerFX.setOnKeyReleased(e -> { switch(e.getCode()) { case ENTER: GTKManagerFX.setFullScreenWithResolution(!GTKManagerFX.isFullScreenWithResolution()); break; default: break; } }); } }
modeV2が2の方法、modeV3が3の方法、modeV4が4の方法で、それぞれEnterでフルスクリーン有効、無効ができるように実装した例です。コメントアウトを設定しなおすなどして、試したいモードを実行させてみてください。
FullScreenResizer、FullScreenControllerFXのクラスを直接叩く方法であれば、Stageオブジェクトも引数に指定できるようにしています。解像度のサイズも指定できます。
また、どちらのクラスにもsetAutoChangeEventメソッドが存在し、trueにしておくと、Stageのfullscreenプロパティの変更を検出して、自動で解像度変更します。
サンプルのGTKManagerFXは内部でFullScreenControllerFXをコールしているだけです。
3,4に関しては解像度変更できないサイズを指定したときの動作は考慮していませんが、Windows、Macでは解像度変更処理が無視されるだけのようです。
ディスカッション
コメント一覧
まだ、コメントがありません