• 検索結果がありません。

PDFファイル 20112 ゲーム制作雑誌 がまぐ!

N/A
N/A
Protected

Academic year: 2018

シェア "PDFファイル 20112 ゲーム制作雑誌 がまぐ!"

Copied!
218
0
0

読み込み中.... (全文を見る)

全文

(1)
(2)

ライセンスについて

ライセンスについて

 このたびは「がまぐ!」をお手にとって下さってありがとうございます。

 この雑誌を使用するに当たっていくつかお願いがありますのであらかじめお読み

下さい。

本文とプログラムについて

 本文、画像、プログラムは「クリエイティブコモンズ 表示 2.1 日本」です。

 出典を明らかにする限り自由に使って構いません。

出典:がまぐ! 2011/2

雑誌全体について

 雑誌本体の PDF ファイル、画像データ、冊子形式の物を含めて、自由にコピーし

て配布して構いません。このとき、コピー・印刷に必要な代金を取っても構いません。

……が、高い値段をつけても、無料に手に入る物を買ってくれる人がいるかどうかは

謎です。

サポートについて

サポートについて

 記事の内容についてのお問い合わせ、ご意見等はサポートページで承ります。直

接著者や編集者に言うのもありです……が、どちらにしても必ず帰ってくるかどうか

は判りません(すみません)。

 記事の訂正、バグ修正などもサポートページで行いますので、定期的に見て下さ

ると助かります。

サポートサイト

https://sites.google.com/site/gamagreader/

ダウンロードコンテンツに関して

ダウンロードコンテンツに関して

 ツール・サンプル等のダウンロードできるコンテンツに関しては、その作者のサ

ポートとなります。こちらでも確認しますが、要望、報告等は直接作者へお願いします。

(3)

目次

ライセンスについて...2

サポートについて...2

ダウンロードコンテンツに関して...2

巻頭言「楽しいゲームつくろうぜ!」...4

特集:ゲームデザイン...5

ゲームデザインということ...6

どこがゲームなの?...14

ボードゲームでも作ってみようか!...42

たのしいということ。...51

足し算ゲームは終わったのか?...55

フロー理論とたんぽぽ...62

脳内麻薬とゲーム...68

クラスでゲームを作ってみよう。...76

月面着陸ゲーム完全版全ソース...89

特別企画:ケモノ学園のレベルデザイン....96

「ケモノ学園」のケモノができるまで。...102

プログラミング TIPS:ジャンプ...109

3Dポリゴンの制作...113

簡易スクリプトエンジン@HSP3...120

空母戦 SLG の制作(第 1 回)...135

Luarida の紹介...146

投稿ゲームと自薦ゲームの紹介...158

ランニングゲーム...159

洞窟探検...163

疑似 3D デモ...172

Hopping...188

「実写でボクシング」の紹介...193

プログラムの楽な打ち込み方...194

ツールのダウンロードとインストールの仕方. 199

著作権について 2011/2...208

作ったらどう見せるか?...212

めざせ! 明日のスタークリエイター...215

ゲーム紹介の募集...216

筆者紹介...217

編集後記...218

奥付...218

(4)

巻頭言「楽しいゲームつくろうぜ!」

巻頭言「楽しいゲームつくろうぜ!」

 ここにゲームがあったとします。

 学校が終わって、帰りがけにゲーム屋さんに寄って買ってきました。学校の帰りに

ゲーム屋さん何か行くなって? まあまあ、固いこと言わずに。発売日に買って、早速

遊びたいのですから、学校サボって並びにいかなかっただけまだいいと自分に言い

聞かせましょう。

 早速ソフトをゲーム機に刺してゲームを開始! 初めは雑誌で見ていた期待通り

の素敵なムービーや人物紹介、ゲームのチュートリアルなどが流れていきますが、あ

る時あなたは気づいてしまいました。

 もしかして、これ、面白くないんじゃなかろうか、と。

 ゲームの世界は 2 種類に大きく分けることができます。面白いゲームと、つまらな

いゲームです。面白いゲームはやっているともっと続けたくなりますが、つまらない

ゲームはやっていて虚しくなってきます。「俺は何でこんなものをやってるんだろう」と。

 次の日、仲間にそのゲームの話をします。しかし、話はかみ合いません。彼らはあな

たがつまらなかったゲームを面白かったと言っています。

 あなたは間違っているのでしょうか?

 結論から言いましょう。何も間違ったことなどありません。あなたにとってつまらな

かったゲームはあなたにとっては「クソゲー」です。他の人が何といおうと。逆に、他の

人が誰も支持しないゲームでも面白くてしかたがないことだってあります。何もおか

しくありません。

 先ほど、ゲームは 2 種類あるという話をしました。しかし、この分類は非常に明確

なものですが、すべての人が同じ分類をするわけではありません。人にとってはクソ

ゲーになるものでも別の人から見て名作に見えることも、その逆だって平気で起こり

ます。ただ、自分から見たときにどちらかに属するのは間違いないでしょう。

 それがなぜ起きるのか、今回は考えてみたいと思います。

 今回の特集は「ゲームデザイン」です。この場合の「デザイン」は見かけのことでは

ありません。ゲームという形、機能などを考えることすべてを示しています。

 なぜこのゲームはつまらないのか、では、どうすればゲームは面白くなるのか。もっ

というと、どうすれば面白いゲームを作れるのか。

 それを考えるだけでも、十分ゲームは楽しいのです。クソゲーだと思った感触を否

定する理由はどこにもありません。ただ、「なぜクソゲーになってしまったのか」を考え

るのはきっと楽しいことでしょう。

がまぐ! 編集長  @117Florian

(5)

特集 特集 : : ゲームデザイン ゲームデザイン

@117Florian

今回の特集で決めたこと

 今回の「がまぐ!」は特集として「ゲームデザイン」を取り上げています。

 実は、この特集を書くに当たって、執筆している筆者みんなにあらかじめ伝えたこ

とがあります。

 それは、「ゲーム性」と言う言葉を使わないということです。

 「ゲーム性」という言葉は使われ始めた当初は意味のある言葉だったのかもしれ

ませんが、今となってはあまりに意味が広くなりすぎてしまって、ある物事を説明する

のには不向きな言葉になってしまいました。下手に「ゲーム性」と言ってしまうと、誰も

がみんな(書いている人も、読んでいる人も)自分の中の「ゲーム性」を持ち出して混

すること

け合いです。

 巻頭言に書いたとおり、クソゲーかそうじゃないかは個人的な感触で大きく変わっ

てきます。すべての人が納得できるクソゲーなんてものはありませんし、誰がやっても

面白いゲームもまたありません。同じように、下手に「ゲーム性」という言葉を使ってし

まうと何も言わないうちに終わってしまいそうなので、それは嫌だと思いました。

 実際、2011 年のお正月は、ゲーム業界関連の人たちは「ゲーム性」という「言葉」

に対して大変盛り上がりました。ゲーム内容ではなくて、言葉に対して、です。普段

ゲームを作っている人たちでもそうなのですから、今から作ろうとしている皆さんが混

乱 することは避けられないでしょう。

実装主義!

 もう一つ、「可能な限り実装を伴わないデザインはしない」というのをルールとして

めました。

 デザイン自体は理屈だけでも成り立ちます。理屈をみているとそれが何となく面白

そうに見える瞬間は確かにきます。

 でも、実際に手を動かして作ってみないとそれが正しいかどうかは判りません。な

ので、いろんな手段でとにかく「実装する」ということにこだわりました。実装と言って

もプログラムとは限りません。プログラムは、たまたまコンピュータゲームを作るのに

向 いている手段ではありますが、ゲームデザインを実装するには唯一の手段ではあ

りません。なので、「こんなのゲームじゃないよ」と思わずに、実際に手を動かして楽し

んでみてください。

 見えてくるものがきっとあると思います。

(6)

ゲームデザインというこ

ゲームデザインというこ

@117Florian

最初に

 乱暴に言ってしまうと、「ゲームデザイ

ン」というのは、「楽しませるためにゲー

ムの形を考える」作業です。それ以上で

もそれ以下でもありません。

ゲーム

 ゲームのジャンルはとても増えてきま

した。ただ一口「ゲーム」と言ったからと

いって画面が想像できるとは限りません。

どう見たってゲームには見えないにも関

わらず、とても面白いゲームだって存在

します。

 ゲームは、既存のゲームに似た見かけ

をしているからゲームなのではありませ

ん。どんなにチップチューンがかつての

ゲームの郷愁を誘う音色だからといって、

これをゲームだと言う人はいないでしょ

う。ポリゴン数、頂点数の少ないコン

ピュータグラフィックだからといって、映

画をゲームと言う人もいませんし、剣と魔

法 のファンタジー小説を全部ゲームだと

も言いません。

 もちろん、世界が「そう言わない」から

それは「ゲームではない」と言い切れる

ものではありません。ただ、作っている側

がこれを「ゲームだ」と思っているものは

多 くの場合ゲームで、その上で遊んでい

る側がこれは「ゲームだ」と思っていれば

ゲームと考えて良さそうです。

 では、ゲームをゲームにしている正体

はなんでしょうか?

ゲームをゲームにしている正体

 まずは、作り手側の「楽しませよう」と

いう意志でしょう。ゲームデザインはその

意志をプレイヤーに伝えるための方法で

す。

 ただ、残念ながら意志だけあれば伝わ

るわけではありません。単に会話や議論

をするのならば。ずっと話を続けていれば、

「楽しませたいんだ」という気分だけは伝

わるでしょう。でも、それはゲームではあ

りません。だって、プレイヤーには「意図」

は伝わったかもしれませんが、実際に楽

しんでいませんので。

 「ゲーム」というメディアを使って楽しま

せることができなければ、意志だけあっ

てもダメなのです。何とかして、形にしな

いと。

単純でつまらないゲーム

 ここで一つ簡単なゲームを作りましょ

う。ちょっとクラスも多く、打ち込む部分は

多 いのですが、NetBeans で「Java アプ

リケーション」を新規作成して打ち込んで

動 かしてみてください。プロジェクト名は

「JackPot」。主クラスは作らずに打ち込

みましょう。実行時は「JackPotFrame」

を主クラスとして指定して下さい。

(7)

SynchronizedList.java

import java.util.ArrayList; import java.util.List;

/** *

* @author tsuchimoto */

public class SynchronizedList<E> extends ArrayList<E> {

private boolean m_IsLocked=false;

private List<E> m_AddList=new ArrayList<E>(); private List<E> m_RemoveList=new ArrayList<E>();

public void lock() {

m_IsLocked=true; }

public void unlock() {

for (E anElement : m_AddList) {

super.add(anElement); }

m_AddList.clear();

for (E anElement : m_RemoveList) {

super.remove(anElement); }

m_RemoveList.clear(); m_IsLocked=false; }

@Override

public boolean add(E anElement) {

if (m_IsLocked) {

m_AddList.add(anElement); return true;

} else {

return super.add(anElement); }

}

@Override

public boolean remove(Object anElement) {

if (m_IsLocked) {

m_RemoveList.add((E)anElement); return true;

} else {

return super.remove(anElement); }

}

}

GameObject.java

import java.awt.Graphics; import java.util.List;

/** *

* @author tsuchimoto */

public interface GameObject {

public static final int KEYIN_UP=0x1; public static final int KEYIN_RIGHT= 0x2; public static final int KEYIN_DOWN = 0x4; public static final int KEYIN_LEFT = 0x8; public static final int KEYIN_ENTER=0x10; public static final int KEYIN_SPACE=0x20;

void move(List<GameObject> gameObjects,int key); void paint(Graphics aGraphics);

}

Player.java

import java.awt.Color; import java.awt.Graphics; import java.util.List;

/** *

* @author tsuchimoto */

public class Player implements GameObject {

public int m_X=320; public int m_Y=240;

private static final int VELOCITY=4; public boolean m_IsMissed;

public void move(List<GameObject> gameObjects, int key)

{

if ((key & KEYIN_UP) != 0) {

m_Y -= VELOCITY; }

if ((key & KEYIN_DOWN) != 0) {

m_Y += VELOCITY; }

if ((key & KEYIN_LEFT) != 0) {

m_X -= VELOCITY; }

if ((key & KEYIN_RIGHT) != 0)

(8)

{

m_X += VELOCITY; }

for (GameObject aGameObject : gameObjects) {

if (aGameObject instanceof Treasure) {

Treasure aTreasure=(Treasure)aGameObject; if (aTreasure.isCollision(m_X, m_Y)) {

gameObjects.remove(aGameObject); break;

} }

if (aGameObject instanceof Enemy) {

Enemy anEnemy=(Enemy)aGameObject; if (anEnemy.isCollision(m_X, m_Y)) {

m_IsMissed=true; break;

} } } }

public void paint(Graphics aGraphics) {

int radius=8;

aGraphics.setColor(Color.BLUE);

aGraphics.fillRect(m_X-radius, m_Y-radius, radius*2, radius*2);

} }

Enemy.java

import java.awt.Color; import java.awt.Graphics; import java.util.List;

/** *

* @author tsuchimoto */

public class Enemy implements GameObject {

private int m_X=50; private int m_Y=240;

private static final int VELOCITY=4;

public boolean isCollision(int x,int y) {

return (Math.max(Math.abs(m_X-x),Math.abs(m_Y- y) ) < 16);

}

public void move(List<GameObject> gameObjects, int key)

{

Player aPlayer=null;

for (GameObject aGameObject : gameObjects)

{

if (aGameObject instanceof Player) {

aPlayer=(Player)aGameObject; }

}

int sign= VELOCITY;

m_X += (aPlayer.m_X == m_X) ? 0 :

((aPlayer.m_X < m_X) ? -1 : 1) * sign; m_Y += (aPlayer.m_Y == m_Y) ? 0 :

((aPlayer.m_Y < m_Y) ? -1 : 1) * sign; }

public void paint(Graphics aGraphics) {

aGraphics.setColor(Color.RED);

aGraphics.fillRect(m_X-8, m_Y-8, 16, 16); }

}

Treasure.java

import java.awt.Color; import java.awt.Graphics; import java.util.List;

/** *

* @author tsuchimoto */

public class Treasure implements GameObject {

private int m_X; private int m_Y;

public Treasure() {

m_X=(int)(Math.random()*640); m_Y=(int)(Math.random()*480); }

public boolean isCollision(int x,int y) {

return (Math.max(Math.abs(m_X-x),Math.abs(m_Y- y) ) < 16);

}

public void move(List<GameObject> gameObjects, int key)

{ }

public void paint(Graphics aGraphics) {

aGraphics.setColor(Color.YELLOW); aGraphics.fillRect(m_X-4, m_Y-4, 8, 8); }

}

(9)

GameView.java

import java.awt.Color; import java.awt.Dimension; import java.awt.Font; import java.awt.Graphics;

import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.KeyEvent; import java.awt.event.KeyListener; import java.util.ArrayList; import java.util.List; import javax.swing.JComponent; import javax.swing.JFrame; import javax.swing.Timer;

/** *

* @author tsuchimoto */

public class GameView extends JComponent implements KeyListener,ActionListener

{

private JFrame m_Controller; private Timer m_Timer; private int m_Key; private Player m_Player;

private State m_State=State.PLAYING;

private static enum State {

PLAYING, GAMEOVER, CLEAR, };

private SynchronizedList<GameObject> m_GameObjects=new SynchronizedList<GameObject>();

public GameView(JFrame aFrame) {

this.setPreferredSize(new Dimension(640,480)); this.setDoubleBuffered(true);

m_Controller=aFrame; this.addKeyListener(this); m_Timer=new Timer(100,this); m_Timer.start();

this.setFocusable(true); this.requestFocus();

m_Player=new Player(); m_GameObjects.add(m_Player); m_GameObjects.add(new Enemy()); for (int i=0;i<8;i++)

{

m_GameObjects.add(new Treasure()); }

}

@Override

public void paintComponent(Graphics aGraphics) {

aGraphics.setColor(Color.DARK_GRAY); aGraphics.fillRect(0, 0, 640, 480);

for (GameObject aGameObject : m_GameObjects) {

aGameObject.paint(aGraphics); }

switch(m_State) {

case PLAYING: break; case CLEAR:

aGraphics.setColor(Color.BLUE); aGraphics.setFont(new Font(Font.SERIF, Font.PLAIN, 32));

aGraphics.drawString("Clear!!", 200, 240);

break; case GAMEOVER:

aGraphics.setColor(Color.RED);

aGraphics.setFont(new Font(Font.SERIF, Font.PLAIN, 32));

aGraphics.drawString("Game Over", 200, 240);

break; }

}

public void keyTyped(KeyEvent e) {

}

public void keyPressed(KeyEvent e) {

switch(e.getKeyCode()) {

case KeyEvent.VK_UP:

m_Key |= GameObject.KEYIN_UP; break;

case KeyEvent.VK_LEFT:

m_Key |= GameObject.KEYIN_LEFT; break;

case KeyEvent.VK_DOWN:

m_Key |= GameObject.KEYIN_DOWN; break;

case KeyEvent.VK_RIGHT:

m_Key |= GameObject.KEYIN_RIGHT; break;

case KeyEvent.VK_SPACE:

m_Key |= GameObject.KEYIN_SPACE; break;

case KeyEvent.VK_ENTER:

m_Key |= GameObject.KEYIN_ENTER; break;

} }

public void keyReleased(KeyEvent e) {

switch(e.getKeyCode()) {

case KeyEvent.VK_UP:

m_Key &= ~GameObject.KEYIN_UP; break;

(10)

case KeyEvent.VK_LEFT:

m_Key &= ~GameObject.KEYIN_LEFT; break;

case KeyEvent.VK_DOWN:

m_Key &= ~GameObject.KEYIN_DOWN; break;

case KeyEvent.VK_RIGHT:

m_Key &= ~GameObject.KEYIN_RIGHT; break;

case KeyEvent.VK_SPACE:

m_Key &= ~GameObject.KEYIN_SPACE; break;

case KeyEvent.VK_ENTER:

m_Key &= ~GameObject.KEYIN_ENTER; switch(m_State)

{

case GAMEOVER: case CLEAR:

m_Controller.add(new TitleView(m_Controller));

m_Controller.remove(this); m_Controller.pack(); m_Controller.repaint(); break;

} break; }

}

public void actionPerformed(ActionEvent e) {

switch(m_State) {

case PLAYING: int treasurecount=0; m_GameObjects.lock();

for (GameObject aGameObject : m_GameObjects) {

aGameObject.move(m_GameObjects, m_Key); if (aGameObject instanceof Treasure) {

treasurecount++; }

}

m_GameObjects.unlock(); if (m_Player.m_IsMissed) {

m_State=State.GAMEOVER; }

if (treasurecount ==0) {

m_State=State.CLEAR; }

}

repaint(); }

}

TitleView.java

import java.awt.Color; import java.awt.Dimension;

import java.awt.Font; import java.awt.Graphics; import java.awt.event.KeyEvent; import java.awt.event.KeyListener; import java.awt.event.MouseEvent; import java.awt.event.MouseListener; import javax.swing.JComponent; import javax.swing.JFrame;

/** *

* @author tsuchimoto */

public class TitleView extends JComponent implements KeyListener

{

private JFrame m_Controller;

public TitleView(JFrame aFrame) {

this.setPreferredSize(new Dimension(640,480)); this.setDoubleBuffered(true);

m_Controller=aFrame; this.addKeyListener(this); this.setFocusable(true); this.requestFocus(); }

public void paintComponent(Graphics aGraphics) {

aGraphics.setColor(Color.BLACK); aGraphics.fillRect(0, 0, 640, 480); aGraphics.setColor(Color.WHITE); aGraphics.setFont(new Font(Font.SERIF, Font.PLAIN, 100));

aGraphics.drawString("JackPot", 100,240);

aGraphics.setColor(Color.RED);

aGraphics.setFont(new Font(Font.SERIF, Font.PLAIN, 32));

aGraphics.drawString("Press Enter to start", 300,340);

}

public void keyTyped(KeyEvent e) {

}

public void keyPressed(KeyEvent e) {

switch(e.getKeyCode()) {

case KeyEvent.VK_ENTER: m_Controller.add(new GameView(m_Controller));

m_Controller.remove(this); m_Controller.pack(); m_Controller.repaint(); break;

}

(11)

}

public void keyReleased(KeyEvent e) {

} }

JackPotFrame.java

import javax.swing.JFrame; import javax.swing.SwingUtilities; import javax.swing.WindowConstants;

/** *

* @author tsuchimoto */

public class JackPotFrame extends JFrame {

public JackPotFrame() {

this.add(new TitleView(this)); this.pack();

this.setTitle("JackPot");

this.setDefaultCloseOperation(WindowConstants.EXI T_ON_CLOSE);

this.setVisible(true); }

public static void main(String[] args) {

SwingUtilities.invokeLater( new Runnable() {

public void run() {

new JackPotFrame(); }

}); } }

• 画面上にプレイヤー一人。プレイ

ヤーはカーソルキーで動く

• 画面上に敵一人。普段はプレイ

ヤーを追ってくる。敵に捕まった

ら負け

• 画面上に宝物たくさん。これを全

部取るとクリア

 どうですか? 控え目に言っても、あま

り面白くはないと思います。

 このゲームを作るに当たって、私はこ

んなことを考えました。

• 敵から逃げるのがドキドキ

• そこを掻い潜って宝物を集めると

どんどん画面がキレイに

• 全部集めると今までの敵を見返せ

てムフン

 絶妙の片仮名使いや、単純きわまりな

いルール等々、突っ込みどころたっぷり

ですが、少なくともゲームを遊んでいる

限りこれらが実現されているとは言えま

せん。

ちょっとは考えてみたゲーム

 なぜ実現されていないのでしょうか?

 楽しませようという意志はたしかにあ

ります。言われてみると、ぱっと見にはな

んとなくその意志も実現されているよう

にも見えます。

 が、まず、ドキドキしません。プレイヤー

と敵が同じ速さで動いているので一瞬

で捕まってしまいます。スリルがあると言

えば 聞こえはいいですが、何をしてもうま

くいかないゲームなんか早々に飽きてし

まいます。

 そこで、ルールを追加します。

• 敵はプレイヤーよりちょっと遅い

Enemy.java

public class Enemy implements GameObject

(12)

{

private int m_X=50; private int m_Y=240;

private static final int VELOCITY=2;

 どうでしょう。ちょっとは遊べるようにな

りました。けど、今度は楽勝過ぎます。ま

ず滅多なことでは負けません。

もう少し考えてみたゲーム

 大体において、最初から最後までずっ

と同じドキドキというのが飽きてしまう原

因 のような気がします。そこで、さらにもう

つルールを加えます。

• 宝物をとったときに、それをしま

う時間がかかる。持っている宝物

が多ければ多いほど時間は余計に

かかる

Player.java

public class Player implements GameObject {

public int m_Y=240;

private int m_FreezeCount=0; private int m_CurrentFreezeCount=0;

public void move(List<GameObject> gameObjects, int key)

{

if (m_CurrentFreezeCount == 0) {

if ((key & KEYIN_UP) != 0) {

m_Y -= VELOCITY; }

if ((key & KEYIN_DOWN) != 0) {

m_Y += VELOCITY; }

if ((key & KEYIN_LEFT) != 0) {

m_X -= VELOCITY; }

if ((key & KEYIN_RIGHT) != 0) {

m_X += VELOCITY; }

} else {

m_CurrentFreezeCount--; }

for (GameObject aGameObject : gameObjects) {

if (aGameObject instanceof Treasure) {

Treasure aTreasure=(Treasure)aGameObject; if (aTreasure.isCollision(m_X, m_Y)) {

gameObjects.remove(aGameObject); m_FreezeCount += 10;

m_CurrentFreezeCount=m_FreezeCount; break;

} }

if (aGameObject instanceof Enemy) {

Enemy anEnemy=(Enemy)aGameObject; if (anEnemy.isCollision(m_X, m_Y)) {

m_IsMissed=true; break;

} } } }

public void paint(Graphics aGraphics) {

int radius=8;

if (m_CurrentFreezeCount > 0) {

radius = (m_CurrentFreezeCount %10) * 2; }

aGraphics.setColor(Color.BLUE);

aGraphics.fillRect(m_X-radius, m_Y-radius, radius*2, radius*2);

}

 するとどうでしょう。今までただの作業

だったのが急にスリルが出てきました。

 ついでに遊んでいる最中に戦略をた

てる必要も出てきます。はじめは敵の存

在 を気にせずに取っていけたのが、その

うち、「まず敵を引き離してから取る」とか、

「画面縁近くのものは後から取る」とか

考え はじめました。

ゲームになるか

 さらに、このままもう一つルールを追加

しましょう。

• ゲーム時間を計って、ゲーム終了

時にそれを出す

(13)

GameView.java

public class GameView extends JComponent implements KeyListener,ActionListener

{

private JFrame m_Controller; private Timer m_Timer; private int m_Key; private int m_TimerCount;

public void paintComponent(Graphics aGraphics) {

aGraphics.setColor(Color.DARK_GRAY); aGraphics.fillRect(0, 0, 640, 480);

for (GameObject aGameObject : m_GameObjects) {

aGameObject.paint(aGraphics); }

switch(m_State) {

case PLAYING:

aGraphics.setColor(Color.WHITE); aGraphics.setFont(new Font(Font.SERIF, Font.PLAIN, 16));

aGraphics.drawString("Time:" + m_TimerCount, 300, 16);

break; case CLEAR:

aGraphics.setColor(Color.BLUE); aGraphics.setFont(new Font(Font.SERIF, Font.PLAIN, 32));

aGraphics.drawString("Clear!!", 200, 240);

aGraphics.setColor(Color.WHITE);

aGraphics.drawString("Your Time is : " + m_TimerCount, 200, 340);

break; case GAMEOVER:

aGraphics.setColor(Color.RED);

aGraphics.setFont(new Font(Font.SERIF, Font.PLAIN, 32));

aGraphics.drawString("Game Over", 200, 240);

break; }

}

public void actionPerformed(ActionEvent e) {

switch(m_State) {

case PLAYING: int treasurecount=0; m_GameObjects.lock();

for (GameObject aGameObject : m_GameObjects) {

aGameObject.move(m_GameObjects, m_Key); if (aGameObject instanceof Treasure) {

treasurecount++;

} }

m_GameObjects.unlock(); m_TimerCount++;

if (m_Player.m_IsMissed) {

m_State=State.GAMEOVER; }

if (treasurecount ==0) {

m_State=State.CLEAR; }

}

repaint(); }

 さて、何度か遊んでみて気づいたで

しょうか?

 私は一言も「この数字は小さいほどい

い」とは言っていません。にもかかわらず、

遊んでいるうちに数字をなるべく小さくし

よ うとしている自分に気づいたのではな

いでしょうか?

 小さい数字が出れば嬉しいですし、前

よ りも大きいとどうすればいいのか考え

るはずです。

 これが、ゲームデザインです。

ゲームをデザインする

 繰り返しましょう。

 ゲームは、なにか既存のゲームに似た

見かけをしているからゲームなのではあ

りません。作り手が楽しませようとして作

り、プレイヤーが楽しんでこそのゲームな

のです。

 ゲームデザインには、今のところ唯一

絶対 の成功する手段は見つかっていな

いようです。

 ですが、これはチャンスです。

 皆さんが作ったゲームが面白くなる可

能性 だって十分あるということですから。

(14)

どこがゲームなの?

どこがゲームなの?

@PetitCafeSoft

はじめに

  こんにちは。今回はプログラムの技術

を学ぶことはひとまずおいて、サンプルプ

ログラムを打ち込んで遊んでみることで、

そもそもゲームとは何か、その中身につ

いて考えていこうと思います。

 この記事は、読者のみなさんが以下の

環境 を用意して、実際にプログラムを打

ち込むことを前提にしています。前号に

掲載 された XNA の記事が参考になりま

す。もちろん、これらがそろっていなくても

読むことはできますので、手元に

W indows が搭載されたコンピュータの

ない人も気軽に読んでみてください。

• “Microsoft WindowsXP, Vista,

7”のいずれかが搭載されたコン

ピュータ。

• “Microsoft Visual C# 2010

Express”(Microsoft のサイトか

ら無償でダウンロードできます。

または有償版の“Visual Studio

2010 Professional”でもかまいま

せん)。

• “Microsoft XNA4.0”(これも無

償です。なお WindowsXP ではス

タンドアロン版のみ対応となって

いますが、それでかまいません。

また日本語パッチを当てておくと

よいでしょう)。

「ブロックくずし」って知って

ます?

 今から 30 年以上前(!)に ATARI と

いうアメリカの会社が『Breakout』とい

うゲームを作りました。俗に「ブロックくず

し」と呼ばれています。日本でも任天堂を

初め、たくさんのメーカーがこの「ブロッ

クくずし」を

もほう

模倣( つまりマネ)しています。

今回 の例題はこの「ブロックくずし」です。

まずはソースコード(プログラムを書いた

文字列、記事の後ろの方にあります)を

打 ち込んでみてください。なおリソース

ファイルの bar.png は丸いですが、ちゃ

んと横長のバーになります)

Program.cs

using System;

namespace BlockGame {

#if WINDOWS || XBOX static class Program {

/// <summary>

/// アプリケーションのメイン エントリー ポイント です。

/// </summary>

static void Main(string[] args) {

using (BlockGame game = new BlockGame()) {

game.Run(); }

} }

#endif }

Constants.cs

static class Constants {

#region 定数

public const int numBalls = 5;

public const int numHitCheckPerTick = 4; public const int screenWidth = 800;

(15)

public const int screenHeight = 480; public const int fieldLeft = (screenWidth - fieldWidth) / 2;

public const int fieldWidth = 384; public const int wallThick = 16;

public const int barTop = screenHeight - barHeight * 2;

public const int barHeight = 16;

public const int barWidth = barHeight * 6; public const int barInnerWidth = barWidth - barHeight;

public const int barEdgeWidth = (barWidth - barInnerWidth) / 2;

public const int ballDiameter = 16;

public const float ballRadius = ballDiameter / 2f;

public const int numBlockRows = 6; public const int numBlockColumns = 12; public const int blockWidth = 32; public const int blockHeight = 12;

public const int blocksTop = wallThick + 48;

public const float initialBallPositionX = fieldLeft + ballDiameter * 2;

public const float initialBallSpeedMagnitude = 3f;

public const float initialBallSpeedTheta = (float)(Math.PI / 3);

public const float maxBallSpeedMagnitude = blockHeight * 2;

public const float ballSpeedAccelerate = (maxBallSpeedMagnitude - initialBallSpeedMagnitude) / 120f;

#endregion #region フィールド

public static readonly Rectangle fieldRect = new Rectangle(

fieldLeft, wallThick, fieldWidth,

screenHeight - wallThick);

#endregion }

}

BlockGame.cs

using Microsoft.Xna.Framework;

using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Input;

namespace BlockGame {

public class BlockGame : Microsoft.Xna.Framework.Game {

#region フィールド

readonly GraphicsDeviceManager graphics;

bool reset = true; Bar bar;

Ball ball;

Blocks blocks; Wall wall;

MouseState mouseState; SpriteBatch spriteBatch; #endregion

#region プロパティ

public MouseState MouseState {

get { return mouseState; } }

internal Bar Bar {

get { return bar; } }

internal Ball Ball {

get { return ball; } }

internal Blocks Blocks {

get { return blocks; } }

internal Wall Wall {

get { return wall; } }

#endregion

#region コンストラクタ

public BlockGame() {

graphics = new GraphicsDeviceManager(this); Content.RootDirectory = "Content";

}

#endregion #region メソッド

/// <summary>

/// リソース(テクスチャ)のロード /// </summary>

protected override void LoadContent() {

spriteBatch = new SpriteBatch(GraphicsDevice);

(16)

//

var barTex = Content.Load<Texture2D>("bar"); var ballTex =

Content.Load<Texture2D>("ball"); var blockTex = Content.Load<Texture2D>("block"); var wallTex = Content.Load<Texture2D>("wall");

//

bar = new Bar(this, barTex); ball = new Ball(this, ballTex);

blocks = new Blocks(this, blockTex); wall = new Wall(this, wallTex); }

/// <summary> /// 初回リセット /// </summary> void Reset() {

bar.Reset(); ball.Reset(); blocks.Reset();

reset = false; }

protected override void Update(GameTime gameTime) {

if (reset) {

Reset(); }

//

mouseState = Mouse.GetState();

//

bar.Update(); ball.Update();

if (ball.OutOfBounds) {

// ミス Exit(); }

if (blocks.ExistsCount == 0) {

// クリア ball.Stop(); }

//

base.Update(gameTime); }

protected override void Draw(GameTime gameTime) {

GraphicsDevice.Clear(Color.Black);

//

spriteBatch.Begin(SpriteSortMode.Deferred, null, SamplerState.PointWrap/*.LinearWrap*/, null, null);

// 壁の描画

wall.Draw(spriteBatch);

// バーの描画

bar.Draw(spriteBatch);

// ボールの描画 ball.Draw(spriteBatch);

// ブロックの描画 blocks.Draw(spriteBatch); spriteBatch.End();

//

base.Draw(gameTime); }

#endregion }

}

Bar.cs

using Microsoft.Xna.Framework;

using Microsoft.Xna.Framework.Graphics;

namespace BlockGame {

class Bar {

#region フィールド

readonly BlockGame game; readonly Texture2D tex; Vector2 pos;

#endregion #region プロパティ

public Vector2 Position {

get { return pos; } }

#endregion

#region コンストラクタ

public Bar(BlockGame game, Texture2D tex) {

this.game = game;

(17)

this.tex = tex; }

#endregion #region メソッド

public void Reset() {

pos = new Vector2(0, Constants.barTop); }

public void Update() {

// ※1・ここから修正

var ballPos = game.Ball.Position; pos.X = MathHelper.Clamp(ballPos.X + (Constants.ballDiameter - Constants.barWidth) / 2, Constants.fieldRect.Left, Constants.fieldRect.Right - Constants.barWidth); ;

// ※1・ここまで }

public void Draw(SpriteBatch spriteBatch) {

// バーの描画

var texWidth = tex.Width; var texHeight = tex.Height;

// バーの左端 spriteBatch.Draw( tex,

new Rectangle((int)pos.X, (int)pos.Y, Constants.barEdgeWidth, Constants.barHeight),

new Rectangle(0, 0, texWidth / 2, texHeight),

Color.White); // バーの内側 spriteBatch.Draw( tex,

new Rectangle((int)pos.X + Constants.barEdgeWidth, (int)pos.Y,

Constants.barInnerWidth, Constants.barHeight), new Rectangle(texWidth / 2, 0, 0, texHeight),

Color.White); // バーの右端 spriteBatch.Draw( tex,

new Rectangle((int)pos.X + Constants.barEdgeWidth + Constants.barInnerWidth, (int)pos.Y, Constants.barEdgeWidth, Constants.barHeight), new Rectangle(texWidth / 2, 0, texWidth / 2, texHeight),

Color.White); }

#endregion }

}

Ball.cs

using System;

using Microsoft.Xna.Framework;

using Microsoft.Xna.Framework.Graphics;

namespace BlockGame {

class Ball {

#region フィールド

readonly BlockGame game; readonly Texture2D tex;

Vector2 pos = new Vector2(0, Constants.fieldRect.Bottom);

Vector2 speed; #endregion #region プロパティ

public Vector2 Position {

get { return pos; } }

public Vector2 Speed {

get { return speed; } }

public bool OutOfBounds {

get { return (pos.Y >= Constants.fieldRect.Bottom); } }

#endregion

#region コンストラクタ

public Ball(BlockGame game, Texture2D tex) {

this.game = game; this.tex = tex; }

#endregion #region メソッド

public void Reset() {

pos = new

Vector2(Constants.initialBallPositionX, Constants.blocksTop + Constants.blockHeight * (Constants.numBlockRows + 2));

speed = new

Vector2((float)Math.Cos(Constants.initialBallSpeedTheta), (float)Math.Sin(Constants.initialBallSpeedTheta)) *

(18)

Constants.initialBallSpeedMagnitude; }

public void Update() {

if (OutOfBounds) {

// 画面外なので更新しない return;

}

var bar = game.Bar;

// ループによるヒットチェック var speedPerHitCheck = speed / Constants.numHitCheckPerTick;

for (var k = 0; k < Constants.numHitCheckPerTick; k++) {

var nextHitCheckPos = pos + speedPerHitCheck;

#region 左右の壁

if ((speedPerHitCheck.X < 0) && (nextHitCheckPos.X <= Constants.fieldRect.Left)) {

// 左の壁で反転

var t = (Constants.fieldRect.Left - nextHitCheckPos.X) / speedPerHitCheck.X; // スピード / 壁 までの距離

nextHitCheckPos.X = pos.X + t * speedPerHitCheck.X; // 壁までtだけ進める speedPerHitCheck.X =

-speedPerHitCheck.X; // 反転

nextHitCheckPos.X += (1.0f - t) * speedPerHitCheck.X; // 壁から残る(1-t)進める }

else if ((speedPerHitCheck.X > 0) && (nextHitCheckPos.X + Constants.ballDiameter >= Constants.fieldRect.Right))

{

// 右の壁で反転

var t = (Constants.fieldRect.Right - Constants.ballDiameter - nextHitCheckPos.X) /

speedPerHitCheck.X;

nextHitCheckPos.X = pos.X + t * speedPerHitCheck.X;

speedPerHitCheck.X = -speedPerHitCheck.X;

nextHitCheckPos.X += (1.0f - t) * speedPerHitCheck.X;

}

#endregion

#region 上の壁とバー

if ((speedPerHitCheck.Y < 0) && (nextHitCheckPos.Y <= Constants.fieldRect.Top))

{

// 上の壁で反転

var t = (Constants.fieldRect.Top - pos.Y) / speedPerHitCheck.Y;

nextHitCheckPos.Y = pos.Y + t * speedPerHitCheck.Y;

speedPerHitCheck.Y = -speedPerHitCheck.Y;

nextHitCheckPos.Y += (1.0f - t) * speedPerHitCheck.Y;

} else {

var barPos = bar.Position;

if (

(speedPerHitCheck.Y > 0) && (pos.Y +

Constants.ballDiameter <= barPos.Y)

&& (nextHitCheckPos.Y + Constants.ballDiameter >= barPos.Y))

{

/* ボールの中心がエッジの左端なら 0、右端なら1.0fになる。 */

var amount = (nextHitCheckPos.X + Constants.ballRadius - barPos.X) / Constants.barWidth;

if (0 <= amount && amount <= 1.0f)

{

#region バーで反転

var t = (barPos.Y - (nextHitCheckPos.Y + Constants.ballDiameter)) / speedPerHitCheck.Y;

nextHitCheckPos = pos + t * speedPerHitCheck;

// ※2・ここから修正 var cosTheta = speedPerHitCheck.X / speedPerHitCheck.Length(); // ※2・ここまで

var speedMagnitude = Math.Min(speed.Length() + Constants.ballSpeedAccelerate, Constants.maxBallSpeedMagnitude);

speedPerHitCheck = new Vector2(cosTheta, -(float)Math.Sqrt(1.0f - cosTheta * cosTheta)) * speedMagnitude /

Constants.numHitCheckPerTick;

//

nextHitCheckPos += (1.0f - t)

* speedPerHitCheck;

#endregion }

} }

#endregion

(19)

#region ブロックとのヒット判定

{

var blocks = game.Blocks;

int hitCol = -1; int hitRow = -1; var minBallToBlockLen = float.MaxValue;

var hitBlockType = BlockType.Broken;

/* 4隅を判定し、まだ残っているブロッ クで最も近いものを探す。 */

for (var j = 0; j < 2; j++) {

// ブロック群内での隅の座標 var yInBlocks =

((nextHitCheckPos.Y - Constants.blocksTop +

Constants.ballDiameter * j) / Constants.blockHeight); var row =

(int)Math.Floor(yInBlocks);

for (var i = 0; i < 2; i++) {

// ブロック群内での隅の座標 var xInBlocks =

((nextHitCheckPos.X - Constants.fieldRect.Left + Constants.ballDiameter * i) / Constants.blockWidth); var col =

(int)Math.Floor(xInBlocks);

if (

(col >= 0) && (col < Constants.numBlockColumns)

&& (row >= 0) && (row < Constants.numBlockRows))

{

var blockType = blocks[row, col];

if (blockType != BlockType.Broken)

{

var blockToBall = new Vector2(xInBlocks - (col + 0.5f), yInBlocks - (row + 0.5f));

/* 距離の計算は正確に はsqrt(dx * dx + dy * dy)だがこれで足りる。 */

var ballToBlockLen = Math.Abs(blockToBall.X) + Math.Abs(blockToBall.Y); if (ballToBlockLen < minBallToBlockLen)

{

hitCol = col; hitRow = row; minBallToBlockLen

= ballToBlockLen;

hitBlockType = blockType;

} } }

} }

//

if (minBallToBlockLen < float.MaxValue)

{

// ブロックにヒット

blocks.BreakOne(hitRow, hitCol);

// ボールの中央のブロック内での座 標

var centerInHitBlock = new Vector2(

((nextHitCheckPos.X - Constants.fieldRect.Left + Constants.ballRadius) / Constants.blockWidth - hitCol),

((nextHitCheckPos.Y - Constants.blocksTop + Constants.ballRadius) / Constants.blockHeight) - hitRow);

// if (

((speedPerHitCheck.X < 0) && (centerInHitBlock.X >= 1f))

|| ((speedPerHitCheck.X > 0)

&& (centerInHitBlock.X <= 0))) {

// ずれて当たったのでスピード のXを反転

speedPerHitCheck.X = -speedPerHitCheck.X;

}

if (

((speedPerHitCheck.Y < 0) && (centerInHitBlock.Y >= 1f))

|| ((speedPerHitCheck.Y > 0)

&& (centerInHitBlock.Y <= 0))) {

// ずれて当たったのでスピード のYを反転

speedPerHitCheck.Y = -speedPerHitCheck.Y;

} } }

#endregion

// ボールの座標を更新 pos = nextHitCheckPos; }

//

speed = speedPerHitCheck * Constants.numHitCheckPerTick;

}

public void Stop() {

speed = Vector2.Zero; }

(20)

public void Draw(SpriteBatch spriteBatch) {

if (OutOfBounds) {

// 画面外なので描画しない return;

}

spriteBatch.Draw( tex,

new Rectangle((int)pos.X, (int)pos.Y, Constants.ballDiameter, Constants.ballDiameter), Color.White);

}

#endregion }

}

Blocks.cs

using Microsoft.Xna.Framework;

using Microsoft.Xna.Framework.Graphics;

namespace BlockGame {

enum BlockType {

Broken, Red, Orange, Yellow, Green, Cyan, Blue, Purple, Silver, Max }

class Blocks {

#region フィールド

static readonly Color[] typeColors = new Color[] {

new Color(), Color.Red, Color.Orange, Color.Yellow, Color.Green, Color.Cyan, Color.Blue, Color.Purple, Color.Silver, };

readonly BlockGame game; readonly Texture2D tex; int existsCount;

readonly BlockType[,] typeTable = new BlockType[Constants.numBlockRows,

Constants.numBlockColumns]; #endregion

#region プロパティ

public int ExistsCount {

get { return existsCount; } }

internal BlockType this[int row, int col] {

get { return typeTable[row, col]; } }

#endregion

#region コンストラクタ

public Blocks(BlockGame game, Texture2D tex) {

this.game = game; this.tex = tex; }

#endregion #region メソッド

public void Reset() {

for (var row = 0; row < Constants.numBlockRows; row++) {

var type = (BlockType)(row + 1);

for (var col = 0; col < Constants.numBlockColumns; col++) {

typeTable[row, col] = type; }

}

existsCount = Constants.numBlockRows * Constants.numBlockColumns;

}

public void BreakOne(int row, int col) {

typeTable[row, col] = BlockType.Broken; existsCount--;

}

public void Draw(SpriteBatch spriteBatch) {

for (var row = 0; row < Constants.numBlockRows; row++) {

var y = Constants.blocksTop + row * Constants.blockHeight;

(21)

for (var col = 0; col < Constants.numBlockColumns; col++) {

var type = typeTable[row, col];

if (type != BlockType.Broken) {

var x = Constants.fieldRect.Left + col * Constants.blockWidth;

var color = typeColors[(int)type];

var blockRect = new Rectangle(x, y, Constants.blockWidth, Constants.blockHeight);

spriteBatch.Draw(tex, blockRect, color);

} } } }

#endregion }

}

Wall.cs

using Microsoft.Xna.Framework;

using Microsoft.Xna.Framework.Graphics;

namespace BlockGame {

class Wall {

#region フィールド

readonly BlockGame game; readonly Texture2D tex; #endregion

#region コンストラクタ

public Wall(BlockGame game, Texture2D tex) {

this.game = game; this.tex = tex; }

#endregion #region メソッド

public void Draw(SpriteBatch spriteBatch) {

// 上の壁 spriteBatch.Draw( tex,

new Rectangle(Constants.fieldRect.Left - Constants.wallThick, Constants.fieldRect.Top -

Constants.wallThick, Constants.fieldRect.Width + 2 *

Constants.wallThick, Constants.wallThick), new Rectangle(0, 0,

((Constants.fieldRect.Width / Constants.wallThick) + 2) * tex.Width, tex.Height),

Color.White); // 左の壁 spriteBatch.Draw( tex,

new Rectangle(Constants.fieldRect.Left - Constants.wallThick, Constants.fieldRect.Top,

Constants.wallThick, Constants.fieldRect.Height + Constants.wallThick),

new Rectangle(0, 0, tex.Width,

((Constants.fieldRect.Height / Constants.wallThick) + 1)

* tex.Width),

Color.White); // 右の壁 spriteBatch.Draw( tex,

new Rectangle(Constants.fieldRect.Right, Constants.fieldRect.Top, Constants.wallThick,

Constants.fieldRect.Height + Constants.wallThick), new Rectangle(0, 0, tex.Width,

((Constants.fieldRect.Height / Constants.wallThick) + 1)

* tex.Width),

Color.White); }

#endregion }

}

bar.png

ball.png

block.png

(22)

wall.png

 ……おつかれさまでした。打ち込んだ

らさっそくテストプレイしてみてください。

画面下部のバーをマウスで操作し、ボー

ルを打ち返して全てのブロックを壊せば

クリアです。えっ、バーが勝手に動くじゃ

ないかって? はい、そういうプログラム

です。それじゃまずいだろうという人は、

// ※1・ここから修正

とある行の下を、

// マウス座標に応じてバーの座標を更新 var mouseState = game.MouseState; pos.X = MathHelper.Clamp(mouseState.X,

Constants.fieldRect.Left, Constants.fieldRect.Right - Constants.barWidth);

と打ち直してみてください。バーをマウス

で動かせるようになりました。あくまでサ

ンプルということで、一度ミスしたらゲー

ムオーバーです。

 遊んでみてどうでしょう。なにぶんサン

プルですから、星が飛び散るといったに

ぎ やかな演出はありませんが……えっ、

それ以前に「ボールの動きが単調で

ゲームになっていない」ですか。では

// ※2・ここから修正

とある行の下を、以下のように書き直して

みてください。

// 速度を修正 float cosTheta;

if (speedPerHitCheck.X < 0) {

if (amount <= 0.6f) {

// バーの左と中央 amount = amount / 0.6f; cosTheta =

MathHelper.Lerp((float)Math.Cos(Math.PI * 11 / 12), (float)Math.Cos(Math.PI * 8 / 12), amount);

} else {

// バーの右

amount = (amount - 0.6f) / 0.4f; cosTheta =

MathHelper.Lerp((float)Math.Cos(Math.PI * 4 / 12), (float)Math.Cos(Math.PI * 1 / 12), amount); }

} else {

if (amount <= 0.4f) {

// バーの左

amount = amount / 0.4f; cosTheta =

MathHelper.Lerp((float)Math.Cos(Math.PI * 11 / 12), (float)Math.Cos(Math.PI * 8 / 12), amount);

} else {

// バーの右と中央

amount = (amount - 0.4f) / 0.6f; cosTheta =

MathHelper.Lerp((float)Math.Cos(Math.PI * 4 / 12), (float)Math.Cos(Math.PI * 1 / 12), amount); }

}

これでようやく「一応、遊べるゲームに

なった」と言えるのではないでしょうか。

どこを遊んでいるの?

 ゲームになっていないとか、遊べる

ゲームになったと言いましたが、では先

ほ どの修正の前と後では、いったい何が

違うのでしょうか。そう、「バーで打ち返し

た時の、ボールのはね方が違う」のです

ね 。たったそれだけのことで、あんがい印

が違うものです。

 簡単なことです。じつは最初に打ち込

んだプログラムのままだと、ボールのは

ね方 が常に一定です。そのため、何度遊

んでも、誰が遊んでも、全く同じゲーム展

開になってしまうのです。誰がやっても同

じになってしまっては、ゲームとは言えま

せん。ただバーを移動させるだけの作業

です。

 改造することでボールのはね方に変

(23)

化 がついて、遊ぶごとに、また遊ぶ人に

よ って違う展開が起こるようになりました。

打 ち返したボールはまたしばらくしてプレ

イヤーのもとに戻ってきますが、当然どこ

に戻ってくるかは毎回違います。その違

う場所に飛んできたボールを、またプレイ

ヤ ーが打ち返します…。この繰り返しで

バラエティに富んだ展開が起こりえます。

 「プレイヤーが操作するとゲーム上で

変化 が起こる。さらにそれに応じた操作

をすることで別の変化が起こるという

“ やり取り”が繰り返される」

 このことは、ゲームにおいて大切なポ

イントなのです。難しい言葉ですが、「イ

ンタラクティブ性」とか「双方向性」など

と言ったりします。ふだんゲームで遊んで

いる時、私たちは無意識にそうした「やり

取り」を楽しんでいるのではないでしょう

か。

 よく知られたゲームを例に考えてみま

しょう『スーパーマリオ 64』以降の 3D

のマリオシリーズは、ただマリオを操作し

ているだけでもいろいろな動きを楽しめ

ます。さらにはマリオを移動させることで、

クリボーやノコノコなどの他のキャラク

ターもそれに反応して動きます。向かっ

てくるクリボーに、あなたのマリオも反応

するはずです。

別のもの見えてきた!

 もう少し考えを進めてみましょう。少し

遊べば気がつくことですが、ブロックくず

しはボールを斜めに打つほど戻ってくる

時も斜めに飛んできますから、コントロー

ルがしにくくなってその後も斜めに打ち

続けてしまいがちです。そういう“流れ”

になってしまったのです。

 「“やり取り”を積み重ねていくうちに、

ゲームに“流れの変化”が起きる」

 例えばテニスのダブルス(2 人組)で

は、ふだん後ろの選手同士が斜めに

ボールを打ち合います。これは「やり取

り」です。しかし、突然前の選手が割り込

んできたりしてチーム内の陣形が変わる

ことがあります。「流れ(状況)が変わっ

た」のです。単に「やり取り」が連続してい

るだけでなく、以前の「やり取り」の結果

がその後の「流れ」に影響するわけです。

 流れが変化がないと、遊んでいてもマ

ンネリを感じさせることになります。3D の

格闘 ゲームでは、その時その時で、間合

いが広がったり、逆に接近戦になったり、

リングぎわでの攻防になったりという状

況 が発生します。よくできた対戦ゲーム

で遊ぶと、自分が「追い込まれている!」

と感じることがあります。

 それともう一つ。この「ブロックくずし」

で遊んでいる時、ただ打ち返すことだけ

を考えていましたか。打ち返すだけなら、

なるべくボールをバーの中央で打つよう

に操作すればいいはずです。最初のうち

はそうかもしれませんが、慣れてくるとも

う少しハイレベルなことを考えなかったで

すか。例えば「ボールをうまくコントロー

ルして 1 点集中で攻撃して、ボールをブ

ロックの裏側に回り込ませると楽」といっ

たことです。

 ブロックくずしはボールを落とすとミス

ですが、落とさないことが目的ではありま

せん。それとは別に全てのブロックをくず

すというクリア条件があります。しかもこ

のサンプルではボールを打ち返すほど

ス ピードが上がりミスしやすくなるので、

なるべく 1 度にたくさんのブロックを壊し

(24)

たほうがお得です。しかし欲を出すと、

うっかりボールを打ち返すことじたいをミ

ス してしまうかも。あちらを立てればこち

らが立たず。ゲームをおもしろいものにす

る上で、こうした矛盾(ジレンマ)を用意

することが大切です。

 「プレイヤーは、ゲームの流れを考えて

ジレンマにどう立ち向かうかという“かけ

き”を要求される」

 例題に戻ると、じつは先ほどの改造に

よ って、飛んできたボールを打ち返す「や

り取り」が生まれたと同時に、プレイヤー

に「その時の流れ」に応じた「かけ引き」

が要求されるようになっていたのです。

別 の見方をすれば、プレイに上手下手が

できたということでもあります。

(25)

いろいろつけ足してみる

 さて、せっかくなのでこのサンプルプロ

グラムをいろいろと改造してみましょう。

例えば   Constants.cs ファイルには

ゲーム開始時のボールのスピードや打

ち返した時の加速度などが設定されて

います。こうしたちょっとした定数(決めら

れた数値)をいじってみるだけでも、プロ

グラム入門者にはいい勉強になります。

 またスコアを設定したりタイムアタック

制 にしたりといった改造も、初級者でも

わりと簡単にできるうえ、「もっと上手に

プレイしてみよう」というトライアルの要

素を持たせることができます。

 今回の記事はゲームの内容を考える

のが目的ですので、改造の実例をお見

せします。以下の 3 つのソースを差し替

てみてください。

BlockGame.cs(改造)

using Microsoft.Xna.Framework;

using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Input;

namespace BlockGame {

public class BlockGame : Microsoft.Xna.Framework.Game {

#region フィールド

readonly GraphicsDeviceManager graphics; bool reset = true;

Bar bar;

readonly Ball[] balls = new Ball[Constants.numBalls];

Blocks blocks; Wall wall;

MouseState mouseState; SpriteBatch spriteBatch; #endregion

#region プロパティ

public MouseState MouseState {

get { return mouseState; } }

internal Bar Bar {

get { return bar; } }

internal Ball[] Balls {

get { return balls; } }

internal Blocks Blocks {

get { return blocks; } }

internal Wall Wall {

get { return wall; } }

#endregion

#region コンストラクタ

public BlockGame() {

graphics = new GraphicsDeviceManager(this); Content.RootDirectory = "Content";

}

#endregion #region メソッド

/// <summary>

/// リソース(テクスチャ)のロード /// </summary>

protected override void LoadContent() {

spriteBatch = new SpriteBatch(GraphicsDevice);

//

var barTex = Content.Load<Texture2D>("bar"); var ballTex =

Content.Load<Texture2D>("ball"); var blockTex = Content.Load<Texture2D>("block"); var wallTex = Content.Load<Texture2D>("wall");

//

bar = new Bar(this, barTex);

for (var i = 0; i < Constants.numBalls; i++) {

balls[i] = new Ball(this, ballTex);

(26)

}

blocks = new Blocks(this, blockTex); wall = new Wall(this, wallTex); }

/// <summary> /// 初回リセット /// </summary> void Reset() {

bar.Reset(); balls[0].Reset(); blocks.Reset();

reset = false; }

protected override void Update(GameTime gameTime) {

if (reset) {

Reset(); }

//

mouseState = Mouse.GetState();

//

bar.Update();

for (var i = 0; i < Constants.numBalls; i++) {

var ball = balls[i]; ball.Update();

if (ball.Multiplied) {

// ヒット

for (var j = 0; j < Constants.numBalls; j++)

{

var clonedBall = balls[j]; if (clonedBall.OutOfBounds) {

ball.MirrorCopyTo(clonedBall)

;

break; }

} } }

var allBallsOutOfBounds = true;

for (var i = 0; i < Constants.numBalls; i++) {

if (!balls[i].OutOfBounds) {

allBallsOutOfBounds = false; break;

} }

if (allBallsOutOfBounds) {

// ミス Exit(); }

if (blocks.ExistsCount == 0) {

// クリア

for (var i = 0; i < Constants.numBalls; i++)

{

balls[i].Stop(); }

}

//

base.Update(gameTime); }

protected override void Draw(GameTime gameTime) {

GraphicsDevice.Clear(Color.Black);

//

spriteBatch.Begin(SpriteSortMode.Deferred, null, SamplerState.PointWrap/*.LinearWrap*/, null, null);

// 壁の描画

wall.Draw(spriteBatch);

// バーの描画

bar.Draw(spriteBatch);

// ボールの描画

for (var i = 0; i < Constants.numBalls; i++) {

balls[i].Draw(spriteBatch); }

// ブロックの描画 blocks.Draw(spriteBatch); spriteBatch.End();

//

base.Draw(gameTime); }

#endregion }

}

Ball.cs(改造)

using System;

using Microsoft.Xna.Framework;

using Microsoft.Xna.Framework.Graphics;

参照

関連したドキュメント

In the sequel we came across another space familiarly known as the weighted Hardy space [3] Let 0(n) be a sequence of positive numbers.. The operator C is known as a

LOBBY LOUNGE ロビーラウンジ BEACH SIDE レストラン ビーチサイド ADAN 阿檀.

Toyotsu Rare Earths India Private Limited、Toyota Tsusho Gas E&amp;P Trefoil Pty Ltd、. Toyota Tsusho

In this article we consider the problem of unique continuation for high-order equations of Korteweg-de Vries type which include the kdV hierarchy.. It is proved that if the difference

Löffler, 2003, Evaluating the Quality of Public Governance: Indicators, Models and Methodologies, Administration Review, Vol.. Proposta e materiali di

Continuous Improvement, Contract Review, Quality System Mgmt, Customer Service, Product Design, Process Design, Engineering, Finance,.

), Principles, Definitions and Model Rules of European Private Law: Draft Common Frame of Reference (DCFR), Interim Outline Edition, Munich 200(, Bénédicte Fauvarque-Cosson

Reset condition: RESET_N falling; REG_RST=1; Watchdog Timer Expiry 0 IBUSRCB_INT 0 R/CLR This interrupt bit is set when the current from VOUT to VBUS exceeds I RCB(TH). Reset