JavaFX 3D 学習ついでに創った 3D ゲーム

このエントリーは、JavaFX Advent Calendar 2013, 10日目です。

昨日は @sk44 さんの JavaFX でページめくりアニメーション的ななにか でした。

明日は @kis さんです。

今日は私が最近グリグリしている JavaFX 3D について断片的ではありますがネット上で見つけることが困難なことについて書きたいと思います。

と言っても私は素人で本来このようなイベントで熱く語るようなスキルは持ち合わせていません。

よっておかしなことや間違ったことをしている可能性は極めて大きいのでコメントでご指摘いただけると幸いです。

そういうことで本来、3D の基礎であるカメラ、ライト、テクスチャ、モデルの移動、回転、スケーリング、そして SubScene といった必ず必要となることは割愛させていただきます。

基礎が知りたいと思われる方もいらっしゃると思います。

ここで耳寄りな情報を!

12 月 13 日に JavaFX 勉強会 & 忘年会 で @skrb さんが初心者にも分かりやすく 3D について解説される予定です。

都会に住まわれている方や、どうしても聞きたい方はこの機会に参加されてグリグリしましょう!(^_^)

残念ながら参加できない方は @skrb さんが 24 日の JavaFX Advent Calendar エントリーに 13 日の内容を紹介してくれる予定です。

 

このエントリーで紹介しているプログラムは JDK 8 Build b118 で創りました。

JDK のバージョンが上がると現在の仕様では動作しなくなるかもしれませんのでご容赦くださいませ。

なぜ 3D なのか? 今年の JavaOne でチェスボードの上を歩く Duke に感動して自分でも創れないかなと思いチャレンジしてきました。

 

その結果が次のようになりました。

 

JavaOne のようになめらかに綺麗に自由自在に動いているわけではありませんが目的は達成しました。Complete Success!! (^_^)

ごく一部の方に喜んでいただけたようなので、JavaFX Advent Calendar のエントリーに 3D ネタを持ってきたわけです。

現在ではネット上で Shape3D を利用したサンプルもけっこう出てます。

ご覧になられた方もけっこういらっしゃると思うのですが、それでは限界があります。

やっぱりモデリングツールで作ったモデルを読み込ませ動かしたい!

そこでこのエントリーではモデルの読み込みと位置情報の取得に焦点をあてていきます。

モデルの読み込みには 3DViewer ってのがあるそうですが動かないようです。(現在は不明です。

ちょっとググってみても使い方やサンプルもヒットしませんでした。(ググり方が甘いかも

そういう訳で私は Jim さんが作られた JFX3DModelImporters を使わせていただいてます。

// Thanks a million!
IntStream.rangeClosed(1, 1_000_000).forEach(i -> {
    System.out.println(“Thanks!”);
});

それではメタセコイアで作った Duke のモデルをインポートしてみます。

a1

 

        // 3D モデルをインポート
        // Duke
        ObjModelImporter dukeImporter = new ObjModelImporter();
        
        try {
            URL objUrl = this.getClass().getResource("Duke/duke.obj");
            dukeImporter.read(objUrl);
        } catch (ImportException e) {
            // G'catch! :p
            e.printStackTrace();
            return;
        }

        MeshView[] dukeMesh = dukeImporter.getImport();

        dukeImporter.close();

        Group dukeGroup = new Group();
        for (MeshView _dukeMesh : dukeMesh) {
            dukeGroup.getChildren().addAll(_dukeMesh);
            _dukeMesh.drawModeProperty().bind(new When(showWireframe).then(DrawMode.LINE).otherwise(DrawMode.FILL));
        }

 

obj ファイルをインポートしてます。

mtl ファイルやテクスチャファイルを忘れずにリソースディレクトリに obj ファイルと一緒にいれといてください。

他のファイル形式もサポートされているのがあるようですが試していないので誰か試してみてください。

読み込みだけだと問題はなさそうですがその後に一工夫いるようです。

MeshView[] にそれぞれのパーツが格納されてしまうのでそれを扱いやすくするために Group として管理します。

258 行目から 262 行目の処理です。

261 行目はドローモードをワイヤーフレームに変更するときにまとめて変更できるように苦肉の策としてついでにやってます。

さぁ、これであなたのキュートな 3D モデルをインポートすることが可能となりました!

私はモデルが管理しやすいようにモデルを複数に分けてインポートしました。

Duke、ボード、タイトル文字、GAMEOVER 文字、ブロックといった具合に。

このやり方が正しいかどうかは解りませんが一度にたくさんのモデルを含んだファイルをインポートしてしまうと管理不能になってしまいそうだから。

インポートしたモデルを解りやすく使いやすく Group として管理してしまえば楽です。

たとえばこの場面ではこの Group は表示させたくないなぁって思うときはこのように管理できるのでいいです。

 

        // ボードを非表示
        boadGroup.setVisible(false);
        // 文字を非表示
        backGroup.setVisible(false);
        // ラケットを非表示
        racketGroup.setVisible(false);
        // GAMEOVER 非表示
        game_overGroup.setVisible(false);

 

これらのインポートしたモデルをノードとして扱えるようにしてしまえばあとは簡単です。

それぞれインポートしたモデルの位置情報さえ取得できればあとはお好きにグリグリ動かしてしまえばいいのです。

たとえば Duke の現在位置(Z 軸手前側の位置)を知るには次のようにすればいいようです。

 

        System.out.println("getBoundsInParent(): " + dukeGroup.getBoundsInParent().getMinZ()); // Duke の現在位置( MinZ )

 

Z 軸奥側の位置を知りたければ行末のメソッドを getMaxZ() にすれば OK です。

もちろん X 軸、Y 軸の位置情報も取得するためのメソッドが用意されてます。

同様にしてボードや他のモデルの位置情報を取得して、AnimationTimer や Timeline を使いちょっとした 3D ゲームのできあがりというわけです!

ここで注意が必要なのが getBoundsInParent()の他に getBoundsInLocal() メソッドと getLayoutBounds() があってこれらは現在位置を取得するためのものではないようです。

現在位置という表現をさっきから使っていますが動かして更新された位置というのが適切かもしれません。

ちょっと JavaDoc を見たのはいいけど、英語が難しくて翻訳ソフトに頼っても???でした。

1.transforms ObservableList
2.scaleX, scaleY
3.rotate
4.layoutX, layoutY
5.translateX, translateY

これら5項目に対応して位置情報を取得できるのは getBoundsInParent() メソッドだけのようです。

英語が問題ない方はすみませんがご自分でご確認くださいませ。m(_ _)m

 

        System.out.println("getBoundsInParent(): " + dukeGroup.getBoundsInParent().getMinZ()); // Duke の現在位置( MinZ )
        System.out.println("getBoundsInLocal():  " + dukeGroup.getBoundsInLocal().getMinZ());
        System.out.println("getLayoutBounds():   " + dukeGroup.getLayoutBounds().getMinZ());

 

このようにして初期位置から Z 軸マイナス方向(手前)に Duke を少しずつ移動させる [ dukeGroup.setTranslateZ(dukeGroup.getTranslateZ() -5); ] アニメーションを実行すると次のように出力されます。

getBoundsInParent(): -175.40234375
getBoundsInLocal():  -175.40234375
getLayoutBounds():   -175.40234375
getBoundsInParent(): -180.40234375
getBoundsInLocal():  -175.40234375
getLayoutBounds():   -175.40234375
getBoundsInParent(): -185.40234375
getBoundsInLocal():  -175.40234375
getLayoutBounds():   -175.40234375
getBoundsInParent(): -190.40234375
getBoundsInLocal():  -175.40234375
getLayoutBounds():   -175.40234375

使いどころを間違えると思わぬバグを仕込んでしまいます。

IDE のお気楽な補完技術に頼っているとやってしまいそうなミスですね。(経験者語る

さて、モデルをインポートして位置情報を把握することが可能となった今、モデルをあなたの意のままに制御可能になったわけです。

たとえば AnimationTimer の handle() メソッドを使って Duke を歩かせてみる。

 

                    // Duke 移動
        dukeGroup.setTranslateX(dukeGroup.getTranslateX() + d_vx);
        dukeGroup.setTranslateZ(dukeGroup.getTranslateZ() + d_vz);

 

このコードは X 軸、Z 軸の現在の位置にそれぞれ d_vx, d_vz 分だけ足した位置に移動させる。

次にボードの右または左に行き当たったら落ちないように反転させる処理はこうなる。(反転モーションは turn() メソッドで Timeline を使って別途処理させてます。)

 

        // 左または右に当たったらx方向速度の符号を反転させる
        if (dukeGroup.getBoundsInParent().getMinX() < bord_left || dukeGroup.getBoundsInParent().getMaxX() > bord_right) {
            d_vx = -d_vx;
            turn();
        }

 

このように位置情報さえ把握できてしまえばモデルの移動、回転、拡大、縮小などを AnimationTimer や Timeline を使って誰でも簡単にできます。

ここでちょっとした疑問がでてきます。

Group として扱ってきたのはいいけど個々の MeshView[] として扱うにはどうすればいいの? そうですよね!

Duke を歩かせるために手足のパーツをそれぞれ動かしたい!そこで MeshView[] として扱ってます。

 

    // Duke モデル手足動作用
    Rotate r_legRotate;
    Rotate l_legRotate;
    Rotate r_armRotate;
    Rotate l_armRotate;

    dukeMesh[2].getTransforms().setAll(
            r_legRotate = new Rotate(0, Rotate.X_AXIS));
    dukeMesh[3].getTransforms().setAll(
            l_legRotate = new Rotate(0, Rotate.X_AXIS));
    dukeMesh[4].getTransforms().setAll(
            r_armRotate = new Rotate(0, Rotate.Z_AXIS));
    dukeMesh[5].getTransforms().setAll(
            l_armRotate = new Rotate(0, Rotate.X_AXIS));

    r_legRotate.angleProperty().bind(angleWalk_r_X);
    l_legRotate.angleProperty().bind(angleWalk_l_X);
    l_armRotate.angleProperty().bind(angleWalk_l_arm);
    r_armRotate.angleProperty().bind(angleWalk_r_arm);

 

MeshView[] が何か解らない場合は標準出力させて確認すればいいでしょう。

メタセコイアではモデルオブジェクトのパーツの順番通りになっているようです。

したがって、obj ファイルの順番通りになっていると思います。(あまり自信がない

歩行モーションはこれらを Timeline を使ってアニメーションさせています。

 

    // 歩行モーション
    dukeAnim_g = new Timeline(
            new KeyFrame(
                    new Duration(300),
                    new KeyValue(angleWalk_r_X, -25),
                    new KeyValue(angleWalk_l_X, 25),
                    new KeyValue(angleWalk_l_arm, -20),
                    new KeyValue(angleWalk_r_arm, 12, Interpolator.EASE_BOTH)
            ),
            new KeyFrame(
                    new Duration(600),
                    new KeyValue(angleWalk_r_X, 0),
                    new KeyValue(angleWalk_l_X, 0),
                    new KeyValue(angleWalk_l_arm, 0),
                    new KeyValue(angleWalk_r_arm, 0, Interpolator.EASE_BOTH)
            ),
            new KeyFrame(
                    new Duration(900),
                    new KeyValue(angleWalk_r_X, 25),
                    new KeyValue(angleWalk_l_X, -25),
                    new KeyValue(angleWalk_l_arm, 20),
                    new KeyValue(angleWalk_r_arm, 12, Interpolator.EASE_BOTH)
            ),
            new KeyFrame(
                    new Duration(1_200),
                    new KeyValue(angleWalk_r_X, 0),
                    new KeyValue(angleWalk_l_X, 0),
                    new KeyValue(angleWalk_l_arm, 0),
                    new KeyValue(angleWalk_r_arm, 0, Interpolator.EASE_BOTH)
            )
    );

    dukeAnim_g.setCycleCount(Timeline.INDEFINITE);
    dukeAnim_g.play();

 

これで歩く Duke の歩行モーションは完成です。

あとは AnimationTimer で移動させれば OK です。

位置情報を取得できるのでお好きなように制御をすれば面白いことができるかもしれませんね!

あと MeshView[] として使っているのは音楽に合わせてスケーリングしている DUKEWALK という文字オブジェクトです。

やってることは単純だけどけっこうインパクトありますね。

 

    @Override
    public void spectrumDataUpdate(double timestamp, double duration, float[] magnitudes, float[] phases) {
        // バックの文字オブジェクトのスケーリング
        IntStream.range(0, magnitudes.length).forEach(i -> {
            backMesh[i].setScaleX((magnitudes[i] + 60.01) * 0.1 + 1.0);
            backMesh[i].setScaleY((magnitudes[i] + 60.01) * 0.1 + 1.0);
            backMesh[i].setScaleZ((magnitudes[i] + 60.01) * 0.1 + 1.0);
        });
    }

 

JavaFX 3D API の基礎はなにも語らず断片的な内容となってしまいましたがいかがでしたでしょうか?

この学習用に作ったサンプルプログラムは下記アドレスからダウンロード可能となってます。

https://www.dropbox.com/sh/itmywi67zb5i3gq/zMn5vf5LPm

こちらはもともとカメラの学習用に移動、回転、拡大、縮小の制御がスライダーでできるようにしてあります。

カメラと言えばこのプログラムを組むときに悩んでいたカメラワークの答えを出してくれた方がいらっしゃいます。

下記サイトより答えをまるっといただきました。(日本語です!)

JavaFX 3Dを理解する

// Thanks a million!
IntStream.rangeClosed(1, 1_000_000).forEach(i -> {
    System.out.println(“Thanks!”);
});

さて、このプログラムのソースはおまじないをはじめ、ぐちゃぐちゃになってしまっているので隠蔽してます。( ̄。 ̄;)

と思ったけど心優しいプロの方もこのエントリーを見られていると思うので恥ずかしながら載せます。

優しく解りやすいアドバイスは大歓迎です!(^_^)

package jp.yucchi.dukewalk;

import com.interactivemesh.jfx.importer.ImportException;
import com.interactivemesh.jfx.importer.obj.ObjModelImporter;
import java.net.URL;
import java.util.Random;
import java.util.stream.IntStream;
import javafx.animation.AnimationTimer;
import javafx.animation.Interpolator;
import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.Timeline;
import javafx.application.Application;
import javafx.beans.binding.When;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.event.ActionEvent;
import javafx.scene.AmbientLight;
import javafx.scene.Group;
import javafx.scene.PerspectiveCamera;
import javafx.scene.PointLight;
import javafx.scene.Scene;
import javafx.scene.SceneAntialiasing;
import javafx.scene.SubScene;
import javafx.scene.control.Accordion;
import javafx.scene.control.Button;
import javafx.scene.control.CheckBox;
import javafx.scene.control.Label;
import javafx.scene.control.Slider;
import javafx.scene.control.TitledPane;
import javafx.scene.image.Image;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.Background;
import javafx.scene.layout.HBox;
import javafx.scene.media.AudioSpectrumListener;
import javafx.scene.media.Media;
import javafx.scene.media.MediaPlayer;
import javafx.scene.paint.Color;
import javafx.scene.shape.DrawMode;
import javafx.scene.shape.MeshView;
import javafx.scene.transform.Rotate;
import javafx.stage.Stage;
import javafx.util.Duration;

public class DukeWalk extends Application implements AudioSpectrumListener {

    // 移動
    private final DoubleProperty xPos = new SimpleDoubleProperty(0);
    private final DoubleProperty yPos = new SimpleDoubleProperty(0);
    private final DoubleProperty zPos = new SimpleDoubleProperty(-1_000);
    // スケール
    private final DoubleProperty sxPos = new SimpleDoubleProperty(1.0);
    private final DoubleProperty syPos = new SimpleDoubleProperty(1.0);
    private final DoubleProperty szPos = new SimpleDoubleProperty(1.0);
    // 回転
    private final DoubleProperty rxPos = new SimpleDoubleProperty(0);
    private final DoubleProperty ryPos = new SimpleDoubleProperty(0);
    private final DoubleProperty rzPos = new SimpleDoubleProperty(0);
    // 表示モード
    private final BooleanProperty showWireframe = new SimpleBooleanProperty(false);
    // 座標
    private double anchorAngleX;
    private double anchorAngleY;
    private final DoubleProperty angleX = new SimpleDoubleProperty(0);
    private final DoubleProperty angleY = new SimpleDoubleProperty(0);
    private double anchorX;
    private double anchorY;
    // Duke 反転処理用
    private final DoubleProperty turnAngleY = new SimpleDoubleProperty(0);
    // Duke 転倒処理用
    private final DoubleProperty turnAngleX = new SimpleDoubleProperty(0);
    // Duke 手足アニメーション用
    DoubleProperty angleWalk_r_X = new SimpleDoubleProperty(0);
    DoubleProperty angleWalk_l_X = new SimpleDoubleProperty(0);
    DoubleProperty angleWalk_l_arm = new SimpleDoubleProperty(0);
    DoubleProperty angleWalk_r_arm = new SimpleDoubleProperty(0);
    // 歩行モーションタイムライン
    private Timeline dukeAnim;
    // カメラ用アニメーションタイマー
    private AnimationTimer walkTimer;
    // 方位角
    private double azimuth;
    // 前回時間
    private long previousHandledTime;
    // 回転速度
    private final DoubleProperty azimuthRateProperty = new SimpleDoubleProperty(15);
    // カメラ高さ
    private final DoubleProperty elevationProperty = new SimpleDoubleProperty(7);
    // 半径
    private static final double SUBCAMERA_RADIUS = 5_500;
    // Duke 反転アニメーション用タイムライン
    private Timeline rightTurn;
    private Timeline leftTurn;
    // BGM
    private final Media bgm = new Media(getClass().getResource("music/bgm.mp3").toExternalForm());
    // メディアプレーヤー
    private MediaPlayer mp_bgm;
    private AudioSpectrumListener audioSpectrumListener;
    // 文字オブジェクト用メッシュビュー配列
    private MeshView[] backMesh;
    // Game 用 Duke の移動スピード
    private double d_vx = -5.0;
    private double d_vz = -6.0;
    // Game 用アニメーションタイマー
    private AnimationTimer walkTimer_g;
    // Game 用 Duke ターンアニメーション用タイムライン
    private Timeline turn_g;
    // Game 用 Duke 転倒タイムライン
    private Timeline fall;
    // GAMEOVER 用回転アングル
    private final DoubleProperty g_turnAngleX = new SimpleDoubleProperty(0);
    // Score カウンター
    private int score;
    // メディアプレーヤー ゲームオーバー
    private MediaPlayer mp_o;
    private final Media o_mp = new Media(getClass().getResource("sound/o.mp3").toExternalForm());
    // スコア専用タイムライン
    private Timeline scoreTimeline;

    @Override
    public void start(Stage primaryStage) {
        Group root = new Group();
        Label translateLabel = new Label("Translate");
        Label xLabel = new Label("X");
        Label yLabel = new Label("Y");
        Label zLabel = new Label("Z");
        Label scaleLabel = new Label("Scale      "); // 良い子はこんなことしちゃだめよ!
        Label sxLabel = new Label("X");
        Label syLabel = new Label("Y");
        Label szLabel = new Label("Z");
        Label rotateLabel = new Label("Rotate    "); // 良い子はこんなことしちゃだめよ!
        Label rxLabel = new Label("X");
        Label ryLabel = new Label("Y");
        Label rzLabel = new Label("Z");
        Label drawModeLabel = new Label("Wireframe");
        Label resetLabel = new Label("        Reset"); // 良い子はこんなことしちゃだめよ!
        Label walkLabel = new Label("   Duke Walk!"); // 良い子はこんなことしちゃだめよ!
        Label stopLabel = new Label("   Duke Stop!"); // 良い子はこんなことしちゃだめよ!
        Label gameLabel = new Label("   Duke Walk Game"); // 良い子はこんなことしちゃだめよ!
        Label scoreLabel = new Label("   L Key < RIGHT >    J Key < LEFT >  Please enjoy!"); // 良い子はこんなことしちゃだめよ!
        Slider xSlider = new Slider(-3000, 3000, 0);
        Slider ySlider = new Slider(-3000, 3000, 0);
        Slider zSlider = new Slider(-5000, 5000, 0);
        Slider sxSlider = new Slider(0.01, 10, 1.0);
        Slider sySlider = new Slider(0.01, 10, 1.0);
        Slider szSlider = new Slider(0.01, 10, 1.0);
        Slider rxSlider = new Slider(-360, 360, 0);
        Slider rySlider = new Slider(-360, 360, 0);
        Slider rzSlider = new Slider(-360, 360, 0);
        xSlider.setPrefSize(533, 0);
        ySlider.setPrefSize(533, 0);
        zSlider.setPrefSize(533, 0);
        sxSlider.setPrefSize(533, 0);
        sySlider.setPrefSize(533, 0);
        szSlider.setPrefSize(533, 0);
        rxSlider.setPrefSize(533, 0);
        rySlider.setPrefSize(533, 0);
        rzSlider.setPrefSize(533, 0);
        xSlider.setShowTickLabels(true);
        xSlider.setMajorTickUnit(500);
        ySlider.setShowTickLabels(true);
        ySlider.setMajorTickUnit(500);
        zSlider.setShowTickLabels(true);
        zSlider.setMajorTickUnit(500);
        sxSlider.setShowTickLabels(true);
        sxSlider.setMajorTickUnit(2.0);
        sySlider.setShowTickLabels(true);
        sySlider.setMajorTickUnit(2.0);
        szSlider.setShowTickLabels(true);
        szSlider.setMajorTickUnit(2.0);
        rxSlider.setShowTickLabels(true);
        rxSlider.setMajorTickUnit(90);
        rySlider.setShowTickLabels(true);
        rySlider.setMajorTickUnit(90);
        rzSlider.setShowTickLabels(true);
        rzSlider.setMajorTickUnit(90);

        CheckBox meshCheckBox = new CheckBox("Wireframe");

        Button resetBtn = new Button();
        resetBtn.setText("Reset");

        Button walkBtn = new Button();
        walkBtn.setText("Walk!");

        Button stopBtn = new Button();
        stopBtn.setText("Stop!");

        Button gameBtn = new Button();
        gameBtn.setText("Game Start!");

        final Accordion accordion = new Accordion();
        final TitledPane translatePane = new TitledPane("Camera Translate", new HBox(30, translateLabel, xLabel, xSlider, yLabel, ySlider, zLabel, zSlider));
        final TitledPane scalePane = new TitledPane("Camera Scale", new HBox(30, scaleLabel, sxLabel, sxSlider, syLabel, sySlider, szLabel, szSlider));
        final TitledPane rotatePane = new TitledPane("Camera Rotate", new HBox(30, rotateLabel, rxLabel, rxSlider, ryLabel, rySlider, rzLabel, rzSlider));
        final TitledPane drawModePane = new TitledPane("DrawMode & Reset & Duke Walk! & Duke Walk Game", new HBox(20, drawModeLabel, meshCheckBox, resetLabel, resetBtn, walkLabel, walkBtn, stopLabel, stopBtn, gameLabel, gameBtn, scoreLabel));

        translatePane.setAnimated(true);
        scalePane.setAnimated(true);
        rotatePane.setAnimated(true);
        drawModePane.setAnimated(true);

        accordion.getPanes().addAll(translatePane, scalePane, rotatePane, drawModePane);
        accordion.setExpandedPane(translatePane);

        root.getChildren().add(accordion);

        accordion.setLayoutX(17);
        accordion.setLayoutY(873);

        Scene scene = new Scene(root, 1910, 1020, true);
        scene.setFill(Color.PINK);

        PerspectiveCamera cam = new PerspectiveCamera(false);
        scene.setCamera(cam);

        root.getChildren().add(cam);

        // サブシーン用のサブカメラ
        PerspectiveCamera subCam = new PerspectiveCamera(true);
        // Field of View 
        subCam.setFieldOfView(45.5);
        // Clipping Planes
        subCam.setNearClip(1.0);
        subCam.setFarClip(1_000_000);

        AnchorPane subRoot = new AnchorPane();
        subRoot.setBackground(Background.EMPTY);

        Group allGroup = new Group();

        // 3D モデルをインポート
        // Duke
        ObjModelImporter dukeImporter = new ObjModelImporter();
        // Bord
        ObjModelImporter boadImporter = new ObjModelImporter();
        // 文字オブジェクト
        ObjModelImporter backImporter = new ObjModelImporter();
        // ラケット
        ObjModelImporter racketImporter = new ObjModelImporter();
        // ゲームオーバー
        ObjModelImporter game_overImporter = new ObjModelImporter();

        try {
            URL objUrl = this.getClass().getResource("Duke/duke.obj");
            dukeImporter.read(objUrl);
        } catch (ImportException e) {
            // G'catch! :p
            e.printStackTrace();
            return;
        }

        MeshView[] dukeMesh = dukeImporter.getImport();

        dukeImporter.close();

        Group dukeGroup = new Group();
        for (MeshView _dukeMesh : dukeMesh) {
            dukeGroup.getChildren().addAll(_dukeMesh);
            _dukeMesh.drawModeProperty().bind(new When(showWireframe).then(DrawMode.LINE).otherwise(DrawMode.FILL));
        }

        try {
            URL objUrl = this.getClass().getResource("Duke_base/base.obj");
            boadImporter.read(objUrl);
        } catch (ImportException e) {
            // G'catch! :p
            e.printStackTrace();
            return;
        }

        MeshView[] boadMesh = boadImporter.getImport();

        boadImporter.close();

        Group boadGroup = new Group();
        for (MeshView _boadMesh : boadMesh) {
            boadGroup.getChildren().addAll(_boadMesh);
            _boadMesh.drawModeProperty().bind(new When(showWireframe).then(DrawMode.LINE).otherwise(DrawMode.FILL));
        }

        try {
            URL objUrl = this.getClass().getResource("back_duke/back_duke.obj");
            backImporter.read(objUrl);
        } catch (ImportException e) {
            // G'catch! :p
            e.printStackTrace();
            return;
        }

        backMesh = backImporter.getImport();

        backImporter.close();

        Group backGroup = new Group();
        for (MeshView _backMesh : backMesh) {
            backGroup.getChildren().addAll(_backMesh);
            _backMesh.drawModeProperty().bind(new When(showWireframe).then(DrawMode.LINE).otherwise(DrawMode.FILL));
        }

        try {
            URL objUrl = this.getClass().getResource("Racket/racket.obj");
            racketImporter.read(objUrl);
        } catch (ImportException e) {
            // G'catch! :p
            e.printStackTrace();
            return;
        }

        MeshView[] racketMesh = racketImporter.getImport();

        racketImporter.close();

        Group racketGroup = new Group();
        for (MeshView _racketMesh : racketMesh) {
            racketGroup.getChildren().addAll(_racketMesh);
            _racketMesh.drawModeProperty().bind(new When(showWireframe).then(DrawMode.LINE).otherwise(DrawMode.FILL));
        }

        try {
            URL objUrl = this.getClass().getResource("game_over/game_over.obj");
            game_overImporter.read(objUrl);
        } catch (ImportException e) {
            // G'catch! :p
            e.printStackTrace();
            return;
        }

        MeshView[] game_overMesh = game_overImporter.getImport();

        game_overImporter.close();

        Group game_overGroup = new Group();
        for (MeshView _game_overMesh : game_overMesh) {
            game_overGroup.getChildren().addAll(_game_overMesh);
            _game_overMesh.drawModeProperty().bind(new When(showWireframe).then(DrawMode.LINE).otherwise(DrawMode.FILL));
        }

        allGroup.getChildren().addAll(dukeGroup, boadGroup, backGroup, racketGroup, game_overGroup);

        // Duke 歩行ターニングポイント
        // オフセットマージン (適当に設定)
        int offsetMargin = (int) (dukeGroup.getLayoutBounds().getDepth() / 6);
        // ターニングポイント -Z
        int bordTurnPoint = (int) (boadGroup.getLayoutBounds().getMinZ() - dukeGroup.getLayoutBounds().getMinZ());
        // ターニングポイント +Z
        int bordEndPoint = (int) (boadGroup.getLayoutBounds().getMaxZ() - dukeGroup.getLayoutBounds().getDepth() / 2 - offsetMargin);

        // ボードを非表示
        boadGroup.setVisible(false);
        // 文字を非表示
        backGroup.setVisible(false);
        // ラケットを非表示
        racketGroup.setVisible(false);
        // GAMEOVER 非表示
        game_overGroup.setVisible(false);

        // 環境光設定
        AmbientLight ambient = new AmbientLight();
        ambient.setColor(Color.rgb(150, 150, 150, 0.6));
        ambient.getScope().addAll(allGroup);

        // ポイントライト設定
        PointLight point = new PointLight();
        point.setColor(Color.rgb(255, 255, 255, 1.0));
        point.setLayoutX(-700);
        point.setLayoutY(-1_000);
        point.setTranslateZ(-1_200);
        point.getScope().addAll(allGroup);

        subRoot.getChildren().addAll(allGroup, subCam, ambient, point);

        // SubScene 作成
        SubScene subScene = new SubScene(subRoot, 1920, 860, true, SceneAntialiasing.BALANCED);
        subScene.setFill(Color.ALICEBLUE);
        subScene.setCamera(subCam);

        root.getChildren().addAll(subScene);

        // マウスで allGroup 操作用
        Rotate xRotate;
        Rotate yRotate;
        allGroup.getTransforms().setAll(
                xRotate = new Rotate(0, Rotate.X_AXIS),
                yRotate = new Rotate(0, Rotate.Y_AXIS)
        );
        xRotate.angleProperty().bind(angleX);
        yRotate.angleProperty().bind(angleY);
        subScene.setOnMousePressed(event -> {
            anchorX = event.getSceneX();
            anchorY = event.getSceneY();
            anchorAngleX = angleX.get();
            anchorAngleY = angleY.get();
        });
        subScene.setOnMouseDragged(event -> {
            angleX.set(anchorAngleX - (anchorY - event.getSceneY()));
            angleY.set(anchorAngleY + anchorX - event.getSceneX());
        });

        // サブカメラ操作用
        // 移動
        xSlider.valueProperty().bindBidirectional(xPos);
        subCam.translateXProperty().bind(xPos);
        ySlider.valueProperty().bindBidirectional(yPos);
        subCam.translateYProperty().bind(yPos);
        zSlider.valueProperty().bindBidirectional(zPos);
        subCam.translateZProperty().bind(zPos);
        // 拡大縮小
        sxSlider.valueProperty().bindBidirectional(sxPos);
        subCam.scaleXProperty().bind(sxPos);
        sySlider.valueProperty().bindBidirectional(syPos);
        subCam.scaleYProperty().bind(syPos);
        szSlider.valueProperty().bindBidirectional(szPos);
        subCam.scaleZProperty().bind(szPos);
        // 回転
        Rotate rxRotate;
        Rotate ryRotate;
        Rotate rzRotate;
        subCam.getTransforms().setAll(
                rxRotate = new Rotate(0, Rotate.X_AXIS),
                ryRotate = new Rotate(0, Rotate.Y_AXIS),
                rzRotate = new Rotate(0, Rotate.Z_AXIS)
        );
        rxRotate.angleProperty().bind(rxPos);
        ryRotate.angleProperty().bind(ryPos);
        rzRotate.angleProperty().bind(rzPos);
        rxSlider.valueProperty().bindBidirectional(rxPos);
        rySlider.valueProperty().bindBidirectional(ryPos);
        rzSlider.valueProperty().bindBidirectional(rzPos);

        // Duke モデル手足動作用
        Rotate r_legRotate;
        Rotate l_legRotate;
        Rotate r_armRotate;
        Rotate l_armRotate;
        dukeMesh[2].getTransforms().setAll(
                r_legRotate = new Rotate(0, Rotate.X_AXIS));
        dukeMesh[3].getTransforms().setAll(
                l_legRotate = new Rotate(0, Rotate.X_AXIS));
        dukeMesh[4].getTransforms().setAll(
                r_armRotate = new Rotate(0, Rotate.Z_AXIS));
        dukeMesh[5].getTransforms().setAll(
                l_armRotate = new Rotate(0, Rotate.X_AXIS));
        r_legRotate.angleProperty().bind(angleWalk_r_X);
        l_legRotate.angleProperty().bind(angleWalk_l_X);
        l_armRotate.angleProperty().bind(angleWalk_l_arm);
        r_armRotate.angleProperty().bind(angleWalk_r_arm);

        // Duke ターン用
        Rotate tyRotate;
        Rotate txRotate;
        dukeGroup.getTransforms().setAll(
                tyRotate = new Rotate(0, Rotate.Y_AXIS),
                txRotate = new Rotate(0, Rotate.X_AXIS)
        );
        tyRotate.angleProperty().bind(turnAngleY);
        txRotate.angleProperty().bind(turnAngleX);

        // GAMEOVER 回転用
        Rotate gxRotate;
        game_overGroup.getTransforms().setAll(
                gxRotate = new Rotate(0, Rotate.X_AXIS));
        gxRotate.angleProperty().bind(g_turnAngleX);

        // 表示モード
        meshCheckBox.selectedProperty().bindBidirectional(showWireframe);

        // ラケット キー操作
        root.setOnKeyPressed((KeyEvent key) -> {
            if (key.getCode() == KeyCode.L) {
                if (racketGroup.getBoundsInParent().getMaxX() < boadGroup.getBoundsInParent().getMaxX()) {
                    racketGroup.setTranslateX(racketGroup.getTranslateX() + 15);
                }
            }
            if (key.getCode() == KeyCode.J) {
                if (racketGroup.getBoundsInParent().getMinX() > boadGroup.getBoundsInParent().getMinX()) {
                    racketGroup.setTranslateX(racketGroup.getTranslateX() - 15);
                }
            }
        });

        // リセット
        resetBtn.setOnAction((ActionEvent event) -> {
            // Icon 変更
            Image myIcon = new Image(this.getClass().getResource("icon/duke_3.png").toString());
            primaryStage.getIcons().add(myIcon);

            // タイムライン、メディアプレーヤーのおまじない
            stopLine();
            // Score ラベル初期化
            scoreLabel.setText("   L Key < RIGHT >    J Key < LEFT >  Please enjoy!");
            // ポイントライト位置初期化
            point.setTranslateZ(-1_200);
            // ボード非表示
            boadGroup.setVisible(false);
            // 文字を非表示
            backGroup.setVisible(false);
            // ラケットを非表示
            racketGroup.setVisible(false);
            // GAMEOVER 非表示
            game_overGroup.setVisible(false);
            // Duke の位置を初期化
            dukeGroup.setTranslateX(0);
            dukeGroup.setTranslateZ(0);
            // Duke の向きを初期化
            turnAngleY.set(0);
            turnAngleX.set(0);
            // ラケットの初期化
            racketGroup.setTranslateX(0);
            // GAMEOVER 用回転アングル初期化
            g_turnAngleX.set(0);

            // サブカメラ位置初期化
            xPos.set(0);
            yPos.set(0);
            zPos.set(-1_000);

            sxPos.set(1.0);
            syPos.set(1.0);
            szPos.set(1.0);

            rxPos.set(0);
            ryPos.set(0);
            rzPos.set(0);

            // Duke 手足の初期化
            angleWalk_r_X.set(0);
            angleWalk_l_X.set(0);
            angleWalk_l_arm.set(0);
            angleWalk_r_arm.set(0);

            // allGroup 初期化
            anchorAngleX = 0;
            anchorAngleY = 0;
            angleX.set(0);
            angleY.set(0);
        });

        // Duke Walk 開始
        walkBtn.setOnAction((ActionEvent event) -> {
            // Icon 変更
            Image myIcon = new Image(this.getClass().getResource("icon/duke_1.png").toString());
            primaryStage.getIcons().add(myIcon);

            // タイムライン、メディアプレーヤーのおまじない
            stopLine();

            // 音楽再生、スペクトラムリスナー実装
            dance();

            // Score ラベル初期化
            scoreLabel.setText("   L Key < RIGHT >    J Key < LEFT >  Please enjoy!");
            // ポイントライト位置変更
            point.setTranslateZ(-2_500);
            // ボードを表示
            boadGroup.setVisible(true);
            // 文字を表示
            backGroup.setVisible(true);
            // ラケットを非表示
            racketGroup.setVisible(false);
            // GAMEOVER 非表示
            game_overGroup.setVisible(false);
            // Duke の位置を初期化
            dukeGroup.setTranslateX(0);
            dukeGroup.setTranslateZ(0);
            // Duke の向きを初期化
            turnAngleY.set(0);
            turnAngleX.set(0);
            // ラケットの初期化
            racketGroup.setTranslateX(0);
            // GAMEOVER 用回転アングル初期化
            g_turnAngleX.set(0);

            // 設定初期化
            azimuth = 0;
            previousHandledTime = 0L;

            // allGroup 初期化
            anchorAngleX = 0;
            anchorAngleY = 0;
            angleX.set(0);
            angleY.set(0);

            // Duke 手足の初期化
            angleWalk_r_X.set(0);
            angleWalk_l_X.set(0);
            angleWalk_l_arm.set(0);
            angleWalk_r_arm.set(0);

            // サブカメラ位置初期化
            xPos.set(0);
            yPos.set(0);
            zPos.set(-1_200);

            sxPos.set(1.0);
            syPos.set(1.0);
            szPos.set(1.0);

            rxPos.set(0);
            ryPos.set(0);
            rzPos.set(0);

            // 歩行モーション
            walkMotion();

            // 歩行
            walkTimer = new AnimationTimer() {
                @Override
                public void handle(long now) {
                    update(now);
                }

                private void update(long now) {
                    if (previousHandledTime == 0) {
                        previousHandledTime = now;
                        return;
                    }
                    // サブカメラを半径 SUBCAMERA_RADIUS の円の中心むけて回転撮影する。
                    azimuth += azimuthRateProperty.get() * (now - previousHandledTime) / 1_000_000_000;
                    previousHandledTime = now;
                    xPos.set(Math.sin(Math.toRadians(azimuth)) * SUBCAMERA_RADIUS);
                    zPos.set(-1 * Math.cos(Math.toRadians(azimuth)) * SUBCAMERA_RADIUS);
                    yPos.set(-1 * Math.sin(Math.toRadians(elevationProperty.getValue())) * SUBCAMERA_RADIUS);
                    ryPos.set(-1 * azimuth);

                    // 歩行制御
                    if (dukeGroup.getTranslateZ() > bordTurnPoint && turnAngleY.get() == 0) {
                        dukeGroup.setTranslateZ(dukeGroup.getTranslateZ() + -5);
                        if (dukeGroup.getTranslateZ() < bordTurnPoint + 1) {
                            rightTurn();
                        }
                    }

                    // 歩行制御
                    if (dukeGroup.getTranslateZ() < bordEndPoint && turnAngleY.get() == 180) {
                        dukeGroup.setTranslateZ(dukeGroup.getTranslateZ() + 5);
                        if (dukeGroup.getTranslateZ() > bordEndPoint - 1) {
                            leftTurn();
                        }
                    }

                }

                // 歩行右反転処理
                private void rightTurn() {
                    rightTurn = new Timeline(
                            new KeyFrame(
                                    new Duration(700),
                                    new KeyValue(turnAngleY, 180, Interpolator.EASE_BOTH)
                            ));
                    rightTurn.play();
                }

                // 歩行左反転処理
                private void leftTurn() {
                    leftTurn = new Timeline(
                            new KeyFrame(
                                    new Duration(700),
                                    new KeyValue(turnAngleY, 0, Interpolator.EASE_BOTH)
                            ));
                    leftTurn.play();
                }

            };
            walkTimer.start();
        });

        // 歩行ストップ
        stopBtn.setOnAction((ActionEvent event) -> {
            // Icon 変更
            Image myIcon = new Image(this.getClass().getResource("icon/duke_2.png").toString());
            primaryStage.getIcons().add(myIcon);

            // タイムライン、メディアプレーヤーのおまじない
            stopLine();
        });

        // Game 開始
        gameBtn.setOnAction((ActionEvent event) -> {

            // Icon 変更
            Image myIcon = new Image(this.getClass().getResource("icon/duke_g.png").toString());
            primaryStage.getIcons().add(myIcon);

            // タイムライン、メディアプレーヤーのおまじない
            stopLine();

            // 音楽再生、スペクトラムリスナー実装
            dance();

            // Score ラベル設定
            scoreLabel.setText("Your Score: " + score);
            // ポイントライト位置変更
            point.setTranslateZ(-2_500);
            // 歩行ベクトル設定
            d_vx = -5.0;
            d_vz = -6.0;
            // ボードを表示
            boadGroup.setVisible(true);
            // 文字を表示
            backGroup.setVisible(true);
            // ラケットを表示
            racketGroup.setVisible(true);
            // GAMEOVER 非表示
            game_overGroup.setVisible(false);
            // Duke の位置を初期化
            dukeGroup.setTranslateX(0);
            Random r = new Random();
            dukeGroup.setTranslateZ(-r.nextInt(700));
            // Duke の向きを初期化
            turnAngleY.set(0);
            turnAngleX.set(0);
            // ラケットの初期化
            racketGroup.setTranslateX(0);
            // GAMEOVER 用回転アングル初期化
            g_turnAngleX.set(0);

            // 設定初期化
            azimuth = 0;
            previousHandledTime = 0L;

            // allGroup 初期化
            anchorAngleX = 0;
            anchorAngleY = 0;
            angleX.set(0);
            angleY.set(0);

            // Duke 手足の初期化
            angleWalk_r_X.set(0);
            angleWalk_l_X.set(0);
            angleWalk_l_arm.set(0);
            angleWalk_r_arm.set(0);

            // サブカメラ位置初期化
            xPos.set(0);
            yPos.set(0);
            zPos.set(-1_200);

            sxPos.set(1.0);
            syPos.set(1.0);
            szPos.set(1.0);

            rxPos.set(0);
            ryPos.set(0);
            rzPos.set(0);

            // サブカメラ位置設定
            zPos.set(-1 * Math.cos(Math.toRadians(azimuth)) * SUBCAMERA_RADIUS);
            yPos.set(-1 * Math.sin(Math.toRadians(elevationProperty.getValue())) * SUBCAMERA_RADIUS);
            ryPos.set(-1 * azimuth);

            // ボードの大きさ
            double bord_left = boadGroup.getLayoutBounds().getMinX();
            double bord_right = boadGroup.getLayoutBounds().getMaxX();
            double bord_front = boadGroup.getLayoutBounds().getMinZ();
            double bord_back = boadGroup.getLayoutBounds().getMaxZ();

            // Duke Turn アングル
            turnAngleY.set(Math.toDegrees(Math.atan(d_vz / d_vx)));

            // 歩行モーション
            walkMotion();

            // Score 専用タイムライン
            scoreTimeline = new Timeline(
                    new KeyFrame(
                            new Duration(1_000), (ActionEvent e) -> {
                                if (score < Integer.MAX_VALUE) {
                                    score++;
                                    scoreLabel.setText("Your Score: " + score);
                                } else {
                                    scoreLabel.setText("そろそろ終わりにしたら?");
                                    score = Integer.MAX_VALUE - 1;
                                }
                            }));
            scoreTimeline.setCycleCount(Timeline.INDEFINITE);
            scoreTimeline.play();

            // 歩行
            walkTimer_g = new AnimationTimer() {

                @Override
                public void handle(long now) {
                    update(now);
                }

                private void update(long now) {
                    if (previousHandledTime == 0) {
                        previousHandledTime = now;
                        return;
                    }

                    // Duke 移動
                    dukeGroup.setTranslateX(dukeGroup.getTranslateX() + d_vx);
                    dukeGroup.setTranslateZ(dukeGroup.getTranslateZ() + d_vz);

                    // 左または右に当たったらx方向速度の符号を反転させる
                    if (dukeGroup.getBoundsInParent().getMinX() < bord_left || dukeGroup.getBoundsInParent().getMaxX() > bord_right) {
                        d_vx = -d_vx;
                        turn();
                    }

                    // ラケットに当たったらy方向速度の符号を反転させる
                    if (dukeGroup.getBoundsInParent().getMinZ() < bord_front) {
                        if (dukeGroup.getBoundsInParent().getMaxX() > racketGroup.getBoundsInParent().getMinX() && dukeGroup.getBoundsInParent().getMinX() < racketGroup.getBoundsInParent().getMaxX()) {
                            d_vz = -d_vz;
                            turn();
                        } else {
                            gameOver();
                        }
                    }

                    // 奥に当たったらy方向速度の符号を反転させる
                    if (dukeGroup.getBoundsInParent().getMaxZ() > bord_back) {
                        d_vz = -d_vz;
                        turn();
                    }
                }

                // ターンモーション
                private void turn() {
                    double h = 0;
                    if (d_vz > 0) {
                        h = -180;
                    }

                    turn_g = new Timeline(
                            new KeyFrame(
                                    new Duration(250),
                                    new KeyValue(turnAngleY, h + Math.toDegrees(Math.atan(d_vz / d_vx)), Interpolator.EASE_BOTH)
                            ));
                    turn_g.play();
                }

                // ゲームオーバー
                private void gameOver() {
                    if (scoreTimeline != null) {
                        scoreTimeline.stop();
                    }
                    if (walkTimer_g != null) {
                        walkTimer_g.stop();
                    }
                    if (turn_g != null) {
                        turn_g.stop();
                    }
                    // 転倒位置調整
                    dukeGroup.setTranslateZ(dukeGroup.getBoundsInParent().getMinZ() - 100);
                    fall = new Timeline(
                            new KeyFrame(
                                    new Duration(350),
                                    new KeyValue(turnAngleX, 140, Interpolator.EASE_BOTH)),
                            new KeyFrame(new Duration(1_000),
                                    new KeyValue(game_overGroup.visibleProperty(), true, Interpolator.EASE_BOTH)),
                            new KeyFrame(new Duration(2_000),
                                    new KeyValue(g_turnAngleX, 360, Interpolator.EASE_BOTH)
                            ));
                    fall.play();

                    // 音楽停止
                    if (mp_bgm != null) {
                        mp_bgm.stop();
                    }

                    // ゲームオーバー用音楽
                    mp_o = new MediaPlayer(o_mp);
                    mp_o.play();
                    if (dukeAnim != null) {
                        dukeAnim.stop();
                    }
                }
            };
            walkTimer_g.start();
        });

        // Icon 設定
        Image myIcon = new Image(this.getClass().getResource("icon/cludia_icon.png").toString());
        primaryStage.getIcons().add(myIcon);

        primaryStage.setTitle("Duke Walk +");
        primaryStage.setScene(scene);
        primaryStage.setResizable(false);
        primaryStage.show();
    }

    public static void main(String[] args) {
        launch(args);
    }

    // バックの文字オブジェクトのスケーリング
    @Override
    public void spectrumDataUpdate(double timestamp, double duration, float[] magnitudes, float[] phases) {
        IntStream.range(0, magnitudes.length).forEach(i -> {
            backMesh[i].setScaleX((magnitudes[i] + 60.01) * 0.1 + 1.0);
            backMesh[i].setScaleY((magnitudes[i] + 60.01) * 0.1 + 1.0);
            backMesh[i].setScaleZ((magnitudes[i] + 60.01) * 0.1 + 1.0);
        });
    }

    // いろいろ停止
    private void stopLine() {
        if (mp_bgm != null) {
            mp_bgm.stop();
        }
        if (mp_o != null) {
            mp_o.stop();
        }
        if (walkTimer != null) {
            walkTimer.stop();
        }
        if (dukeAnim != null) {
            dukeAnim.stop();
        }
        if (leftTurn != null) {
            leftTurn.stop();
        }
        if (rightTurn != null) {
            rightTurn.stop();
        }
        if (walkTimer_g != null) {
            walkTimer_g.stop();
        }
        if (turn_g != null) {
            turn_g.stop();
        }
        if (fall != null) {
            fall.stop();
        }
        if (scoreTimeline != null) {
            scoreTimeline.stop();
        }
    }

    // 音楽再生、スペクトラムリスナー実装
    private void dance() {
        audioSpectrumListener = this;
        mp_bgm = new MediaPlayer(bgm);
        mp_bgm.setAudioSpectrumListener(audioSpectrumListener);
        mp_bgm.setAudioSpectrumInterval(0.01);
        mp_bgm.setAudioSpectrumNumBands(8);
        mp_bgm.setVolume(0.9);
        mp_bgm.setAutoPlay(true);
        mp_bgm.setCycleCount(MediaPlayer.INDEFINITE);
    }

    // 歩行モーション
    private void walkMotion() {
        dukeAnim = new Timeline(
                new KeyFrame(
                        new Duration(300),
                        new KeyValue(angleWalk_r_X, -25),
                        new KeyValue(angleWalk_l_X, 25),
                        new KeyValue(angleWalk_l_arm, -20),
                        new KeyValue(angleWalk_r_arm, 12, Interpolator.EASE_BOTH)
                ),
                new KeyFrame(
                        new Duration(600),
                        new KeyValue(angleWalk_r_X, 0),
                        new KeyValue(angleWalk_l_X, 0),
                        new KeyValue(angleWalk_l_arm, 0),
                        new KeyValue(angleWalk_r_arm, 0, Interpolator.EASE_BOTH)
                ),
                new KeyFrame(
                        new Duration(900),
                        new KeyValue(angleWalk_r_X, 25),
                        new KeyValue(angleWalk_l_X, -25),
                        new KeyValue(angleWalk_l_arm, 20),
                        new KeyValue(angleWalk_r_arm, 12, Interpolator.EASE_BOTH)
                ),
                new KeyFrame(
                        new Duration(1_200),
                        new KeyValue(angleWalk_r_X, 0),
                        new KeyValue(angleWalk_l_X, 0),
                        new KeyValue(angleWalk_l_arm, 0),
                        new KeyValue(angleWalk_r_arm, 0, Interpolator.EASE_BOTH)
                )
        );
        dukeAnim.setCycleCount(Timeline.INDEFINITE);
        dukeAnim.play();
    }
}

3DViewer が動き、ボーンの埋め込まれたモデルが自由自在になめらかに動かせる時代が来ればこんなことは必要なくなるかもしれませんね。

JavaOne の Duke がどのように作られたのか?

一般の開発者にも簡単にできることなのか?

いろいろ謎はあるけど待っているだけでは面白くないよね。

ちょっと古いけど 「いつやるの?」 「今でしょ!」

最後に一言

JavaFX 楽しい! (^_^)

Hatena タグ:

JavaFX で 3D ゲームを創ってみた。

生まれて初めてゲームを創ってみた。

JavaFX を今年の夏からかじりだして興味のあるところ優先で、あーでも無い、こうでも無いと悪戦苦闘しながらも

ぼちぼちと少しずつ学習してきた。

自分の無力さを棚に上げときながら twitter で泣きをいれると親切な JavaFX マン達が助けてくれた。

感謝感激です!

初期の頃のドタバタで JavaFX 大嫌いになっていたが今は好きになっている。

楽しい!(^_^)

最近調子ぶっこいて良く解らないのに 3D をグリグリしていた。

ある人が twitter で Duke のゲームをつくってみたいと呟いていた。

楽しみにしていたが待ちきれないし、ちょうど学習用素材のアプリにゲームを載っけちゃえってノリで創ってみた。

私はゲームは今まで一度も創ったことが無い!

しかも、ゲームは好きだけど上手くない!(>_<。)

不器用なので最近の凝ったシステムのゲームはほとんど途中でリタイアしてしまう。

バイオハザード5 だけはなんとかクリアできた。

とても疲れたのでその後のシリーズはパスしてます。( ̄。 ̄;)

そう言うことなので非常にシンプルで誰でも遊べるゲームにしました。

上の文章を翻訳すると「まともなゲームの作り方は解りません。」となります。(ヲヒ

なんだかんだで出来上がったのはこんな感じのものになりました。

UI がリッチな JavaFX のはずなのにちょっと残念なことになってますが学習用につくったものに付け足したから仕方ないってことで・・・(^_^;)

JavaFX (Java 8 Build b118) で創ってあるのでインストールされている勇者様は下記リンクにプログラムをダウンロードできるようにしてありますのでお楽しみくださいませ!

https://www.dropbox.com/sh/itmywi67zb5i3gq/zMn5vf5LPm

ソースは非常にぐちゃぐちゃになって酷くなってしまったので隠蔽してます。(;´Д`)

あっ、Duke の姿がちょっと変だとは言わないでね。

歩きやすいようにしっかりした足を付けてあげたんだ。

私ってなんて優しいんだろう(ヲヒ

Hatena タグ: