応用プログラミング 第8回


この授業の目的

プログラミング課題を作成するためのテンプレートとなるようなプログラムを紹介します.今回の授業では特に,ブロック崩しを例として,インタラクティブ(対話的)なグラフィクスのプログラミング手法について解説します.基本的な制御構造はこれまでの授業で説明したプログラム例をできるだけ同じ形で利用していますので,理解できない部分があった場合は遡って復習して下さい.


出席メール

出席メールで「授業中の課題の実施」を確認します.メールを出した時刻,PCの場所(アドレス)を確認します.

to:jhoshino@esys.tsukuba.ac.jp
subject: 応用プログラミング 第8回.氏名,学籍番号
本文: なし(書かなくて結構です)

 


JAVAプログラミングの応用:ブロック崩し

プログラムのソースコードは,http://www.entcomp.iit.tsukuba.ac.jp/java/8kai/src.zip にあります.

実行結果 はこちらです.

ブロック崩しを作るときには,例えば,次のような機能を追加することが必要となります.

JAVAを使ったインタラクティブなアプリケーションは右図のような構成になっています.これまでに授業でやったCanvasを使ったダブルバッファ,Listner, Adapterによるイベント受付けに,アプリケーション固有の制御機能を付け加えることで作ります.

以下の例題のプログラムは,以下のようなクラス構成になっています.

class mainApplet:初期設定やボタンの設定を行います.canvasを起動します.

class myCanvas:ダブルバッファを作成します.また,スレッドを使ってSimpleBlockクラスのメソッドを連続的に呼び出して,ゲームを動かす役割を果たします.

class SimpleBlock:ボールの動きの制御,ブロックやラケットに当たったときの制御を行うメソッドが記述してあります.右図ではゲームクラス群に相当します.今回は機能が単純なのでクラスが一つしかありませんが,複数のクラスに分割して記述することもできます.

class MyMouseMotionAdapter:マウスの動きを読みとります.

 


mainApplet.java

mainApplet.javaは第6回の授業のサンプルプログラムとほぼ同じですが,違うところを赤色で示します.

import java.applet.*;
import java.awt.*;
import java.awt.event.*;


/*
<applet code = "mainApplet" width = 860 height = 600>
</applet>
*/ public class mainApplet extends Applet implements ActionListener{ myCanvas canvas; SimpleBlock SB; Button b[]; public void init(){ setLayout(null); setBackground(Color.white); SB = new SimpleBlock(); canvas = new myCanvas(SB); //Canvasの定義 canvas.setBounds(10,10,220,320); //Applet上のCanvasの位置と大きさを指定する add(canvas); SB.ball = getImage(getDocumentBase(),".//image//ball.gif"); SB.init(this,canvas); b = new Button[3]; //ボタンの定義 b[0] = new Button("start"); b[1] = new Button("stop"); b[2] = new Button("init"); for(int i=0;i < 3;i++){ b[i].addActionListener(this); b[i].setBounds(280+i*40,120,40,20); b[i].addActionListener(this); add(b[i]); } canvas.start(); } public void actionPerformed(ActionEvent e){ if(e.getSource() == b[0]){ canvas.waitFlag = false; }else if(e.getSource() == b[1]){ canvas.waitFlag = true; }else if(e.getSource() == b[2]){ SB.init(this,canvas); } } }

SimpleBlockクラスの変数宣言

まず変数宣言で特徴的な部分を説明します.MediaTrackeクラスはイメージが読み込まれるまで待つ機能を提供しているクラスです.getImageメソッドでは,メソッドを呼び出したときに画像を読み込むのではなくdrawImageメソッド等で呼び出されたときに画像を読み込んでいます.この場合,画像が表示されないという事態が発生することがありますので,MediaTrackerクラスを用いて画像を必ず表示するようにします.今回はブロックをくずすためのボールに画像を用いています.それ以外のものはコメントに書いているので間単に読んでください.


  MediaTracker mt;
  Image ball; // 表示するボールの絵を入れる変数
  int num = 3; // ボールの残数
  int bx, by; // ボールの位置(x座標、y座標)
  int rx = 120, ry = 280; // ラケットの位置(x座標、y座標)
  int ballWidth = 16, ballHeight = 16; // ボールの絵の幅、高さ
  int racketWidth = 40, racketHeight = 8; // ラケットの絵の幅、高さ
  int margin = 10; // 端まで行かないよう余裕を取る
  int score; // 点数
  boolean blockFlag[] = new boolean[30]; // ブロックがあるかどうかの配列
  int blockX[] = new int[30]; // ブロックのx座標
  int blockY[] = new int[30]; // ブロックのy座標
  int blockWidth = 16, blockHeight = 12; // ブロックの幅、高さ
  int oldx=0;
  Thread kicker = null; // アニメーションのためのスレッド変数
  int SPEED = 4; //ボールの進む量
  int dx,dy;//ボールの進む量(x増分、y増分)
  Dimension d; //Canvasのサイズ

initメソッドの中身

initメソッドではSimpleBlockクラスの機能を初期化します.一般的なゲームを作る際に初期化をするメソッドをゲーム開始の初期化だけでなくGameOver後の再スタート等にも利用できるように書いておくとコードの削減にもなって便利です.このメソッドではブロックの表示,ボールの残数,ボールの位置,スコア−を初期化しています.mt.addImage(ball,0)でMediaTrackerオブジェクトに画像とID番号を登録します.

  public void init(Applet myApplet,myCanvas canvas){
    d = canvas.getSize();
    dx = SPEED;
    dy = SPEED;

    mt = new MediaTracker(canvas);
    mt.addImage(ball,0);

    //ブロックの位置(x、y座標)の設定
    int k = 0;  //ブロック番号
    int yy; // 段毎のy座標
    for(int i = 0 ; i < 3 ; i++ ){
      yy = i * (blockHeight+3) + margin*3 ;
      for(int j = 0; j < 10 ; j++ ){
        blockX[k] = j * (blockWidth+4) + margin +2 ;
        blockY[k] = yy;
        blockFlag[k] = true; // ブロックがある
        k += 1;
      }
    }

    num = 3;
    score = 0;

    /* ボールの初期位置の設定 */
    bx = margin + (int)(Math.random() * (float)(d.width - (margin*2+ballWidth+1)));
    by = 150;
  }

Canvas.java

次にmyCanvas.javaも前回と異なるところを赤色で示しています.ダブルバッファなどの基本的な機能も,これまでの授業で説明していますので復習しておいて下さい.

主な違いはMouseMotionListenerが追加されていることです.MyMouseMotionAdapterクラスではSimpleBlockオブジェクトの参照を受け取っています.mouseMovedメソッドはマウスが移動している間位置を取得し続けるので,ブロックを打ち返すためのラケットの動作を制御するのに用いています.

/*
  myCanvas.java
*/
import java.awt.event.*;
import java.awt.*;
//ダブルバッファリングを備えたCanvasの作成
public class myCanvas extends Canvas implements Runnable{
  Thread th;
  Image buffer;
  Graphics bufferg;
  boolean waitFlag;
  SimpleBlock SB;
  public myCanvas(SimpleBlock SB){
    this.SB = SB;
    waitFlag = false;
    th = new Thread(this);
    addMouseMotionListener(new MyMouseMotionAdapter(SB));
  }
  public void start(){
    Dimension d =  getSize();
    buffer = createImage(d.width,d.height);
    th.start();
  }
  public void run(){
    try{
      while(true){
        if(!waitFlag){
          repaint();
          SB.run();
        }
        th.sleep(30);
      }
    }catch(Exception e){}
  }

  public void update(Graphics g){
    paint(g);
  }
  public void paint(Graphics g){
      if(bufferg == null) bufferg = buffer.getGraphics();
      Dimension d = getSize();
      SB.drawBackGround(bufferg,d);  //背景,ボール,ブロック,ラケット,スコア−の描画
      SB.drawBall(bufferg,this);
      SB.drawBlock(bufferg);
      SB.drawRacket(bufferg);
      SB.drawScore(bufferg);
      g.drawImage(buffer,0,0,this);
  }
}

class MyMouseMotionAdapter extends MouseMotionAdapter{
  SimpleBlock SB;
  public MyMouseMotionAdapter(SimpleBlock SB){
    this.SB = SB;
  }
  public void mouseMoved(MouseEvent e){
      SB.mouseMoved(e);    //マウスの動きを読みとります.
  }
}

SB.run()メソッド

SB.run()メソッド について順に説明していきます.
runメソッドの中身は

 public void run(){
    /*全てのイメージの読み込みを待つ */
    try{
      mt.waitForID(0);   //ID番号0の画像が読み込まれるまで待機します
    } catch (InterruptedException e) {}

    racketColProcess();  //ボールとラケットとの衝突処理
    wallColProcess();    //ボールと壁との衝突処理
  blockColProcess(); //ボールとブロックの衝突処理 bx += dx; by += dy; }

のようになってます.mt.waitForID(0)でID番号0の画像が読み込まれるまで待機します.
ブロックくずしなのでボールがラケットや壁やブロックに衝突した場合の処理を考える必要があります.ボールとラケットとの衝突処理を表すracketColProcessメソッド,ボールと壁との衝突処理を表すwallColProcessメソッド,ボールとブロックの衝突処理を行うblockColProcessが呼び出されています.このプログラムでは以下のような反射モデルを参考に作られています.

これらを見ながらメソッドの中身を見てもらえば理解できると思います.
bx += dy,by += dyは次の位置にボールを進めています.

その他のメソッド

SB.drawBackGround,SB.drawBall,SB.drawBlock,SB.drawRacket,SB.drawScore,ではそれぞれ背景,ボール,ブロック,ラケット,スコア−の描画を行っているのでソースの中身を簡単に見てください.

 


SimpleBlock.java

SimpleBlock.javaの内容を以下に示します.

import java.awt.*;
import java.applet.*;
import java.awt.event.*;
import java.net.*;

public class SimpleBlock{
  MediaTracker mt;
  Image ball; // 表示するボールの絵を入れる変数
  int num = 3; // ボールの残数
  int bx, by; // ボールの位置(x座標、y座標)
  int rx = 120, ry = 280; // ラケットの位置(x座標、y座標)
  int ballWidth = 16, ballHeight = 16; // ボールの絵の幅、高さ
  int racketWidth = 40, racketHeight = 8; // ラケットの絵の幅、高さ
  int margin = 10; // 端まで行かないよう余裕を取る
  int score; // 点数
  boolean blockFlag[] = new boolean[30]; // ブロックがあるかどうかの配列
  int blockX[] = new int[30]; // ブロックのx座標
  int blockY[] = new int[30]; // ブロックのy座標
  int blockWidth = 16, blockHeight = 12; // ブロックの幅、高さ
  int oldx=0;
  Thread kicker = null; // アニメーションのためのスレッド変数
  int SPEED = 4; //ボールの進む量
  int dx,dy;//ボールの進む量(x増分、y増分)
  Dimension d; //Canvasのサイズ

  public void init(Applet myApplet,myCanvas canvas){
    d = canvas.getSize();
    dx = SPEED;
    dy = SPEED;

    mt = new MediaTracker(canvas);
    mt.addImage(ball,0);

    //ブロックの位置(x、y座標)の設定
    int k = 0;  //ブロック番号
    int yy; // 段毎のy座標
    for(int i = 0 ; i < 3 ; i++ ){
      yy = i * (blockHeight+3) + margin*3 ;
      for(int j = 0; j < 10 ; j++ ){
        blockX[k] = j * (blockWidth+4) + margin +2 ;
        blockY[k] = yy;
        blockFlag[k] = true; // ブロックがある
        k += 1;
      }
    }

    num = 3;
    score = 0;

    /* ボールの初期位置の設定 */
    bx = margin + (int)(Math.random() * (float)(d.width - (margin*2+ballWidth+1)));
    by = 150;
  }

  public void mouseMoved(MouseEvent e){
    rx = e.getX();	// ラケットの位置の更新
    /* ラケットがコートを出ないための処理 */
    if ( rx < margin ) rx = margin;
    if ( rx + racketWidth > d.width - margin ) rx = d.width - margin -racketWidth;
  }

  public void run(){
    /*全てのイメージの読み込みを待つ */
    try{
      mt.waitForID(0);
    } catch (InterruptedException e) {}

    racketColProcess();
    wallColProcess();
   blockColProcess();

    bx += dx;
    by += dy;
  }

  // ブロックに当たったときの処理
  public void blockColProcess(){
    for(int i = 0 ; i < 30 ; i++ ){
      if ( blockFlag[i] == true ) {
        if ( by + ballHeight >= blockY[i] && by <= blockY[i]+blockHeight
              && bx + ballWidth >= blockX[i] && bx <= blockX[i]+blockWidth ){
           dy = - dy;   // ブロックに当たったら反転
           score += 1; // 得点の加算
           blockFlag[i] = false; // ブロックを消す
        }
      }
    }
  }
  // ボールがラケットに当たったときの処理
  public void racketColProcess(){
    if ( by + ballHeight >= ry && by + ballHeight <= ry+racketHeight && bx + ballWidth >= rx && bx <= rx+racketWidth ) {
      dy = -SPEED; // ラケットに当たったら上へ返す
      if ( bx < rx || bx + ballWidth > rx + racketWidth ) { // ラケットの端に当たった時
        oldx = dx;
        if ( oldx == 0 ) { // 垂直に来たボール
          if ( bx < rx ) dx = -SPEED;  // 左端に当たったら左斜め上に返す
          else if ( bx + ballWidth > rx + racketWidth ) dx = +SPEED;  // 右端に当たったら右斜め上に返す
        } else dx = 0;  // 斜めに来たボールは垂直に返す
      }
    }
  }
  // 左端、右端、上端に来たときの壁との衝突処理
  public void wallColProcess(){
    if (bx < 0 + margin ) dx = SPEED; // 左端に来たら反転
    else if (bx + ballWidth > d.width - margin) dx = -SPEED;  // 右端に来たら反転
    else if (by < 0 + margin) dy = SPEED;// 上端に来たら反転
    else if ( by + ballHeight > d.height - margin ) {  // ラケットの下へ行ったときの処理
      // 下端に来たらボールを初期位置へ
      bx = margin + (int)(Math.random() * (float)(d.width - (margin*2+ballWidth+1)));
      by = 150;
      --num;// = num - 1; // ボールの残数を減らす
    }
  }
  public void drawBackGround(Graphics g,Dimension d){
     g.setColor(Color.orange);
     g.fillRect(0,0,d.width, d.height);
     g.setColor(Color.green);
     g.fillRect(margin,margin,d.width-margin*2, d.height-margin*2);
  }

  public void drawBall(Graphics g,Canvas canvas){
    /* 読み込み中メッセージの表示 */
    if (mt.checkID(0) == false) {
      g.setColor(Color.black);
      g.fillRect(0, 0, d.width, d.height);
      g.setColor(Color.yellow);
      g.setFont(new Font("TimesRoman", Font.PLAIN, 14));
      g.drawString("Loading...", 40, 50);
      return;
    }
    else g.drawImage(ball, bx, by,canvas); // ボールを描く
  }

  public void drawBlock(Graphics g){
    /* ブロックを描く */
    for(int i = 0 ; i < 30 ; i++ ){
      if ( i < 10 ) g.setColor(Color.blue);// 1段目は青
      else if ( 10 <= i && i < 20 ) g.setColor(Color.red); // 2段目は赤
      else if ( 20 <= i ) g.setColor(Color.pink); // 3段目はピンク

      if ( blockFlag[i] == true )
        g.fillRect(blockX[i], blockY[i], blockWidth, blockHeight);  // ブロックがあれば、ブロックを描く
    }
  }
  public void drawRacket(Graphics g){
    /* ラケットを描く */
    g.setColor(Color.white);
    g.fillRect(rx, ry, racketWidth, racketHeight);
  }
  public void drawScore(Graphics g){
    g.setColor(Color.black);
    g.drawString("Score : "+score, 24, 24);  // 点数の表示
    if ( num <= 0 ) {   //ボールの残数が無くなった場合
      g.setColor(Color.red);
      g.drawString("GAME OVER !", 40, 160);
    }else if ( score == 30 ) {  // 全てのブロックをくずした場合
      g.setColor(Color.black);
      g.drawString("PERFECT !", 40, 160);
    }
  }
}

 

実習

ブロック崩しのプログラムをコンパイルして機能を確認して下さい.

プログラムのソースコードは,~jhoshino/public_html/java/8kai/src にあります.

   cp ~jhoshino/public_html/java/8kai/src/*.java  自分の作業ディレクトリ名

でコピーすることができます.画像ファイルは~jhoshino/public_html/java/8kai/src/image/にあります.

 


練習課題

1)ブロック崩しのプログラムを改良して,ブロックの数が減るに従ってボールのスピードが早くなるように変更して下さい.

2)プログラミング課題(自由課題)で制作予定のアプリの候補案を1つに決めて、a) 名称(アプリの名前)、b) 目的(何を実現したいか、c)基本機能(画面構成やどのような機能を実現するか), d)クラス構成などを設計して簡単に説明して下さい。最終的に変わっても構いません。

提出先のアドレスはjhoshino@esys.tsukuba.ac.jpです.Subject欄に「応用プログラミング 第8回課題,氏名,学籍番号」を書いて下さい.本文にソースコード,実行結果,考察を記載して下さい.

締め切りは次の授業の開始時まで.ソースコードはメール本文へのコピーあるいは添付どちらでも結構です.