스스로 먹이를 찾는 뱀
✋ 소개
HTML Canvas
를 활용하여 이것 저것 만들어보던 중 마치 게임처럼 스스로 행동하는 무언가를 만들어보면 어떨까라는 생각을 하게 되었습니다.
그래서 먹이를 스스로 찾아 움직이는 뱀을 구현해보자고 마음 먹고 해당 프로젝트를 시작해보았습니다.

🧑🤝🧑 구성원
- 백엔드 및 프론트엔드 : 2명
💻 기술 스택
- TypeScript, HTML Canvas
내가 기여한 기능
일단 화면에 나타낼 요소들을 구분하여 4가지의 클래스로 나누어 코드를 작성해 구현하였습니다.
- Worm
- Food
- Explode
- Particle
Worm
뱀은 자신의 위치와 먹이의 위치를 이용해 먹이를 찾아가게 됩니다. 먹이를 먹게 되면 새로운 먹이가 랜덤한 위치에 생성되고 뱀은 이를 반복합니다.
뱀의 움직임을 구현하기 위해 뱀의 몸을 구성하는 몸통의 좌표를 스택 자료 구조에 넣어 관리하였고 움직임은 push
, pop
을 통해 구현하였습니다.
private set_next_head(position: Position): void {
this.body.unshift(position);
this.body.pop();
}
특히, 뱀의 바로 뒤쪽으로 먹이가 생길 시 자연스럽게 돌아나가는 걸 구현하고 싶어 많은 고민을 하였습니다. 따라서 뱀이 바라보는 지점과 먹이까지의 각도를 이용하여 조건을 걸어 일정 조건 시 무시하도록 하여 자연스러운 움직임을 구현할 수 있었습니다.
private find_food(): void {
const food = Data.getInstance().get_food;
const food_position = food.get_position;
const head = this.body[0];
const pre_angle = this.angle;
const target_angle = get_angle(head.x, head.y, food_position.x, food_position.y) * 60;
const error = this.pre_target_angle - target_angle;
this.pre_target_angle = target_angle;
const diff = target_angle - pre_angle;
if (error > 360 || error < -360) this.ignore_times = 20;
if (this.ignore_times === 0) {
if (diff < .1) this.angle -= 5;
else if (diff > .1) this.angle += 5;
else this.angle = 0;
}
else this.ignore_times--;
}
뱀이 음식을 찾아 먹는 과정을 구현하기 위해 우선, 뱀이 먹이와 충돌하는 것을 감지해야만 했습니다. 따라서 뱀의 머리 좌표와 먹이의 좌표를 이용하여 두 좌표 사이의 거리를 구하고 두 반지름의 합과 비교하여 충돌 여부를 판단하였습니다.
export const get_distance = (source_x: number, source_y: number, target_x: number, target_y: number) => {
const disX = source_x - target_x;
const disY = source_y - target_y;
return Math.sqrt((disX * disX) + (disY * disY));
}
private eat_food() {
const head = this.body[0];
const food = Data.getInstance().get_food;
const food_position = food.get_position;
const food_radius = food.get_radius;
const dis = get_distance(head.x, head.y, food_position.x, food_position.y);
if (dis < this.RADIUS + food_radius) {
Data.getInstance().set_explodes = new Explode(food_position.x, food_position.y, 25);
Data.getInstance().set_food = new Food();
}
}
충돌했다고 판단되면 먹이를 먹은 것으로 간주해 해당 지점에 이펙트를 추가하기 위해 이후에 설명할 Explode
클래스를 이용해 이펙트를 나타냈고 새로운 먹이를 생성하였습니다.
Food
현재 클라이언트가 보고 있는 캔버스 사이즈를 계산하여 캔버스 사이즈 내에서 랜덤한 좌표를 생성하였고 해당 좌표에 먹이가 나타나도록 구현하였습니다.
Explode, Particle
뱀이 먹이를 먹는 과정은 전부 구현하였는데 먹이를 먹은 후 해당 위치의 먹이가 사라지고 다른 위치에 먹이가 딱 생기는 모습이 너무 밋밋해보였습니다. 그래서 게임처럼 먹이를 먹은 후 이펙트가 나타나도록 구현하였습니다.
이펙트는 폭죽이 터지는 모습을 구현하고 싶었습니다.
이펙트를 나타내기 위해서 Explode
와 Particle
클래스를 작성하였습니다.
Particle
클래스는 폭죽이 터졌을 때 나타나는 수많은 불꽃들을 나타내기 위해 사용됩니다.
그리고 Explode
클래스는 수많은 Particle
클래스들을 관리하는 하나의 큰 폭죽을 나타내기 위해 사용됩니다.
따라서 Particle
클래스는 Explode
클래스 내부에서 사용되고 있는 형태로 구현되었습니다.
Explode
클래스는 생성되면서 랜덤한 색상의 Particle
클래스들을 생성하게 됩니다.
// Explode
constructor(x: number, y: number, particle_cnt: number) {
this.life = true;
this.position = { x, y };
this.particles = [];
this.colors = ['#07B0F2', '#27CDF2', '#ADBF24', '#F2B705', '#D96941'];
for (let i = 0; i < particle_cnt; i++) {
const color_index = Math.round(Math.random() * this.colors.length - 1);
const color = this.colors[color_index];
this.particles.push(new Particle(x, y, 5, color));
}
}
그렇게 생성된 수많은 Particle
클래스는 여러 가지 기능을 수행하게 됩니다.
우선, 사방으로 날라가는 모습을 표현하기 위해 랜덤한 반지름과 가속도를 가지게 됩니다.
// Particle
constructor(x: number, y: number, r: number, color: string) {
this.life = true;
this.postion = { x, y };
const velocity_x = positive_or_negative() ? Math.random() * 8 : -(Math.random() * 8);
const velocity_y = positive_or_negative() ? Math.random() * 8 : -(Math.random() * 8);
this.velocity = { x: velocity_x, y: velocity_y };
this.r = r;
this.COLOR = color;
}
이렇게 생성된 Particle
은 가속도에 의해 자연스럽게 x, y 좌표로 움직이게 되고 마치 폭발 후 자연스럽게 사방으로 불꽃들이 퍼지는 모습처럼 보이도록 구현할 수 있었습니다.
최적화
여러 가지 표시할 요소들을 계산하면서 구현하다보니 이펙트가 많이 표시될 시 느려질 수 있겠다는 생각이 들었습니다. 따라서 사람이 인지하지 못하는 상황을 계산해 해당 조건일 때는 렌더링하지 않고 연산에서 제외시키는 방법으로 최적화를 진행하였습니다.
최적화는 Explode
와 Particle
클래스에 적용하였습니다.
life
라는 변수를 두어 해당 변수가 false
일 경우에는 연산에서 제외하여 렌더링되지 않는 방법을 이용해보았다.
private life_check() {
const stageWidth = document.body.clientWidth;
const stageHeight = document.body.clientHeight;
if (
this.postion.x < 0
|| this.postion.x > stageWidth
|| this.postion.y < 0
|| this.postion.y > stageHeight
|| this.r < .1
) this.life = false;
}
life
는 화면에서 벗어나거나 또는 사람이 인지하기 어려울 정도로 반지름의 크기가 작아지면 false
가 되도록 구현하였습니다.
그렇게 life
가 false
가 된 요소들은 연산에서 제외시켜 빠른 속도를 유지할 수 있도록 최적화 할 수 있었습니다.
마치며
평소 웹사이트 개발 시 자주 사용되는 기술은 아니어서 어려움은 있었지만 새로운 기술을 배운다는 점에 있어 흥미를 많이 느꼈고 자신감 또한 얻을 수 있었습니다.
