Javaにおける2Dゲーム描画設計 (Imageクラスの描画)
画面にビットマップが描画されることにより、やっとゲームとしての表現が始まる感が出てくると思われます。
では、前回の流れから、コンポーネントであるJPanelクラスから派生させたMainScreenクラスにImageクラスの内容を描画する説明をしてみます。
コンポーネントの描画はイベント関数である、paintをオーバーライドして実現します。
この引数にはGraphicsクラスが渡され、このクラスに対して描画を行います。
とりあえずですが、以前設計で紹介したImageToolクラスにてビットマップファイルを読み込んだImageクラスがあることを前提とした描画のソースコードは以下のようになります。
いままでの説明ではImageクラスはキャラクター管理のタスククラスに内包されることになりますが、「とりあえず、どこかにImageクラスがある」という考えで読み進めてください。
尚、画面は256x240(ファミリーコンピュータの画素数と同じ)、画像は16x16の大きさのsample.pngというPNG形式のファイルとします。

[sample.png]
【単純なコーディング例】
// とりあえずグローバルなどこかでImageクラスが読み込まれているとします public class Global { // 描画するイメージ public static Image m_image; // コンストラクタ public Global() { m_image = ImageTool.Load("/images/sample.png"); } } /** * メインスクリーン */ public class MainScreen extends JPanel { : /** * 描画を行います * @param g コンポーネントグラフィックス */ public void paint(Graphics g) { // 背景を塗り潰します g.setColor(Color.BLACK); g.fillRect(0, 0, 256, 240); // イメージの描画 g.drawImage( Global.m_image, 0, 0, 16, 16, // 表示する矩形座標 0, 0, 16, 16, // 表示する画像の座標と大きさ null); } } |
このようなコーディングを行うと以下のような結果になります。

ここでpaint関数内のコードの説明をすると、まず最初にGraphicsクラスに対して画面を黒く塗り潰します。
これはsetColor関数にて黒色という設定を行い、fillRect関数にて設定した黒色で横256,縦240ドットの塗り潰すという流れです。
fillRect関数の引数はこの場合、スクリーンの座標の左,上,幅,高さとなります。
詳細はJavaのGraphicsクラスの仕様を参照していただくとして、この場合の座標系は(0,0)の位置が画面の左上となることを理解してください。
この塗り潰しを行わないと、この次に画像を描画する時に前に描画した内容が残ってしまい、残像のようになってしまうため、この処理を行います。
次にdrawImage関数ですが、この関数にて画像を描画します。
この場合は10個の引数を取りますが、これらは3種類の情報に区分けできます。
この例では、第1引数が描画をするImagaクラス、第2~5引数がGraphicsクラスの画面に表示する座標、第6~9引数がImageクラス内の描画すべき座標、第10引数が描画に対する通知オブジェクトとなります。このうち第10引数は今回は使用しないのでnullとします。

◇ ◇ ◇
Imageクラスの描画処理の実装が行えたら、次は描画の更新処理を行います。
この処理を行わないと、肝心なゲーム画面の表示が「運で書き変わる」事になります。
これはJavaやOSの仕様に依存している問題で、「画面の再描画」というメカニズムに起因しており、「画面を再描画するべき」とOSとJavaの仮想マシンが判断した際に、コンポーネントのpaint関数イベントが発生します。そこでゲームプログラムとしてはこれを任意に発生させる必要があります。
この方法はコンポーネント(ここではMainScreenクラス)に対して以下の関数を呼び出せば実現できます。
【単純なコーディング例】
m_component.paintImmediately(0, 0, 256 ,240);
※ここではコンポーネントをMainScreen、スクリーンサイズを256x240としています)
この呼び出し方にて画面の再描画イベントが発生し、paint関数イベントが発行されます。
では、ゲームにおける更新タイミングというのはいつになるでしょうか?
それは以前説明したゲームのメインループのタイミングとなります。
1/5(メインループの説明)に説明した内容を例にすると以下のようになります。
【単純なコーディング例】
long m_nowTime; long m_framerate = 17; while(true) { try { // 処理開始時間を取得します m_nowTime = System.currentTimeMillis(); // 入力処理、タスク、描画、サウンド等の処理 // 演算が完了したので再描画を行います m_component.paintImmediately(0, 0, 256, 240); // 処理した時間を計算して、ウエイトが必要か判断します long gap = System.currentTimeMillis() - m_nowTime; if((m_framerate - gap) < 0) { // System.out.println("処理落ち発生!!"); } else { // 処理が早く終わったので、少し待ちます Thread.sleep(m_framerate - gap); } } catch(Exception e) {} } |
この実装により約1/60に再描画依頼がVMに依頼され、コンポーネント(ここではMainScreenクラス)のpaint関数が呼び出されます。
尚、高度に再描画処理を実装したい場合にはスレッド化を用いるなどするとフレームスキップ機能等を設計することも可能です。
◇ ◇ ◇
ここまでで画像の描画の説明をしましたが、次回はいままでの説明を組合せて、Javaにおけるフレームワークをサンプルとして挙げたいと思います。
これはプログラム起動から、ゲームのメインループ、タスク管理、キャラクタータスクへの派生、キャラターの描画等を具体例としたものとなります。
Javaにおける2Dゲーム描画設計 (ウィンドウとコンポーネント)
階層をイメージるうとこのような感じとなります。

このイメージは1/9に説明した概念に基いた考えとなります。
では実際にどのように実装するか検討してみることにしましょう。ここではアプリケーション形式にて作成することを前提に説明したいと思います。
まず、Javaアプリケーションとして、プログラムインスタンスを生成する訳ですが、このプログラムインスタンスをウィンドウクラスから派生させます。
ウィンドウクラスはFrameというクラスが実現を行えるのですが、このFrameクラスはかなり使い勝手が悪いことと、OSのリソースを消費しやすいという理由から、ここではSwingコンポーネントであるJFrameクラスを用いて実現します。
(JFrameはFrameから派生したSwingコンポーネントクラスとなります)
プログラムインスタンスとウィンドウを作成する単純なコードは以下のようになります。
【単純なコーディング例】
import java.awt.*; import javax.swing.*; public class Game extends JFrame { /** * プログラムのエントリポイント * @param argvs プログラムの引数 */ public static void main(String[] argvs) { // インスタンス(ウィンドウ)を作成します Game game = new Game(); // ウィンドウの表示場所と大きさを設定します game.setBounds(100, 100, 200, 200); // ウィンドウの大きさを固定します game.setResizable(false); // ウィンドウを閉じたらプログラム終了とします game.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); // ウィンドウを表示させます game.setVisible(true); } } |
まずはこれでウィンドウが表示されます。
ここでポイントとなるのは、ゲームのウィンドウのサイズを変更可能にするか不可能にするかということです。
もしも変更可能にすると、ゲーム中でもマウスにより大きさの変更ができるために、場合により影響が出てきます。
とりあえずここでは大きさは変更できないものとして設計を進めていきます。また、とりあえず今は200x200の画面サイズとして紹介します。
次にウィンドウ内のコンポーネントの作成です。このコンポーネントがゲームの本当の画面(1/9でも説明したようにウィンドウはあくまでも画面の外側となります)となり、ここにImageクラスの情報が描画されるようになります。
先程のソースを拡張してコンポーネントをウィンドウの中に収めてみましょう。
ここではソースファイルを分けて紹介します。
また、今後は徐々にプログラムサイズが大きくなっていくので、今迄紹介していったソースファイルを盛り付けたものを添付した形で紹介していくと思います。
【単純なコーディング例】
[Game.java] import java.awt.*; import javax.swing.*; public class Game extends JFrame { /** * プログラムのエントリポイント * @param argvs プログラムの引数 */ public static void main(String[] argvs) { // インスタンス(ウィンドウ)を作成します Game game = new Game(); // ウィンドウの表示場所と大きさを設定します game.setBounds(100, 100, 200, 200); // ウィンドウの大きさを固定します game.setResizable(false); // ウィンドウを閉じたらプログラム終了とします game.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); // メインスクリーンをウィンドウに登録します MainScreen screen = new MainScreen(200, 200); game.getContentPane().add(screen, BorderLayout.CENTER); // コンポーネントを追加したので更新を行います game.getContentPane().validate(); // 内部コンポーネントの大きさにアジャストします game.pack(); // ウィンドウを表示させます game.setVisible(true); } } [MainScreen.java] import java.awt.*; import javax.swing.*; /** * メインスクリーン */ public class MainScreen extends JPanel { public Dimension m_screenSize = new Dimension(); /** * コンストラクタ * @param width スクリーン幅(単位ピクセル) * @param height スクリーン高さ(単位ピクセル) */ public MainScreen(int width, int height) { m_screenSize.width = width; m_screenSize.height = height; } /** * 推奨サイズイベント(上部で pack されたとき) * @return パックされた幅と高さ */ public Dimension getPreferredSize() { return new Dimension(m_screenSize.width, m_screenSize.height); } } |
サンプルソース 2008.02.12
今回はコンポーネントであるJPanelクラスから派生させたMainScreenというクラスを作成しました。
このコンポーネントクラスをJFrameの派生であるGameクラスの中に追加した形となります。
ここまでの作成で、Imageクラスを描画するための土台が作成できました。
次回はこのコンポーネントクラスの中にImageクラスのビットマップを描画してみたいと思います。
Javaにおける2Dゲーム描画設計 (パレット処理)
まず「パレット」とは何かというと、ビットマップの各ピクセルにおけるカラー情報(RGB)の配列となります。
(配列を絵の具のパレットと同じようなイメージで見たてます)
例えば7段階のグレースケールのパレットを用意したとします。
これをカラー情報配列を作成すると以下のようになります。
配列 | 色 | RGB | 16進 | 色 |
0 | 白 | R:255, G:255, B:255 | 0xffffff | ■ |
1 | : | R:212, G:212, B:212 | 0xd4d4d4 | ■ |
2 | : | R:170, G:170, B:170 | 0xaaaaaa | ■ |
3 | 灰色 | R:128, G:128, B:128 | 0x808080 | ■ |
4 | : | R:85, G:85, B:85 | 0x555555 | ■ |
5 | : | R:43, G:43, B:43 | 0x2b2b2b | ■ |
6 | 黒 | R:0, G:0, B:0 | 0x000000 | ■ |
これを虹の配色に変化させるとした場合
配列 | 色 | RGB | 16進 | 色 |
0 | 赤 | R:255, G:0, B:0 | 0xff0000 | ■ |
1 | 橙 | R:255, G:128, B:0 | 0xff8000 | ■ |
2 | 黄 | R:255, G:255, B:0 | 0xffff00 | ■ |
3 | 緑 | R:0, G:255, B:0 | 0x00ff00 | ■ |
4 | シアン | R:0, G:255, B:255 | 0x00ffff | ■ |
5 | 青 | R:0, G:0, B:255 | 0x0000ff | ■ |
6 | 紫 | R:255, G:0, B:255 | 0xff00ff | ■ |
のような配列を用意します。
前回MaskFilterクラスのfilterRGB関数にて
マスクカラーと一致している色の場合、"return 0"として戻り値を返しているといった具合です。 この関数で0を返すとそのピクセルは透明になります。 それ以降の処理は有効となるビットマップのピクセルをどのようにするかという判定になります。 |
ということを説明しましたが、この"return 0"を行わなかった場合以降の処理にパレット処理を行います。
具体的にコードで表わすと、以下の手順でコーディングを行います。
1.MaskFilterクラスから、パレットを持つクラスを派生させる。
2.派生させたクラスのコンストラクタにて、基底クラス(MaskFilter)の
メンバ変数m_paletteにパレット配列を設定する。
といった手順となります。
サンプルコードでは以下のような感じです。(前回のMaskFilterクラスも記述します)
【単純なコーディング例】
/** /** /** /** // パレットが無い場合は明るさのみをフィルタリング // 一致したパレットを置き換えます // 輝度モードの場合は 1 ピクセルごとに輝度を設定します /** /** // パレットをアタッチします /** // パレットをアタッチします |
この派生したクラスにより、グレースケールのビットマップを虹色のビットマップに変換するこができます。
そしてパレットでカラーを置き換えた処理の後、またはパレット処理を行わなくても、この有効ピクルに対して輝度の設定も行いつつ、更にそれに対して透明度との積を取った値を戻り値として決定しています。

そして、前々回(2/2)のImageクラスのビットマップファイル読み込み処理と併せて実現すると、
【単純なコーディング例】
/** /** /** /** mt.addImage(destImage, 1); return destImage; /** |
と前回の関数に更に任意のパレット用フィルターを引数に渡せるloadImge関数を最後に追加して完成です。
実際に呼び出すコードは至極単純で、文頭のグレースケールのピクセルで構成されたビットマップファイルを用意(ここでは"gray.png"とします)して、
【単純なコーディング例】
// グレースケールのビットマップの読み込み // 虹色のビットマップの読み込み |
というプログラムコードを記述すれば、グレースケールと、虹色のImageクラスを生成することができます。
更に色を暗くしたり半透明にしたい場合は、
【単純なコーディング例】
// 虹色を少し暗い色に // 更に半透明に |
といった具合で実現できます。
今回説明したパレット処理では指定したビットマップファイルにつき、一つのImageクラスが生成されてしまいますが、一つのImageクラスに対してリアルタイムにパレット処理を行いたい場合は、ImageToolクラスを改良して実現することも可能です。
(ただし、パレット数が多ければ多い程、オーバーヘッドが大きくなるので、この辺はゲーム内容との兼ね合いとなるかと思います)
昔のビデオゲームはハードウェアに依存した情報として色管理を行っていたものが多く、ハードウェアの色情報を変化させると、それに付随して自動的にキャラクターの色情報が高速且つ容易に変更できたのですが(逆にハードウェアの色情報を誤って設定してしまうと全ての色が変化してしまいます)、時代はパレットではなく直接ビットマップピクセルカラーを変更する方式に流れたので、このようなとても面倒な処理を実装する必要となってしまう傾向にあります。
次回は画面にImageクラスを描画する処理に入りたいと思います。
Javaにおける2Dゲーム描画設計 (マスク処理)
前回ではビットマップファイルの読み込みを行ったところまで説明しましたが、この状態では描画したくない部分(キャラクターの周りの背景部分)まで描画の対象となってしまうという現象が発生してしまいます。
この状態を改善する方法がマスク処理となります。マスク処理はビットマップ内の指定の色を描画しないという情報に書き換える処理の事を表します。
JavaのImageクラスではイメージフィルタという方法を用いると、ビットマップデータを容易に変更することが出来ます。
(他にも色々な方法がありますが、ここではこの方法で説明したいと思います)
さて、目的である「ビットマップ内の描画をしたくない部分を作成する」にはビットマップイメージをRGBの形式で操作するRGBImageFilterクラスを用いると、比較的容易に実現が行えます。
まずはサンプルソースにて実装例を挙げてみます。
【単純なコーディング例】
/** /** /** /** // パレットが無い場合は明るさのみをフィルタリング // 一致したパレットを置き換えます // 輝度モードの場合は 1 ピクセルごとに輝度を設定します |
少し長いコードですが、要点だけを抑えればおおよそシンプルに考えることができます。また、「パレット」というキーワードが記述されていますが、とりあえずこの部分は次回に説明したいと思います。
まずMaskFilterクラスはRGBImageFilterより派生した、「マスクカラー」「透明度」「明るさ」を持つクラスとなります。
コンストラクタにて引数を指定しない場合は、緑色(RGBと透明度で表現すると 0xff00ff00)をマスクカラー、透明度を完全不透明(0xff。逆に透明は0xffとなります)としています。
コンピューターにおける古くからマスクカラーにはよくマゼンダ色がマクスカラー(クロマキーとも言ったりします)にされたりするのですが、色が沈みやすく個人的に見辛いと判断したので、ここでは緑色にしました。
次にfilterRGB関数ですが、この関数はFilteredImageSourceクラスがクラスを使用して、更にComponentクラスがFilteredImageSourceを用いてこのImageクラスを生成すると、この際に1ピクセル毎に呼び出されるコールバック関数となります。
…文章ですと複雑なので、サンプルソースとイメージで説明すると以下のとおりです。
(ソースは前回のGlobal, ImageToolクラスを流用して記述します)
【呼び出し例のサンプルコード】
/** /** /** /** mt.addImage(destImage, 1); return destImage; |
この方法を用いると、Component.createImage が呼び出された時点で、MaskFilterクラスのfilterRGB関数がビットマップの1ピクセル毎に対して呼び出されます。
そして、filterRGB関数の先頭にて
public int filterRGB(int x, int y, int rgb) { : |
と判定している処理がマスク処理となります。
マスクカラーと一致している色の場合、"return 0"として戻り値を返しているといった具合です。
この関数で0を返すとそのピクセルは透明になります。
それ以降の処理は有効となるビットマップのピクセルをどのようにするかという判定になります。次回はこの有効ピクセルの情報を変化させるためのパレット処理について説明したいと思います。

これでImageクラスで読み込んだビットマップを描画処理にかけると緑の枠が表示されない状態になります。
(描画処理自体についてはまだまだ先となりますが、現時点では「まぁ、ゲームとしては表現しやすいビットマップを作成した。」という概念で話を進める事とします)
Javaにおける2Dゲーム描画設計 (ビットマップファイルの読み込み)
用意したビットマップファイルをImageクラスへ設定する方法はJDKのバージョンにより方法が色々とあるのですが、基本的にアッパーコンパチ(上位互換)の方法を取ると、多くのプラットフォームで対応できるため、ここではToolkitクラスを用いた方法を説明します。
また、このビットマップ読み込みの際、「どのような形式のビットマップファイルにするか」から始まり、「アプリケーションやアプレットによる実現」、「ディスクのパスからの読み込みやJARファイルからの読み込み」等、設計により色々なアプローチがありますが、ゲームを一つのパッケージファイルとした考えとして以下の仕様と考えて説明したいと思います。
【プログラム実行方式】
アプリケーション
【ビットマップファイル読み込み方式】
JARファイルからの読み込み
※ここでJARファイルについての説明はJARに関して知識があることを前提に説明させていただきます。
【ビットマップファイル形式】
PNG
ビットマップファイル形式を「PNG」にしているのは、"JPEGのようにビットイメージが劣化しない"、"ビットマップを圧縮する"という理由で、劣化さえしないでToolkitクラスが読み込んでくれるのであれば、特にどの形式でも構わないと思います。
よくみかけるフリーのゲームには透過属性やアニメーション属性があるGIFなどが好まれていますが、色数の問題や2000年頭まで存在した著作権問題などがあったため、好みが分れていた感があります。(そのためPNGが開発されたという経緯が生れた訳ですが...)
これらの仕様を決定したところで、ビットマップファイルの読み込み処理を考えて行きます。
まずは適当なサンプルのソースコードを挙げてから順序だてて説明行きましょう。
【単純なコーディング例】
/** /** /** /** |
ここではGlobalクラスとImageManagerクラスという2つのクラスを挙げています。
概念的なポリシーは以下の通りとなります。
Globalクラス:
プログラム実行時に生成されるシステム唯一の情報を持つクラス。
このクラスはプログラムが実行された際に、最初に生成します。
そして、画面(パネル)を生成した際、このクラスのm_componentにこの画面を設定します。
ImageToolクラス:
Imageクラスを扱うツールとなるクラス。
Imageの生成等を引き受けます。
これらは共にstatic関数を持つ汎用的なクラスとなり、プログラム上、どのクラス(タスク等)からもアクセスができます。
さて、これらのサンプルを元にビットマップの読み込みについて説明します。
◇ ◇ ◇
【JAR内にあるビットマップファイルパスの取得】
Global.getURLにてJARファイル内のファイルパスを取得します。この関数はGlobalクラスのインスタンスが生成される際に設定される、Javaのクラスローダーを元にJAR内のファイルパス(URL)を取得します。
【ビットマップファイルを読み込んだImageクラスの取得】
ImageTool.loadImageを用いて指定のパスから読み込んだImageクラスを取得します。
呼び出し例)
Image playerImage = ImageTool.loadImage("/images/player.png");
次にImageクラスのビットマップファイル読み込み処理の手順を説明します。
【実現処理の説明】
Imageクラスがビットマップを読み込むには、Toolkit, Component, MediaTrackerの3つのクラスを用いて実現します。
これらは以下フローにより実現します。
1.ToolkitによるImageクラスの生成
Imageクラスの読み込み先を設定します。Toolkit.getDefaultToolkitにてインスタンスを取得し、このインスタンスよりgetImageを用いてImageクラスを生成します(この時点ではビットマップは読み込まれていません)。
2.MediaTrackerによるImageクラスのビットマップの読み込み
MediaTrackerにComponentクラスを設定し、MediaTracker.addImageにて指定のImageクラスにビットマップを読み込んで設定します。
MediaTrackerはComponentクラスのディスプレイ情報(色数等)を元に登録されたImageクラスの読み込み先からビットマップを読み込みます。
MediaTrackerは非同期にビットマップを読み込む為、MediaTracker.waitForIDを用いてビットマップが読み込まれるまで待ちます。
(今回の設計では待つように説明していますが、一括で読み込み処理を実現する場合等では、この待ちはここで行わないように設計が可能です)
このフローにて、Imageクラスにビットマップファイルが読み込まれた状態になります。
このImageクラスを描画処理にて描画を行うと、イメージでは以下のようになります。
このままだと、描画したくないもの(この例の場合は緑の部分)まで描画されてしまいます。
これではゲームとしては問題となるので、この後は「この部分の抜き処理(マスク処理)」とその応用による「パレット処理」について順に説明しようと思います。