Processingで創る「環境でルールが変わるライフゲーム」の試行錯誤と作例コード

2026年9月12日土曜日

Processing 作例

t f B! P L
title image

Processing 過去作の「環境でルールが変わるライフゲーム」を改造して、動きを面白くできたので、そのコードを紹介します。

You can read this article in English.


ライフゲーム(Conway's Game of Life)とは?

単純なルールから複雑な形が生成される、数学者コンウェイ考案のセル・オートマトンです。
皆さん、きっと見たことがあるでしょう。詳しくは Wikipedia をご覧ください。

ライフゲーム: Wikipedia
https://ja.wikipedia.org/wiki/ライフゲーム
セル・オートマトン: Wikipedia
https://ja.wikipedia.org/wiki/セル・オートマトン

環境でルールが変わるライフゲーム

隣接するセルの状態によって自身の生死が決まるというのがライフゲームのルールで、このルールは不変です。

でも、生き物って周りの環境に影響を受けますよね。気温の違いだけでも生きやすさに影響しますし、森林、砂漠、高原など、場所によっても違いがあるでしょう。

このように、環境によって生死の条件が変わると面白いのでは?というアイデアが、今回のコードの骨子です。

旧コードの良いとこ、悪いとこ

Processing 過去作の紹介ページ(英語)

セルの生死じゃなくて体力にした

セルが「生きてる・死んでる」の 2値ではなく、体力として 0 から 100 まで幅を持たせ、ルールによってその体力値を増減させました。

隣接するセルの保持

セルのクラスに隣接するセルを保持させ、セル自身が隣接セルを把握しているようにしました。

これにより、ルール適用の度に隣接するセルを求めるための座標計算が不要になりました。これはナイスアイデアなのでは?

セルが自分で自分の状態を変えちゃうの?

体力を増減させるルールをセルのクラスに持たせました。自分で自分の体力値を変えているわけですが、考え方として合ってるような合ってないような。どうもスッキリしません。

見た目がいまいちなのよ ← 致命的

時間と xy座標の 3D ノイズで環境を変化させてみましたが、環境の違いがルールに反映されている様が、アニメーションを見ても直感的に伝わってきません。

しかも、ルールがいまいちで、セルの変化にあまり面白みが感じられません。

新コードでの改善アプローチ

新コードには以下の工夫を加えました。

  • 「隣接するセルを自身のセルが知ってる」は踏襲。描画時に東西南北方向のセルを判別したいがために、贅沢にも HashMap を使って隣接セルを保持させました。
  • 盤面上のセルをコントロールするフィールド・クラスを導入。ルールの処理はこっちに移動。
  • 環境はフィールド全面で時間変化させることで影響を分かりやすくさせた。
  • ルールも大胆に変更。
  • セルの初期配置を規則的にすることで、見て楽しい動きになるようにした。

Processingのコード

リアルタイムのアニメーション描画はしません。frames ディレクトリ中にアニメーション用の画像を書き出す形式です。

Click to view the source code

/**
 * Castle Walls
 * A Custom Twist on Conway’s Game of Life.
 * 
 * @author @deconbatch
 * @license GPL3
 * @version 0.2
 * Processing 4.3.3
 * updated : 2026/09/13 : Refactor corner round code
 * updated : 2026/09/10 : New rule and new looks
 * created : 2019/09/23
 */

import java.util.Map;

void setup() {
  size(720, 720);
  colorMode(HSB, 360.0, 100.0, 100.0, 100.0);
  rectMode(CENTER);
  smooth();
  noLoop();
}

void draw() {
  int   frmMax    = 24 * 20;
  float cellSize  = 18.0;
  int   layoutMod = 13; // ex. common divisor of (width / cellSize - 1)
  float lifeFull  = 90.0;
  
  int canvasW = floor(width / cellSize);
  int canvasH = floor(height / cellSize);
  Field field = new Field(canvasW, canvasH, layoutMod, lifeFull);

  translate(cellSize * 0.5, cellSize * 0.5);
  for (int frmCnt = 0; frmCnt < frmMax; frmCnt++) {

    float envFactor = map(frmCnt, 0, frmMax - 1, 0.1, 0.8);

    blendMode(BLEND);
    background(map(envFactor, 0.0, 1.0, 120.0, 260.0), 80.0, 30.0, 100.0);
    field.calculateLife(envFactor);
    field.drawField(cellSize);

    saveFrame("frames/anim." + String.format("%04d", frmCnt) + ".png");
  }
  exit();
}

/**
 * Field
 * manage field of cells.
 */
private class Field {
  private Cell[][] cells;
  private int fieldW;
  private int fieldH;
  private int layoutMod;
  private float lifeFull;

  Field(int _w, int _h, int _layoutMod, float _lifeFull) {
    fieldW    = _w;
    fieldH    = _h;
    layoutMod = _layoutMod;
    lifeFull  = _lifeFull;
    initCells();
  }

  /**
   * initCells
   * initialize the cells on the field.
   */
  private void initCells() {
    cells = new Cell[fieldW][fieldH];
    // cells constraction
    for (int x = 0; x < fieldW; x++) {
      for (int y = 0; y < fieldH; y++) {
        cells[x][y] = new Cell();
        cells[x][y].setLife(lifeFull * (((x * y) % layoutMod == 0) ? 0.6 : 0.9));
      }
    }

    // set 8 neighbor cells
    Map<String, Cell> nei = new HashMap();
    for (int x = 0; x < fieldW; x++) {
      for (int y = 0; y < fieldH; y++) {
        int mX = getMinus(x, fieldW);
        int mY = getMinus(y, fieldH);
        int pX = getPlus(x, fieldW);
        int pY = getPlus(y, fieldH);

        nei.clear();
        nei.put("w",  cells[mX][y]);
        nei.put("nw", cells[mX][mY]);
        nei.put("n",  cells[x][mY]);
        nei.put("ne", cells[pX][mY]);
        nei.put("e",  cells[pX][y]);
        nei.put("se", cells[pX][pY]);
        nei.put("s",  cells[x][pY]);
        nei.put("sw", cells[mX][pY]);
        cells[x][y].setNeighbors(nei);
      }
    }
  }

  /**
   * calculateLife
   * calculate the life value of the cells with deconbatch's game of life rule.
   * @param  _env : 0.0 - 1.0 : the value that have an impact on calculation
   */
  private void calculateLife(float _env) {
    // calculate neighbors life
    // key of the code: neighbor life > cell life, not neighbor life > fixed value
    int neighborLife[][] = new int[fieldW][fieldH];
    for (int x = 0; x < fieldW; x++) {
      for (int y = 0; y < fieldH; y++) {
        float cellLife = cells[x][y].getLife();
        neighborLife[x][y] = 0;
        for (Cell nei : cells[x][y].getNeighbors().values()) {
          if (nei.getLife() > cellLife) {
            neighborLife[x][y]++;
          }
        }
      }
    }

    // calculate and set cells life
    float lifeBorder = lifeFull * _env;
    for (int x = 0; x < fieldW; x++) {
      for (int y = 0; y < fieldH; y++) {
        float cellLife = cells[x][y].getLife();
        if (cellLife < lifeBorder) {
          // Cell is weak
          cellLife -= lifeBorder * 0.4;
          if (neighborLife[x][y] == 3) {
            cellLife += lifeBorder * 0.5;
          }
        } else {
          // Cell is fine
          cellLife += lifeBorder * 0.4;
          if (neighborLife[x][y] == 2) {
            cellLife += lifeBorder * 0.3;
          } else if (neighborLife[x][y] == 3) {
            cellLife += lifeBorder * 0.2;
          } else {
            cellLife -= lifeBorder * 0.4;
          }
        }
        cellLife -= (1.0 - _env) * lifeFull / 100.0;
        
        cells[x][y].setLife(constrain(cellLife, 0.0, lifeFull));
      }
    }
  }

  /**
   * drawField
   * draw the field based on the cells value.
   * @param  _cellSize : the base size of the cell
   */
  private void drawField(float _cellSize) {
    noStroke();
    for (int x = 0; x < fieldW; x++) {
      for (int y = 0; y < fieldH; y++) {
        int life = round(cells[x][y].getLife() / 10.0) * 10;
        
        float eSiz = _cellSize * life / lifeFull;
        float sSiz = _cellSize * sin(PI * life / lifeFull) * 0.25;
        pushMatrix();
        translate(x * _cellSize, y * _cellSize);
        fill(0.0, 0.0, 90.0, 100.0);
        if (life == lifeFull) {
          // corner round
          float joinBorder = lifeFull * 0.9;
          float lifeE = cells[x][y].getNeighbors().get("e").getLife();
          float lifeW = cells[x][y].getNeighbors().get("w").getLife();
          float lifeS = cells[x][y].getNeighbors().get("s").getLife();
          float lifeN = cells[x][y].getNeighbors().get("n").getLife();
          float tl = (lifeW > joinBorder || lifeN > joinBorder) ? 0.0 : eSiz * 0.5;
          float tr = (lifeN > joinBorder || lifeE > joinBorder) ? 0.0 : eSiz * 0.5;
          float br = (lifeE > joinBorder || lifeS > joinBorder) ? 0.0 : eSiz * 0.5;
          float bl = (lifeS > joinBorder || lifeW > joinBorder) ? 0.0 : eSiz * 0.5;
          rect(0.0, 0.0, eSiz, eSiz, tl, tr, br, bl);
        } else {
          circle(0.0, 0.0, eSiz);
        }
        if (sSiz > _cellSize * 0.125) {
          fill(0.0, 0.0, 20.0, 100.0);
          circle(0.0, 0.0, sSiz);
        }
        popMatrix();

      }
    }
  }

  /**
   * getMinus
   * calculate the coordinates of the point. take overflow into account.
   * @param  _a      : coordinates of the point, x or y
   * @param  _border : canvas width or height
   */
  private int getMinus(int _a, int _border) {
    int ret = _a - 1;
    if (ret < 0) {
      ret = _border - 1;
    }
    return ret;
  }

  /**
   * getPlus
   * calculate the coordinates of the point. take overflow into account.
   * @param  _a      : coordinates of the point, x or y
   * @param  _border : canvas width or height
   */
  private int getPlus(int _a, int _border) {
    int ret = _a + 1;
    if (ret >= _border) {
      ret = 0;
    }
    return ret;
  }

}

/**
 * Cell
 * manage one cell.
 */
private class Cell {

  private float myLife;
  private Map neighbors;

  Cell() {
    myLife = 0.0;
  }

  /**
   * setLife
   * set the life value of this cell.
   * @param  _life : life value
   */
  public void setLife(float _life) {
    myLife = _life;
  }

  /**
   * getLife
   * get the life value of this cell.
   * @return : life value
   */
  public float getLife() {
    return myLife;
  }

  /**
   * setNeighbors
   * set the neighbor cells.
   * @param  _nei : neighbor cells
   */
  public void setNeighbors(Map<String, Cell> _nei) {
    neighbors = new HashMap();
    for (Map.Entry<String, Cell> entry : _nei.entrySet()) {
      neighbors.put(entry.getKey(), entry.getValue());
    }
  }

  /**
   * getNeighbors
   * get the neighbor cells.
   * @return : neighbor cells
   */
  public Map<String, Cell> getNeighbors() {
    return neighbors;
  }

}

/*
Copyright (C) 2026- deconbatch

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program.  If not, see <http://www.gnu.org/licenses/>
*/

  

パラメータの数値を変えたり、初期配置を変えたり、ルールを変えたり、思うがままに改変してお楽しみください。

パラメータの数値(難易度: 低)


float cellSize  = 18.0;
int   layoutMod = 13; // ex. common divisor of (width / cellSize - 1)
float lifeFull  = 90.0;

float envFactor = map(frmCnt, 0, frmMax - 1, 0.1, 0.8);

初期配置(難易度: 中)


// cells constraction
for (int x = 0; x < fieldW; x++) {
  for (int y = 0; y < fieldH; y++) {
    cells[x][y] = new Cell();
    cells[x][y].setLife(lifeFull * (((x * y) % layoutMod == 0) ? 0.6 : 0.9));
  }
}

ルール(難易度: 高)


// calculate and set cells life
float lifeBorder = lifeFull * _env;
for (int x = 0; x < fieldW; x++) {
  for (int y = 0; y < fieldH; y++) {
    float cellLife = cells[x][y].getLife();
    if (cellLife < lifeBorder) {
      // Cell is weak
      cellLife -= lifeBorder * 0.4;
      if (neighborLife[x][y] == 3) {
        cellLife += lifeBorder * 0.5;
      }
    } else {
      // Cell is fine
      cellLife += lifeBorder * 0.4;
      if (neighborLife[x][y] == 2) {
        cellLife += lifeBorder * 0.3;
      } else if (neighborLife[x][y] == 3) {
        cellLife += lifeBorder * 0.2;
      } else {
        cellLife -= lifeBorder * 0.4;
      }
    }
    cellLife -= (1.0 - _env) * lifeFull / 100.0;
    
    cells[x][y].setLife(constrain(cellLife, 0.0, lifeFull));
  }
}

まとめ

コンセプトは「周りの環境に影響を受けるライフゲーム」ですが、厳密な科学シミュレーションではなく、見た目の面白さとアイデアを具現化する楽しさを追求した、単なる遊び("Game")です。

面白い動きが持続するルールを編み出すのには相応の時間がかかりました。「小さなアイデアを思いついては試す」を泥臭く繰り返すのが、私のクリエイティブ・コーディングのスタイルです。

地味で愚直な試行錯誤の連続ですが、これが楽しくて楽しくて、やめられません。

QooQ