Javaでライフゲームを作る
去年、大学の講義でライフゲームを作る課題がありました。
無事単位も出たので、ライフゲームについて記事にまとめようと思います。
tl;dr
作ったもの https://github.com/natmark/LifeGame
ライフゲームの作り方
そもそもライフゲームとは
ライフゲーム (Conway's Game of Life[1]) は1970年にイギリスの数学者ジョン・ホートン・コンウェイ (John Horton Conway) が考案した生命の誕生、進化、淘汰などのプロセスを簡易的なモデルで再現したシミュレーションゲームである。単純なルールでその模様の変化を楽しめるため、パズルの要素を持っている。 - Wikipediaより引用
ライフゲームは碁盤のようなマス目を使用して、シミュレーションを行うものです。
一つの格子をセルと呼び、セル自体は生か死かの状態しか持ちません。
ライフゲームのルール
今回、下記のルールを用いました。
誕生
死んでいるセルに隣接する生きたセルがちょうど3つあれば、次の世代が誕生する。
生存
生きているセルに隣接する生きたセルが2つか3つならば、次の世代でも生存する。
過疎
生きているセルに隣接する生きたセルが1つ以下ならば、過疎により死滅する。
過密
生きているセルに隣接する生きたセルが4つ以上ならば、過密により死滅する。
セルの変化の例
※生きているセルを黒、死んでいるセルを白とする。赤枠のセルに着目したとき、世代nにおける赤枠セルの周囲8つのセルの状態から、世代n+1における赤枠のセルの状態が決定する。
実装
メインクラス
public class LifeGame {
public static void main(String[] args){
GameFrame frame = new GameFrame(Const.FRAME_SIZE.width,Const.FRAME_SIZE.height);
frame.setTitle("LifeGame");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
try {
frame.setCells(Const.CELL_SIZE);
} catch (Exception e) { e.printStackTrace(); }
frame.setVisible(true);
Thread t = new Thread(frame);
t.start();
}
}
ゲームフレーム用クラス
public class GameFrame extends JFrame implements Runnable{
private int width;
private int height;
private boolean running = false;
//盤面に配置されたセルの配列
private ArrayList<LifeCell> cells;
//プリセットパターンを生成する際に使用。cellsを二次元配列にしたもの。
private LifeCell[][] cell_matrix;
/**
* Constructors
*/
public GameFrame(int width, int height){
super();
this.width = width;
this.height = height;
setSize(width,height);
cells = new ArrayList<LifeCell>();
}
public GameFrame(String title){
super(title);
cells = new ArrayList<LifeCell>();
}
/**
* セルサイズの変更
* @param cellSize
* @throws Exception
* @throws cellSizeがFrameSizeに合わない場合
*/
public void setCells(int cellSize) throws Exception {
if((width % cellSize != 0) || (width < cellSize)){
throw new Exception("セルのサイズがフレームの幅に合っていないか、セルサイズがフレームの幅より大きいです。");
}
if(height % cellSize != 0 || (height < cellSize)){
throw new Exception("セルのサイズがフレームの高さに合っていません、セルサイズがフレームの高さより大きいです。");
}
//縦の総セル数
int v_cell_cnt = height / cellSize;
//横の総セル数
int h_cell_cnt = width / cellSize;
//JFrameの高さには、タイトルバーが含まれてしまうため、heightを更新
setVisible(true);
//visible状態でないと、Insetsが取得不可
Insets insets = getInsets();
setVisible(false);
//セル用パネルの生成
JPanel p = new JPanel();
p.setLayout(null);
p.setBounds(0, 0, width + h_cell_cnt, height + v_cell_cnt);
p.setBackground(Color.black);
//ツールパネルの生成
JPanel toolPanel = new JPanel();
toolPanel.setLayout(null);
toolPanel.setBounds(0, p.getBounds().height, p.getBounds().width, 100);
toolPanel.setBackground(Color.white);
//パネルへのボタン追加
addButtonsOnPanel(toolPanel);
//フレームサイズの更新
int insets_height = insets.top + insets.bottom;
setResizable(true);
setSize(width + h_cell_cnt,height + insets_height + v_cell_cnt + toolPanel.getBounds().height);
setResizable(false);
cell_matrix = new LifeCell[v_cell_cnt][h_cell_cnt];
//セルの生成
for(int y = 0; y < v_cell_cnt; y ++){
for(int x = 0; x < h_cell_cnt; x++){
LifeCell cell = new LifeCell(new Rectangle(x * (cellSize + 1),y * (cellSize + 1),cellSize,cellSize));
p.add(cell);
cell_matrix[y][x] = cell;
cells.add(cell);
}
}
//セルの外周をセット。
for(int y = 0; y < v_cell_cnt ;y++){
for(int x = 0; x < h_cell_cnt ;x++){
LifeCell cell = cell_matrix[y][x];
int dx_min = -1;
int dy_min = -1;
int dx_max = 1;
int dy_max = 1;
//yが一番上
if(y == 0){ dy_min = 0; }
//xが一番左
if(x == 0){ dx_min = 0; }
//yが一番下
if(y == v_cell_cnt - 1){ dy_max = 0; }
//xが一番右
if(x == h_cell_cnt - 1){ dx_max = 0; }
for(int dx = dx_min; dx <= dx_max;dx++){
for(int dy = dy_min; dy <= dy_max;dy++){
if(!(dx == 0 && dy == 0)){
cell.addSurroundings(cell_matrix[y+dy][x+dx]);
}
}
}
}
}
Container contentPane = getContentPane();
contentPane.add(p);
contentPane.add(toolPanel);
}
/**
* ツールパネルへのボタン追加
*/
private void addButtonsOnPanel(JPanel panel){
//ボタンのアクションリスナー作成
ActionListener RunBtnAction = new ActionListener(){
public void actionPerformed(ActionEvent event){
//動作状態を切り替え
running = !running;
JButton btn = (JButton)event.getSource();
if(running){
btn.setText("Stop");
}else{
btn.setText("Run");
}
}
};
ActionListener ClearBtnAction = new ActionListener(){
public void actionPerformed(ActionEvent event){
//盤面全てを初期化
LifeCell.forceKillAll(cells);
}
};
ActionListener generateGliderBtnAction = new ActionListener(){
public void actionPerformed(ActionEvent event){
//グライダーのパターン生成
CellPattern.patternGenerator(cell_matrix, CellPattern.Pattern.GLIDER);
}
};
ActionListener generateSpaceShipBtnAction = new ActionListener(){
public void actionPerformed(ActionEvent event){
//宇宙船のパターン生成
CellPattern.patternGenerator(cell_matrix, CellPattern.Pattern.SPACESHIP);
}
};
ActionListener generateGalaxyBtnAction = new ActionListener(){
public void actionPerformed(ActionEvent event){
//銀河のパターン生成
CellPattern.patternGenerator(cell_matrix, CellPattern.Pattern.GALAXY);
}
};
//格納順を保持したいため、LinkedHashMapを使用
LinkedHashMap<String,ActionListener> btnSources = new LinkedHashMap<String,ActionListener>();
btnSources.put("Run", RunBtnAction);
btnSources.put("Clear", ClearBtnAction);
//スペーサー
btnSources.put("", null);
btnSources.put("グライダー", generateGliderBtnAction);
btnSources.put("宇宙船", generateSpaceShipBtnAction);
btnSources.put("銀河", generateGalaxyBtnAction);
//ボタン生成
int i = 0;
for(Map.Entry<String, ActionListener> btnSrc : btnSources.entrySet()){
//空文字の場合、インクリメントしてスキップ
if(btnSrc.getKey().equals("")) { i++; continue; }
JButton button = new JButton(btnSrc.getKey());
button.setBackground(Color.black);
button.setBounds(10 + i * 80,panel.getBounds().y, 80, 50);
button.addActionListener(btnSrc.getValue());
panel.add(button);
i++;
}
}
public void run(){
while(true){
//Startボタンが押されていない場合、処理を中断
while(!running){
try {
Thread.sleep(1000);
} catch (InterruptedException e) { e.printStackTrace(); }
}
try {
//更新速度
Thread.sleep(Const.SLEEP_TIME_MS);
} catch (InterruptedException e) { e.printStackTrace(); }
for(LifeCell cell : cells){
//周囲のセルの状況を確認
cell.checkSurroundings();
}
for(LifeCell cell : cells){
//世代交代(セルの塗り替え)
cell.generationalChange();
}
}
}
}
今回ゲームのフレームサイズやセルのサイズをユーザ定義で変更できるようにしました。
(その結果、少しソースが長くなってしまいましたが...)
Runnable
を実装して、run()が実行されると、無限ループを発生させています。
スリープを挟みながら、毎フレームセルの更新処理をさせています。
//周囲のセルの状況を確認
cell.checkSurroundings();
この部分で、セルの周囲の状態から、次の世代におけるセルの状態を確定し(世代 n)
//世代交代(セルの塗り替え)
cell.generationalChange();
この部分で世代を交代させています(世代 n + 1)
セル用クラス
最後にセル用のクラスですが、セルはJButtonを継承させています。
セルをマウスでクリックしたときに、セルの状態を切り替えることができるようになっています。
class LifeCell extends JButton{
//周囲のセル
private ArrayList<LifeCell> surroundings;
//現在の生存フラグ
private boolean isLiving = false;
//世代更新後の生存フラグ
private boolean willLiving = false;
/**
* Constructors
*/
public LifeCell(){
super("");
surroundings = new ArrayList<LifeCell>();
setLayout();
}
public LifeCell(Rectangle rect) {
super("");
this.setBounds(rect);
surroundings = new ArrayList<LifeCell>();
setLayout();
}
/**
* レイアウトの設定
*/
private void setLayout(){
this.setLayout(null);
this.setBackground(Color.white);
this.setContentAreaFilled(true);
this.setOpaque(true);
this.setBorderPainted(false);
LifeCell button = this;
this.addActionListener(
new ActionListener(){
public void actionPerformed(ActionEvent event){
if(button.isLiving){
button.forceKill();
}else{
button.forceSpawn();
}
}
}
);
}
/**
* 周囲のセルを追加
* @param cell:LifeCell
*/
public void addSurroundings(LifeCell cell){
surroundings.add(cell);
}
/**
* 世代交代を行う
*/
public void generationalChange(){
isLiving = willLiving;
willLiving = false;
if(isLiving){
setBackground(Color.black);
}else{
setBackground(Color.white);
}
}
/**
* 周りのセルを調べて、世代交代の準備
*/
public void checkSurroundings(){
int cnt = 0;
for(LifeCell cell : surroundings){
if(cell.isLiving) cnt++;
}
//誕生
if(!isLiving){
//生きているセルが3つ
if(cnt == Const.BIRTH_CNT){
this.willLiving = true;
}
return;
}
if(isLiving){
//生存
if(Const.LIVING_RANGE.includes(cnt)){
this.willLiving = true;
}
//過疎
if(Const.LIVING_RANGE.lowerBound > cnt){
this.willLiving = false;
}
//過密
if(Const.LIVING_RANGE.upperBound < cnt){
this.willLiving = false;
}
}
}
/**
* ボタンを押して生成する場合
*/
public void forceSpawn(){
isLiving = true;
this.setBackground(Color.black);
}
/**
* ボタンを押してセルを消す場合
*/
public void forceKill(){
isLiving = false;
this.setBackground(Color.white);
}
/**
* 盤面初期化用
*/
public static void forceKillAll(ArrayList<LifeCell> cells){
for(LifeCell cell : cells){
cell.isLiving = false;
cell.willLiving = false;
cell.setBackground(Color.white);
}
}
}
GameFrame.java
のsetCells(int cellSize)
でセルを配置するときに、
addSurroundings(LifeCell cell)
を用いて、周囲のセルのインスタンスを保持しています。
そのため、世代交代で周りのセルを調べる際は
/**
* 周りのセルを調べて、世代交代の準備
*/
public void checkSurroundings(){
int cnt = 0;
for(LifeCell cell : surroundings){
if(cell.isLiving) cnt++;
}
//n世代で死
if(!isLiving){
//n + 1世代で誕生
if(cnt == Const.BIRTH_CNT){
this.willLiving = true;
}
return;
}
//n世代で生
if(isLiving){
//n + 1世代で生存
if(Const.LIVING_RANGE.includes(cnt)){
this.willLiving = true;
}
//n + 1世代で死(過疎)
if(Const.LIVING_RANGE.lowerBound > cnt){
this.willLiving = false;
}
//n + 1世代で死(過密)
if(Const.LIVING_RANGE.upperBound < cnt){
this.willLiving = false;
}
}
}
周囲のセルが格納されているsurroundings
を用いて、生きているセルの数を数えるだけで済むようにしています。
これによって毎フレーム二重配列を走査させる必要がないので、処理を軽くすることができます。
ライフゲームにおけるパターン
[ライフゲーム - Wikipedia]
(https://ja.wikipedia.org/wiki/%E3%83%A9%E3%82%A4%E3%83%95%E3%82%B2%E3%83%BC%E3%83%A0)のパターンの例を見るとわかりやすいですが、
ライフゲームにおけるセルのパターンには、特殊な性質を持ったものが存在します。
講義の課題に、パターンを調べる課題が含まれていたので、作成したライフゲーム内で、下記のパターンを生成できるようにしました。
パターン名 | 種類 | パターン |
---|---|---|
グライダー | 移動物体 | |
宇宙船 | 移動物体 | |
銀河 | 振動子 |
まとめ
今回初めてライフゲームを作ってみました。
単純なルールに従って、画面が変わっていくだけの単純なものですが、眺めていると結構楽しいです。
今回紹介した3パターン以外にも、固定物体(世代が進んでも変化しない)パターンや、繁殖型(世代が進むと、別のパターンを新たに生成する)パターンなどいろいろあるので、新しいパターンを探して見るのも楽しいかもしれません。