ジャンケンゲームの実装
オンラインゲームでは、ゲームのクライアントとサーバーがデータの送受信を行いながらゲームを構成します。 まずははじめに、クライアントとサーバーがそれぞれ手を選択して勝敗を決めるようなジャンケンゲームを作ってみましょう。 以下のような通信が行われます。
通信方向 | 送信内容 |
---|---|
クライアント→サーバー | プレイヤーが選択した手 |
クライアント←サーバー | 相手が選択した手 |
ここでは単純化のために、サーバー側はユーザーが操作せず、ランダムに手を選択するようにします。
サーバープログラム
サーバープログラムは非常にシンプルです。
TCPのコネクションを待ち受け、何らかのデータを受け取ったら、サーバー側が選択した手を表す0から2の整数を返します。 グー(G)、チョキ(C)、パー(P)をそれぞれ0、1、2とすることにします。
サーバープログラムの全体は以下の通りです。
import processing.net.*;
Server server;
void setup() {
server = new Server(this, 5204);
}
void draw() {
}
void clientEvent(Client client) {
client.read();
server.write(int(random(3)));
}
クライアントプログラム
クライアントプログラムは、ゲーム画面の描画やユーザー入力の受け取り、サーバーとの通信など様々な処理を行う必要があります。
画面に3つのボタンを表示し、それぞれグー、チョキ、パーを表すことにします。
まずは、ジャンケンの手の処理を考えます。
ジャンケンの手を表すために以下のような列挙型 Hand
を導入します。
Hand
型は、手を表す0から2の数値(id)や、文字列への変換処理を持たせます。
また、ジャンケンの勝敗のロジックもここに含めます。
勝ち、負け、あいこの結果は 列挙型 Result
で表すことにします。
enum Result {
WIN,
DRAW,
LOOSE
}
enum Hand {
G(0),
C(1),
P(2);
private int id;
private Hand(final int id) {
this.id = id;
}
static Hand valueOf(int id) {
if (id == 0) {
return Hand.G;
} else if (id == 1) {
return Hand.C;
} else {
return Hand.P;
}
}
int getId() {
return this.id;
}
String toString() {
if (this.id == 0) {
return "G";
} else if (this.id == 1) {
return "C";
} else {
return "P";
}
}
Result battle(Hand hand) {
if (this == Hand.G) {
if (hand == Hand.G) {
return Result.DRAW;
} else if (hand == Hand.C) {
return Result.WIN;
} else {
return Result.LOOSE;
}
} else if (this == Hand.C) {
if (hand == Hand.G) {
return Result.LOOSE;
} else if (hand == Hand.C) {
return Result.DRAW;
} else {
return Result.WIN;
}
} else {
if (hand == Hand.G) {
return Result.WIN;
} else if (hand == Hand.C) {
return Result.LOOSE;
} else {
return Result.DRAW;
}
}
}
}
手の選択のボタンを以下のように Button
クラスとして実装します。
ボタンに関する情報の保持や、クリックの判定、描画の処理等を含みます。
class Button {
Hand hand;
int cx, cy, size;
color fillColor;
boolean selected = false;
Button(Hand hand, int cx, int cy, int size, color fillColor) {
this.hand = hand;
this.cx = cx;
this.cy = cy;
this.size = size;
this.fillColor = fillColor;
}
boolean clicked() {
return dist(mouseX, mouseY, cx, cy) <= size / 2;
}
void draw() {
noStroke();
if (this.selected) {
fill(this.fillColor);
} else {
fill(this.fillColor, 100);
}
ellipse(this.cx, this.cy, this.size, this.size);
fill(0);
textAlign(CENTER, CENTER);
text(this.hand.toString(), this.cx, this.cy);
}
}
ゲームの進行状況を列挙型 Status
で表すことにします。
INPUT
はユーザーの入力待ち、WAIT
はサーバーからの結果待ち、RESULT
は結果画面の表示を表します。
INPUT
状態から、ボタンが選択されると WAIT
状態になり、サーバーから相手の手を受け取ると RESULT
状態に遷移します。
RESULT
状態から画面がクリックされるとまた INPUT
状態に戻るようにします。
enum Status {
INPUT,
WAIT,
RESULT
}
ゲームの初期化及び描画処理は以下のようになります。
Client client;
Status currentStatus = Status.INPUT;
Button[] buttons;
Hand enemy = null;
void setup() {
size(400, 400);
client = new Client(this, "127.0.0.1", 5204);
int size = 100;
buttons = new Button[3];
buttons[0] = new Button(Hand.G, size / 2, height / 2, size, color(255, 0, 0));
buttons[1] = new Button(Hand.C, width / 2, height / 2, size, color(0, 255, 0));
buttons[2] = new Button(Hand.P, height - size / 2, height / 2, size, color(0, 0, 255));
}
void draw() {
background(255);
Hand hand = null;
for (Button button : buttons) {
button.draw();
if (button.selected) {
hand = button.hand;
}
}
if (currentStatus == Status.RESULT && hand != null) {
fill(0);
textAlign(CENTER, CENTER);
text("enemy : " + enemy.toString(), width / 2, 60);
Result result = hand.battle(enemy);
if (result == Result.WIN) {
text("win", width / 2, 100);
} else if (result == Result.DRAW) {
text("draw", width / 2, 100);
} else {
text("lose", width / 2, 100);
}
}
}
ユーザーからの入力の処理と通信の処理を以下のようにイベントハンドラーとして実装します。
それぞれの処理に応じて、currentStatus
を変更し、ゲームの状態を更新します。
グー、チョキ、パーのボタンが押されたらその内容をサーバーに送信し、サーバーからの応答を受け取ると結果画面へと遷移させます。
void mouseClicked() {
if (currentStatus == Status.INPUT) {
int selected = -1;
for (Button button : buttons) {
if (button.clicked()) {
selected = button.hand.getId();
button.selected = true;
}
}
if (selected != -1) {
client.write(selected);
currentStatus = Status.WAIT;
}
} else if (currentStatus == Status.RESULT) {
for (Button button : buttons) {
button.selected = false;
}
enemy = null;
currentStatus = Status.INPUT;
}
}
void clientEvent(Client client) {
enemy = Hand.valueOf(client.read());
currentStatus = Status.RESULT;
}
クライアントプログラムの全体は以下の通りです。
import processing.net.*;
enum Result {
WIN,
DRAW,
LOOSE
}
enum Hand {
G(0),
C(1),
P(2);
private int id;
private Hand(final int id) {
this.id = id;
}
static Hand valueOf(int id) {
if (id == 0) {
return Hand.G;
} else if (id == 1) {
return Hand.C;
} else {
return Hand.P;
}
}
int getId() {
return this.id;
}
String toString() {
if (this.id == 0) {
return "G";
} else if (this.id == 1) {
return "C";
} else {
return "P";
}
}
Result battle(Hand hand) {
if (this == Hand.G) {
if (hand == Hand.G) {
return Result.DRAW;
} else if (hand == Hand.C) {
return Result.WIN;
} else {
return Result.LOOSE;
}
} else if (this == Hand.C) {
if (hand == Hand.G) {
return Result.LOOSE;
} else if (hand == Hand.C) {
return Result.DRAW;
} else {
return Result.WIN;
}
} else {
if (hand == Hand.G) {
return Result.WIN;
} else if (hand == Hand.C) {
return Result.LOOSE;
} else {
return Result.DRAW;
}
}
}
}
enum Status {
INPUT,
WAIT,
RESULT
}
class Button {
Hand hand;
int cx, cy, size;
color fillColor;
boolean selected = false;
Button(Hand hand, int cx, int cy, int size, color fillColor) {
this.hand = hand;
this.cx = cx;
this.cy = cy;
this.size = size;
this.fillColor = fillColor;
}
boolean clicked() {
return dist(mouseX, mouseY, cx, cy) <= size / 2;
}
void draw() {
noStroke();
if (this.selected) {
fill(this.fillColor);
} else {
fill(this.fillColor, 100);
}
ellipse(this.cx, this.cy, this.size, this.size);
fill(0);
textAlign(CENTER, CENTER);
text(this.hand.toString(), this.cx, this.cy);
}
}
Client client;
Status currentStatus = Status.INPUT;
Button[] buttons;
Hand enemy = null;
void setup() {
size(400, 400);
client = new Client(this, "127.0.0.1", 5204);
int size = 100;
buttons = new Button[3];
buttons[0] = new Button(Hand.G, size / 2, height / 2, size, color(255, 0, 0));
buttons[1] = new Button(Hand.C, width / 2, height / 2, size, color(0, 255, 0));
buttons[2] = new Button(Hand.P, height - size / 2, height / 2, size, color(0, 0, 255));
}
void draw() {
background(255);
Hand hand = null;
for (Button button : buttons) {
button.draw();
if (button.selected) {
hand = button.hand;
}
}
if (currentStatus == Status.RESULT && hand != null) {
fill(0);
textAlign(CENTER, CENTER);
text("enemy : " + enemy.toString(), width / 2, 60);
Result result = hand.battle(enemy);
if (result == Result.WIN) {
text("win", width / 2, 100);
} else if (result == Result.DRAW) {
text("draw", width / 2, 100);
} else {
text("lose", width / 2, 100);
}
}
}
void mouseClicked() {
if (currentStatus == Status.INPUT) {
int selected = -1;
for (Button button : buttons) {
if (button.clicked()) {
selected = button.hand.getId();
button.selected = true;
}
}
if (selected != -1) {
client.write(selected);
currentStatus = Status.WAIT;
}
} else if (currentStatus == Status.RESULT) {
for (Button button : buttons) {
button.selected = false;
}
enemy = null;
currentStatus = Status.INPUT;
}
}
void clientEvent(Client client) {
enemy = Hand.valueOf(client.read());
currentStatus = Status.RESULT;
}
動作確認
サーバープログラム、クライアントプログラムの順に実行し、動作確認してみましょう。
入力待ち状態
結果表示状態