Game of Life 그래픽를 Next.js 블로그에 넣기
Game of Life란?
- Game of Life는 영국의 수학자 John Conway가 고안한 법칙이자, cellurar automata입니다.
- 여기서 cellurar automata는 격자형태에서, 세포(cell)의 초기 상태로부터 시간이 지남에 따라 세대간의 규칙이 생기는 모형을 말하는데요.
- 들어가기 전에 고안자인 John Conway의 automata에 대한 생각과 고찰이 담긴 영상을 소개해드립니다! 철학적이면서도 이분법적(공학적 의미)이라서 매우 흥미롭기 때문이죠..
- https://www.youtube.com/watch?v=R9Plq-D1gEk
- 또한 이를 바탕으로 한 다양한 게임과 프로젝트가 많습니다. 참고한 흥미로운 Game of Life 관련 사이트도 같이 소개해드립니다.
- Game of Life 게임으로 만든 사이트
- https://playgameoflife.com/
- Game of Life의 규칙으로 sound 만들어보기
- https://drakon.tistory.com/142
- Game of Life 게임으로 만든 사이트
저는 이 흥미로운 주제로 저의 홈페이지를 꾸미기로 결정했고, Game of Life의 코드를 짜기 위해 더 자세히 알아보기 시작했습니다.
Game of Life의 규칙
아래는 Game of Life의 규칙입니다. 아래 규칙을 통해 코드를 짜면서 Game of Life의 세계관을 구성할 수 있어요.
초기상태
- Game of Life는 NxN의 격자 형태에서 시작한다.
- 각 세포는 인접해있는 ‘이웃 세포’가 있고, 각 세포는 죽었거나(0) 살아있는(1) 상태이다.
- 아래 규칙에 따라 주기적으로 상태가 변할 수도 있다.
규칙
- 현재 세포가 살아있고, 이웃 세포가 2~3개 살아있다면, 다음에도 현재 세포는 산다.
- 현재 세포가 살아있고, 이웃 세포가 0~1개 살아있다면, 현재 세포는 죽는다. [overcrowding]
- 현재 세포가 살아있고, 이웃 세포가 4개 이상 살아있다면, 현재 세포는 죽는다. [loneliness]
- 현재 세포가 죽어있고, 이웃 세포가 3개라면, 현재 세포는 살아난다.
위 초기 상태와 규칙 아래에 생기는 모형을 보면 꽤나 흥미로운데요. 시간이 지나도 변함이 없는 고정 패턴, 일정한 행동을 주기적으로 반복하는 패턴, 한 쪽으로 전진하는 패턴 등 다양한 모습을 발견할 수 있습니다.
그렇다면 무수히 많은 시간동안 매우 큰 NxN에서 Game of Life를 진행시킨다면 어떠한 패턴이 또 창조될까요? 이러한 궁금증은 아래 영상에서 해소할 수 있습니다. (Game of Life에 진심인 사람이 제작한 영상인 느낌이죠..)
- https://www.youtube.com/watch?v=viA-HIW-2C4
그럼 이제 본격적으로 코드를 짜보겠습니다.
Game of Life 코드
p5.js 코드
- 격자를 그리고 칠하는 면에서 p5.js 언어를 사용하면 편하기 때문에 p5.js 기반으로 코드를 구성했습니다.
- 크게 1) 초기 설정 단계인 setup, 2) 프레임마다 화면을 그리는 draw, 3) 사용자의 mouse input 을 받는 mousePressed 함수를 바탕으로 짜여진 코드입니다.
setup
초기 설정 단계
- 행, 열 크기를 설정하고 2차원 빈 배열을 만들어주는 함수입니다.
- 각 위치(currentCells[][])마다 0 또는 1을 임의로 배치합니다.
let cellSize = 20;
let columnCount;
let rowCount;
let currentCells = [];
let nextCells = [];
function setup() {
frameRate(10);
createCanvas(720, 400);
// 행, 열의 크기를 구한다.
columnCount = floor(width / cellSize);
rowCount = floor(height / cellSize);
// 열마다 빈 배열을 값으로 넣어준다.
for (let column = 0; column < columnCount; column++) {
currentCells[column] = [];
nextCells[column] = [];
// 초기 상태 설정 추가한다.
for (let row = 0; row < rowCount; row++) {
currentCells[column][row] = p5.random([0, 1]);
nextCells[column][row] = 0;
}
}
noLoop();
}
draw
프레임마다 화면을 그리는 단계
- 세포를 모두 돌면서 게임 규칙에 따라 결정된 세포의 다음 상태를 정해줍니다.
- 세포의 운명 색은 generate 함수를 통해 결정합니다.
- 죽으면(0) 하얀색, 살아있으면(1) 검은색으로 칠해주게 됩니다.
function draw() {
generate();
for (let column = 0; column < columnCount; column++) {
for (let row = 0; row < rowCount; row++) {
// 현재 세포의 상태 cell
let cell = currentCells[column][row];
// 현재 세포의 상태가 1이면, black으로 칠하고 | 0이면, white로 칠한다.
fill((1 - cell) * 255);
stroke(0);
rect(column * cellSize, row * cellSize, cellSize, cellSize);
}
}
}
generate
게임 규칙에 따른 세포의 다음 운명을 정하는 단계
- 모든 세포를 돌면서, 다음 주기의 세포의 상태를 결정합니다.
- 규칙은 아래와 같이 1) 해당 세포의 상태와, 2) 주변 이웃의 살아있는 상태 개수에 따라 정해지는데요.
- 현재 세포가 살아있고, 이웃 세포가 0~1개 혹은 4개 이상 살아있다면, 현재 세포는 죽습니다. [overcrowding & loneliness]
if (neighbours < 2 || neighbours > 3) { nextCells[column][row] = 0; }
- 현재 세포가 죽어있고, 이웃 세포가 3개라면, 현재 세포는 살아납니다.
else if (neighbours === 3) { nextCells[column][row] = 1; }
- 현재 세포가 살아있고, 이웃 세포가 2~3개 살아있다면, 다음에도 현재 세포는 살게 됩니다.
else nextCells[column][row] = currentCells[column][row];
- 현재 세포가 살아있고, 이웃 세포가 0~1개 혹은 4개 이상 살아있다면, 현재 세포는 죽습니다. [overcrowding & loneliness]
// 다음 세대의 세포의 상태를 결정하는 함수
function generate() {
// 2차원 배열을 돌면서 상태를 정한다.
for (let column = 0; column < columnCount; column++) {
for (let row = 0; row < rowCount; row++) {
// 왼쪽 이웃
let left = (column - 1 + columnCount) % columnCount;
// 오른쪽 이웃
let right = (column + 1) % columnCount;
// 위쪽 이웃
let above = (row - 1 + rowCount) % rowCount;
// 아래쪽 이웃
let below = (row + 1) % rowCount;
// 주변 이웃이 얼마나 살아있는지 센다.
let neighbours =
currentCells[left][above] +
currentCells[column][above] +
currentCells[right][above] +
currentCells[left][row] +
currentCells[right][row] +
currentCells[left][below] +
currentCells[column][below] +
currentCells[right][below];
if (neighbours < 2 || neighbours > 3) {
nextCells[column][row] = 0;
} else if (neighbours === 3) {
nextCells[column][row] = 1;
} else nextCells[column][row] = currentCells[column][row];
}
}
// 현재 세포의 상태 배열을 구한 nextCells로 바꾼다.
let temp = currentCells;
currentCells = nextCells;
nextCells = temp;
}
mousePressed
게임을 초기화하는 단계
- 사용자가 마우스를 클릭하면 다시 세포의 상태를 재배열해줌으로써 게임을 초기화합니다.
// 마우스를 클릭하면 랜덤하게 재배치한다.
function mousePressed() {
randomizeBoard();
loop();
}
function randomizeBoard() {
for (let column = 0; column < columnCount; column++) {
for (let row = 0; row < rowCount; row++) {
// 0과 1중 랜덤하게 모든 격자를 재배열한다.
currentCells[column][row] = random([0, 1]);
}
}
}
* c++ 코드
- c++ 코드의 경우, 아래 링크를 참고하면 c언어가 익숙한 독자는 이해하기 편할 거라 생각합니다.
- https://blog.naver.com/lsm_origin/120211281982
Next.js 블로그에 Game of Life 인터랙션 넣기
- 저는 블로그를 next.js & typescript 환경에서 운영 중인데요. 따라서 p5.js 코드를 제 블로그에 적용하기 위해서 아래와 같은 과정을 거쳐야 했습니다.
❎ 외부 라이브러리를 사용
- 우선
react-p5
React-Wrapper
와 같은 외부 라이브러리를 사용하려 했습니다. 하지만 빌드 과정에서 p5(p5.js를 위한 기본 라이브러리)의 type과 react-p5의 type이 충돌하면서 많은 애를 먹고, 직접 구현을 마음 먹게 되었습니다. 혹시 몰라 react-p5를 활용한 예제를 남겨놓겠습니다.- https://dev.to/christiankastner/integrating-p5-js-with-react-i0d
✅ P5js 캔버스를 직접 React에 띄우기
-
다음은 직접 Instance를 사용해 P5.js를 React의 가상 DOM과 함께 사용하는 방법입니다. 아래와 같이 많은 레퍼런스가 있고, 또 직관적이라고 판단되어 아래와 같이 진행했습니다.
- https://whoisryosuke.com/blog/2024/adding-p5js-to-a-nextjs-blog
- https://github.com/alecrem/nextjs-p5js-tutorial/tree/main
-
큰 흐름만 설명하고, 자세한 코드는 제 깃허브를 남겨놓겠습니다.
import type p5Types from "p5";
p5
패키지를 다운 받아 p5.js의 기본적인 함수를 사용할 준비를 합니다.- https://www.npmjs.com/package/p5
new p5((p: p5Types) => { p.setup = () => { // 캔버스 생성 및 부모 요소 설정 const canvas = p.createCanvas(500, 500) canvas.parent(canvasRef.current) }
- 전역 스코프가 아닌, p5 인스턴스를 만들어 관련 함수를 내부에 캡슐화해줍니다.
- 이를 통해 독립적으로 React의 DOM과 더 잘 동작하게 합니다.
const canvas = p.createCanvas(500, 500); canvas.parent(canvasRef.current);
- ref를 통해 p5 canvas와 마운트되게 구현합니다.
-
아래는 간략한 react & p5.js의 예시 코드입니다.
// Next.js/React 환경에서의 P5.js 구현
import { useEffect, useRef } from "react";
import type p5Types from "p5";
export default function P5Container() {
const canvasRef = useRef < HTMLDivElement > null;
useEffect(() => {
if (typeof window === "undefined") return;
async function initP5() {
const p5 = (await import("p5")).default;
new p5((p: p5Types) => {
p.setup = () => {
// 캔버스 생성 및 부모 요소 설정
const canvas = p.createCanvas(500, 500);
canvas.parent(canvasRef.current);
};
p.draw = () => {
// 애니메이션 로직
};
});
}
initP5();
}, []);
return <div ref={canvasRef}></div>;
}
자세한 건 제 깃허브 코드를 봐주시면 감사하겠습니다!
https://github.com/kwonET/kwonbomixyz/tree/main/components/P5js
재밌는 규칙을 직접 javascript로 표현해보고, 블로그에 상징적인 의미로 남길 수 있는 좋은 포스팅이었습니다.