[java 인공지능] [java] 라이프 게임 (life game)

 

 

 

 

 

스프링노트를 운영하던 시절 썼던 글인데 티스토리로 옮기면서 다시 읽어 보니 감회가 새롭네요

 

요즘엔 AWT/Swing 이 거의 쓰이지 않지만 이때 공부했던 GUI 프로그래밍이 이후에 플렉스와 아이폰을 

 

공부하면서 GUI에 적응하는데 밑거름이 되었던 것 같네요! 

 

 

 

 

 

1. 라이프 게임 개발을 시작하다

 

 

2008년 6월 25일 수요일 새벽 잠들기 전....

 

1년간 프로그래밍에 전혀 손 대지 않다가... 다시 프로그래밍을 하려고 하니 영~ 힘들다  

 

1년이란 시간이 매우 길긴 기나부다...그 많은 것을 잊어버렸다. 자바 Spring 프레임워크를 파보려고 하는데

 

읽어도 무슨말인지 모르겠고, 이클립스 등 개발환경 셋팅하는데만 하루가 걸렸다. ㅡㅡ;;

 

잊어버린 자바 관련 지식을 상기시키고자, 첫 번째 프로젝트에 들어갔다. 프로그램은 "콘웨이"에 '라이프 게임!'

 

게임 알고리즘이 간단해서 연습용 프로젝트에 딱인 것 같다.

 

그럼 라이프 게임이 무엇인지부터 알아보자!

 

 

 

2. 라이프 게임이란?

 

 

라이프 게임(Game of Life) 또는 생명 게임은 영국의 수학자 존 호튼 콘웨이가 고안해낸 세포 자동자의 

일종으로, 가장 널리 알려진 세포 자동자 가운데 하나이다. 미국의 과학잡지 사이언티픽 어메리칸 1970년 

10월호 중 마틴 가드너의 칼럼 〈Mathematical Games(수학 게임)란을 통해 처음으로 대중들에게 소개되어 

단순한 규칙 몇가지로 복잡한 패턴을 만들어낼 수 있다는 점 때문에 많은 관심과 반응을 불러일으켰다.

 

설명

이 ‘게임’은 사실 게임을 하는 사람이 자신의 의지로 게임의 진행을 결정하는 일반적인 게임과는 다르다. 

라이프 게임의 진행은 처음 입력된 초기값만으로 완전히 결정된다.

 

라이프 게임은 무한히 많은 사각형(혹은 ‘세포’)로 이루어진 격자 위에서 돌아간다. 각각의 세포 주위에는 

인접해 있는 여덟 개의 ‘이웃 세포’가 있으며, 또 각 세포는 ‘죽어’ 있거나 ‘살아’ 있는 두가지 상태중 

한가지 상태를 갖는다.  격자를 이루는 세포의 상태는 연속적이 아니라 이산적으로 변화한다. 즉, 현재 세대의 

세포들 전체의 상태가 다음 세대의 세포 전체의 상태를 결정한다.

...

 

패턴의 예

라이프 게임에는 전혀 변화가 없는 고정된 패턴(정물 靜物, still life), 일정한 행동을 주기적으로 반복하는 패턴

(진동자, oscillator), 한쪽 방향으로 계속 전진하는 패턴(우주선, spaceship) 등 여러 패턴이 존재한다.

 

 

‘block’과 ‘boat’는 정물이고, ‘blinker’와 ‘toad’는 진동자, 그리고 ‘글라이더(glider)’와 ‘경량급 우주선(lightweight spaceship — LWSS)’은 우주선에 속한다...

 

 

출처 Wikipedia

 

      

 

 

 

3. 게임 규칙

 

게임규칙을 요약하면 아래와 같다 

 

* 셀의 상,하,좌,우,각 대각선 8개의 인접한 셀을 이웃으로 한다.

* 셀은 세대를 거듭하며 살거나 죽는다.

 

1. 정확히 3개의 이웃이 살아있다면,  (죽어있는) 셀이 살아난다.

2. 2개의 이웃이 살아있다면 살아있는 셀은 다음세대에도 살아남든다

3. 1개 이하 또는 4개 이상의 이웃이 살아있다면, 살아있는 셀은 외로워서 또는 질식해서 죽는다.

 

 

 

4. 구현하기

 

프로그래밍을 하기 위하여 구현한 절차는 아래와 같다. 

 

먼저 모든 셀들을 순회하면서 살아있는 이웃셀을 카운트하고, 살아있는 이웃의 개수를 저장한다

 

이를 바탕으로 위 세가지 규칙을 적용하여 다음세대 살아있는 셀들을 결정한다.

 

간략하게 만들어 화면은 아래와 같다. 

 

 

 

 

 

 

 

 

4.1 게임판의 표현

 

게임판은 JPanel을 상속하는 Cell을 가로, 세로 size 개수 만큼의 요소로 갖는 이차원 배열로 표현하였다. 

LifeGame 클래스는 게임의 전체적인 흐름을 관리하는 메소드들을 갖는다. 

public class LifeGame extends JPanel  {
       ...
	private void init() {	
		setLayout(new GridLayout(size,size));
		cells = new Cell[size][size];
		
		for (int i=0; i < size; i++)
			for (int j=0; j < size; j++) {
				cells[i][j] = new Cell();
				cells[i][j].setBorder(BorderFactory.createLineBorder(Color.BLACK));
				add(cells[i][j]);
			}
		rule = new GameRule(cells);
	}
        ...
	public void transition() {
		rule.countAliveNeibor();
		rule.applyRule();
	}
        ...
}

 

 

 

 

4.2 셀의 표현과 셀의 상태 

 

Cell은 live 상태(true이면 살아있고, false 죽어있는 상태)와 살아있는 이웃의 개수에 대한 변수를 갖는다. 

live 상태에서 따라서 셀이 그려지거나 혹은 그려지지 않거나 한다.

public class Cell extends JPanel {
	private boolean live = false;
	private int neighborCount = 0;
	private Image img = null;
	private int w, h;
        ...
       @Override
	protected void paintComponent(Graphics g) {
	        ...
		if (live) {
			gg.drawImage(img, 0, 0, this);
		}
		else {
			gg.setColor(getBackground());
			gg.fillRect(0, 0, getWidth(), getHeight());
		}
		g.drawImage(image, 0, 0, this);
	}
}

 

 

 

 

4.3 게임규칙의 표현

 

턴마다 LifeGame 클래스의 transition()메소드가 호출되면, GameRule 클래스의 countAliveNeibor() 메소드가 

먼저 호출되고, 이웃하는 셀들에 개수를 모두 카운팅 한뒤, 살아있는 세대를 결정하기 위해 applyRule()메소드가 호출된다. 

public class GameRule {
    ...
     public void countAliveNeibor() {
         ...
     }
     public void applyRule() {
		int neighborCount;

		for (int r = 0; r < rows; r++) {
			for (int c = 0; c < cols; c++) {
				neighborCount = cells[r][c].getNeighborCount();

				if (cells[r][c].getLife()) {
					if (neighborCount <= 1 || neighborCount >= 4)
						cells[r][c].setLife(false);
				}
	
				else  {
					if (neighborCount == 3) 
						cells[r][c].setLife(true);
				}
			}
		} //for 
	}
}

 

 

 

 

 

5. 버려진 프로그램은 싫다! 리팩토링~

 

2009년 08월 04일

 

예전에 만든 라이프게임 UI가 허접해서 라이프게임 알고리즘은 그대로 드고, 드로잉부분만 수정해서 다시 만들어봤다. 

 

기존에는 메인패널 하나에 좌표를 가지고 Graphics 객체의 드로잉 메소드로 그렸지만, 이번에는 

 

셀하나가 JPanel을 상속하게 했고, 셀이 살아있으면, 이미지를 로딩해서 그리게 했다. 

 

빠른 드로잉을 위해 역시 더블 버퍼링을 사용한다

 

Timer로는 javax.util.Timer를 사용했다.

 

TimerTask 쓰레드를 정의해서, 2초마다 상태전이(transition) 후 드로잉하도록 했다.

 

pauser 기능은 wait() /  notify() 를 사용해서, TimerTask 쓰레드를 wait()로 대기상태로 만들고

 

notifyAll()로 다시 깨우도록 처리했다.

 

 

 

 

 

6. 개발하게 하면서 이런걸 알게되었다~

 

 1. AWT의 컴포넌트(Canvas)와 Swing의 컴포넌트(JMenu)를 함께 사용하면, Canvas에 가려 메뉴가 안보인다

 

    중량컨테이너(High Weight Container)인 AWT 와 경량컨테이너(Light Weight Container)인 Swing을 함께 

 

    사용해서 그렇다고 한다. 따라서 중량 컨테이너와 중량 컨테이너를 함께 사용하지 말아야 한다.  

 

    Swing에서는 Canvas대신 Panel을 사용한다

 

 2. Javax.swing.Timer 객체를 통해 타이머를 구현할 수 있다.

    Javax.swing.Timer timer = new Timer(int timeoutMil, ActionListener listener);

 

 

 3 . 더블버퍼링

      Image buffer = Component.createImage(int width, int height);    컴포넌트에 버퍼를 얻고,

      Graphics g = buffer.getGraphics(); 그래픽스 객체 g에 드로잉한다

      Componet.getGraphics().drawImage(buffer, posintX, positionY, Componet);   버퍼를 컴포넌트에 덮는다

 

 

 4. 이차원 배열의 사용

  Cell을 표현하는 이차원 배열의 생성은 다음과 같다

       Cell[][] cell = new Cell[size][size];

       for (int i=0; i < size; i++)

           for (int j=0; j < size; j++)

               cell[i][j] = new Cell();

 

주의할 점은 Cell 타입의 이차원배열을 생성하고, 그 이차원 배열을 순회하며 실제로 각 배열의 원소에 Cell 객체를 

생성하고 할당해야 한다는 것!

 

 

 

7. 만들면서 삽질하게 만든 요인들

 

첫째로, 

타이머 이벤트 발생할 때마다,  JPanel 클래스를 상속한 MyPanel에서 public void paint(Graphics g) 

메소드를 오버라이드 했는데, 해당좌표에 셀이 도무지 제대로 그려지지 않았다 --^; 

갖은 실수 끝에 실수로 알고리즘 메소드가 주석처리 되었었다는 사실을 깨닫고 수정했다. otz...

 

둘째로, 

이전 라이프셀이 지워지지 않는채, 계속 덮입혀져 그려진다. 이것은 더블버퍼링을 이용해서 해결! 

생각보다 자바의 드로잉은 속도가 느려서 더블버퍼링을 사용하지 않으면, 화면갱신이 드로잉을 따라가지 

못하는 것 같다. 

 

셋째로, 

AWT에서의 Canvas 대신, Swing에서는 JPanel을 사용하는데, 드로잉을 하는데 있어서 잘 생각해야 한다.

프로그램이 시작되자 마자 JPanel에 무엇이 그려져야 한다면, 더더욱 그렇다. 게임이 시작하자마자 셀라인을 

보이게 하려고 했다. JFrame의 생성자에 그리는 코드를 넣을게 아니라, public void paint(Graphics g) 메소드에서

처리하면 해결할 수 있다. 이때 그리는데 필요한 데이터값이 정확히 대입이 됐는지 잘 살펴야, 삽질을 피할수 있다.

 

어쨌든 이래저래 해서, 몸풀기 자바 프로그램을 완성했다. 요거 하면서 그나마 잊어버렸던 자바 지식들을 조금씩 

기억하게 되어 도움이 되었다.

 

 

 

 

 

 

 

8. 짜잔~ 최종적으로 만들어진 프로그램

 

 

 

 

 

 

소스 다운로드 

 JDK_000_LifeGame.zip

 

 

 

 

다음은  LifeGame, GameRule, Cell 클래스 소스이다. 

 

package lifegame;
import java.awt.Color;
import java.awt.Graphics;
import java.awt.GridLayout;

import javax.swing.BorderFactory;
import javax.swing.JPanel;

@SuppressWarnings("serial")
public class LifeGame extends JPanel  {
	private Cell[][] cells;
	private int size; 
	private GameRule rule; 

	public LifeGame(int size) {
		this.size = size;
		init();
	}

	// 초기화 
	private void init() {	
		setLayout(new GridLayout(size,size));
		cells = new Cell[size][size];
		
		for (int i=0; i < size; i++)
			for (int j=0; j < size; j++) {
				cells[i][j] = new Cell();
				cells[i][j].setBorder(BorderFactory.createLineBorder(Color.BLACK));
				add(cells[i][j]);
			}
		rule = new GameRule(cells);
	}

	public void cellAlive(int r, int c) {
		cells[r][c].setLife(true);
	}
	
	public void cellDead(int r, int c) {
		cells[r][c].setLife(false);
	}

	public boolean getLifeCell(int r, int c) {
		return cells[r][c].getLife();
	}

	public void clearGame() {
		for (Cell[] cs : cells) 
			for (Cell c : cs)
				c.setLife(false);
	}

	public void transition() {
		rule.countAliveNeibor();
		rule.applyRule();
	}
	
	@Override
	protected void paintComponent(Graphics g) {
		for (Cell[] cs : cells)
			for (Cell c : cs)
				c.repaint();
	}
}

 

package lifegame;

public class GameRule {
	private Cell[][] cells;
	private static final int LEFT = -1, UP = -1;
	private static final int RIGHT = 1, DOWN = 1;
	private int rows, cols;
	
	public GameRule(Cell[][] cells) {
		this.cells = cells;
		rows = cells.length;
		cols = cells[0].length;
	}

	// 각 셀의 살아있는 이웃을 센다 
	public void countAliveNeibor() {
		int nNeighbor;

		for (int r = 0; r < rows; r++) {
			for (int c = 0; c < cols; c++) {
				
				nNeighbor = 0;
				if (r + UP >= 0)
					if(cells[r + UP][c].getLife())
						nNeighbor++;

				if (r + UP >= 0 && c + RIGHT < cols)
					if (cells[r + UP][c + RIGHT].getLife())
						nNeighbor++;
							
				if (c + RIGHT < cols)
					if (cells[r][c + RIGHT].getLife())
						nNeighbor++;

				if (r + DOWN < rows && c + RIGHT < cols)
					if (cells[r + DOWN][c + RIGHT].getLife())
						nNeighbor++; 
							
				if (r + DOWN < rows)
					if (cells[r + DOWN][c].getLife())
						nNeighbor++;

				if (r + DOWN < rows && c + LEFT >= 0)
					if (cells[r + DOWN][c + LEFT].getLife())
						nNeighbor++;				

				if (c + LEFT >= 0)
					if (cells[r][c + LEFT].getLife())
						nNeighbor++;

				if (r + UP >= 0 && c + LEFT >= 0)
					if (cells[r + UP][c + LEFT].getLife())
						nNeighbor++;

				cells[r][c].setNeighborCount(nNeighbor);
			}
		} // for

	}
	// 살아있는 셀은 이웃이 1명 이하, 4명 이상이면 죽는다
	// 죽어있는 셀은 이웃이  3명이면 살아난다
	// 이웃이 2,3명인 살아있는 셀은 계속 산다 
	public void applyRule() {
		int neighborCount;

		for (int r = 0; r < rows; r++) {
			for (int c = 0; c < cols; c++) {
				neighborCount = cells[r][c].getNeighborCount();

				if (cells[r][c].getLife()) {
					if (neighborCount <= 1 || neighborCount >= 4)
						cells[r][c].setLife(false);
				}
	
				else  {
					if (neighborCount == 3) 
						cells[r][c].setLife(true);
				}
			}
		} //for 
	}

}

 

package lifegame;

import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Image;
import java.io.BufferedInputStream;

import javax.swing.JPanel;
import javax.imageio.ImageIO;


@SuppressWarnings("serial")
public class Cell extends JPanel {
	private boolean live = false;
	private int neighborCount = 0;
	private Image img = null;
	private int w, h;
	
	
	public Cell() {
		try {
			img = ImageIO.read(new BufferedInputStream(Res.class.getResourceAsStream("../life.png")));
			w = img.getWidth(this);
			h = img.getHeight(this);
			setPreferredSize(new Dimension(w,h));
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
	
	public void setLife(boolean s) { 
		live = s;
	}
	
	public boolean getLife() {
		return live;
	}
	
	public void setNeighborCount(int n) {
		neighborCount = n;
	}
	
	public int getNeighborCount() {
		return neighborCount;
	}

	@Override
	protected void paintComponent(Graphics g) {
		// 더블 버퍼링 
		Image image = createImage(getWidth(), getHeight());
		Graphics gg = image.getGraphics();
		
		if (live) {
			gg.drawImage(img, 0, 0, this);
		}
		else {
			gg.setColor(getBackground());
			gg.fillRect(0, 0, getWidth(), getHeight());
		}
		g.drawImage(image, 0, 0, this);
	}
}

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

본 웹사이트는 광고를 포함하고 있습니다.
광고 클릭에서 발생하는 수익금은 모두 웹사이트 서버의 유지 및 관리, 그리고 기술 콘텐츠 향상을 위해 쓰여집니다.
번호 제목 글쓴이 날짜 조회 수
430 [Java] Java Console Input and Output Examples 졸리운_곰 2021.11.07 3
» [java 인공지능] [java] 라이프 게임 (life game) file 졸리운_곰 2021.10.19 3
428 [Java][MyBatisc] myBatis에서 null과 nullString을 체크할 때 졸리운_곰 2021.09.13 2
427 [MongoDB/Java] MongoDB에 JSON 형식 데이터 삽입하기 file 졸리운_곰 2021.07.13 5
426 [Java, MongoDB] mongodb java driver 3.0: how to store JSON document 졸리운_곰 2021.07.13 3
425 [Java] \ 문자 빠구기 : replaceAll 사용시 특수문자 졸리운_곰 2021.07.13 3
424 [java, spring] Spring에서 request와 response를 JSON format 으로 한번에 로깅하기 file 졸리운_곰 2021.06.18 9
423 [Spring Boot] 2) Springboot OncePerRequestFilter 와 GenericFilterBean의 차이 file 졸리운_곰 2021.06.18 3
422 [Spring boot] [Spring boot] Spring Boot servlet filter 사용하기 졸리운_곰 2021.06.18 5
421 [SpringBoot] Filter(필터) OncePerRequestFilter간단히 사용하기 file 졸리운_곰 2021.06.18 5
420 [Spring boot] Spring boot 에서 Filter 사용하기 졸리운_곰 2021.06.18 3
419 [Spring Boot] 스프링 부트에 필터를 '조심해서' 사용하는 두 가지 방법 졸리운_곰 2021.06.18 4
418 [Java 자료구조] [Java] 문자열의 첫 글자 제거 졸리운_곰 2021.05.24 19
417 [Java 자료구조] [java] 특정 문자열 사이의 문자열 추출하기, 정규식 졸리운_곰 2021.05.24 725
416 [Java 자료구조] [JAVA] Java언어로 JSON 생성, 파싱 예제 file 졸리운_곰 2021.05.17 11
415 이클립스에서 java 버전 변경 file 졸리운_곰 2021.04.29 8
414 [Java 자료구조] [Java] Immutable Class (불변 클래스) file 졸리운_곰 2021.03.07 15
413 [Java 자료구조] 불변 객체란? Java Immutable Object file 졸리운_곰 2021.03.07 13
412 [Java 자료구조] [Java] Immutable Object(불변객체) 졸리운_곰 2021.03.07 16
411 [java 자료구조] Oracle + Mybatis 환경에서의 Date 다루기 졸리운_곰 2021.02.25 23
대표 김성준 주소 : 경기 용인 분당수지 U타워 등록번호 : 142-07-27414
통신판매업 신고 : 제2012-용인수지-0185호 출판업 신고 : 수지구청 제 123호 개인정보보호최고책임자 : 김성준 sjkim70@stechstar.com
대표전화 : 010-4589-2193 [fax] 02-6280-1294 COPYRIGHT(C) stechstar.com ALL RIGHTS RESERVED