LoginSignup
38
38

More than 5 years have passed since last update.

Javaでライフゲームを作る

Last updated at Posted at 2017-04-07

Javaでライフゲームを作る

去年、大学の講義でライフゲームを作る課題がありました。
無事単位も出たので、ライフゲームについて記事にまとめようと思います。

tl;dr

作ったもの https://github.com/natmark/LifeGame

demo.gif

ライフゲームの作り方

そもそもライフゲームとは

ライフゲーム (Conway's Game of Life[1]) は1970年にイギリスの数学者ジョン・ホートン・コンウェイ (John Horton Conway) が考案した生命の誕生、進化、淘汰などのプロセスを簡易的なモデルで再現したシミュレーションゲームである。単純なルールでその模様の変化を楽しめるため、パズルの要素を持っている。 - Wikipediaより引用

ライフゲームは碁盤のようなマス目を使用して、シミュレーションを行うものです。
一つの格子をセルと呼び、セル自体は生か死かの状態しか持ちません。

ライフゲームのルール

今回、下記のルールを用いました。
誕生
  死んでいるセルに隣接する生きたセルがちょうど3つあれば、次の世代が誕生する。
生存
  生きているセルに隣接する生きたセルが2つか3つならば、次の世代でも生存する。
過疎
  生きているセルに隣接する生きたセルが1つ以下ならば、過疎により死滅する。
過密
  生きているセルに隣接する生きたセルが4つ以上ならば、過密により死滅する。

セルの変化の例

スクリーンショット 2017-04-07 14.29.16.png
※生きているセルを黒、死んでいるセルを白とする。

赤枠のセルに着目したとき、世代nにおける赤枠セルの周囲8つのセルの状態から、世代n+1における赤枠のセルの状態が決定する。

実装

メインクラス

LifeGame.java
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();
    }
}

ゲームフレーム用クラス

GameFrame.java
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を継承させています。
セルをマウスでクリックしたときに、セルの状態を切り替えることができるようになっています。

LifeCell.java
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.javasetCells(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のパターンの例を見るとわかりやすいですが、
ライフゲームにおけるセルのパターンには、特殊な性質を持ったものが存在します。

講義の課題に、パターンを調べる課題が含まれていたので、作成したライフゲーム内で、下記のパターンを生成できるようにしました。

パターン名 種類 パターン
グライダー 移動物体
宇宙船 移動物体
銀河 振動子

まとめ

今回初めてライフゲームを作ってみました。
単純なルールに従って、画面が変わっていくだけの単純なものですが、眺めていると結構楽しいです。
今回紹介した3パターン以外にも、固定物体(世代が進んでも変化しない)パターンや、繁殖型(世代が進むと、別のパターンを新たに生成する)パターンなどいろいろあるので、新しいパターンを探して見るのも楽しいかもしれません。

38
38
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
38
38