HTML5 미니게임 개발 튜토리얼
이전 글 : Swing my baby 를 통해 보는 HTML5 게임 개발(4)
현재 글 : Swing my baby 를 통해 보는 HTML5 게임 개발(5)
다음 글 : Swing my baby 를 통해 보는 HTML5 게임 개발(6)
분량조절에 대차게 실패한 시리즈.... 뭐 어쨌든 이렇게 된 거 어떻게든 끝까지 한 번 가보자. 지난 시간에 지형을 만들고 충돌체크까지 했으니 이번엔 아이템을 만들어 보자.
개발 과정
아이템 만들기
이제 드디어 아이템을 만들 차례다! 게임의 긴장감과 재미를 높여주는 아주 소중한 존재긴 하지만 사실 어려운 부분은 위에서 다 했으니 이 녀석 정도는 아주 간단히 구현할 수 있다. 차근차근 구현을 해보자.
아이템 스프라이트
먼저 아이템들의 이미지를 스프라이트로 정의해두자. 스크립트 맨 위에 정의했던 SpriteDefs
에 아래 내용을 추가한다.
"item": [ { x: 160, y: 80, w: 50, h: 50, origin: { x: 25, y: 25 } }, { x: 210, y: 80, w: 50, h: 50, origin: { x: 25, y: 25 } }, { x: 300, y: 0, w: 40, h: 40, origin: { x: 20, y: 20 } }, { x: 300, y: 40, w: 40, h: 40, origin: { x: 20, y: 20 } }, { x: 300, y: 80, w: 40, h: 40, origin: { x: 20, y: 20 } }, { x: 300, y: 120, w: 40, h: 40, origin: { x: 20, y: 20 } }, { x: 300, y: 160, w: 40, h: 40, origin: { x: 20, y: 20 } }, { x: 300, y: 200, w: 40, h: 40, origin: { x: 20, y: 20 } }, { x: 160, y: 260, w: 40, h: 40, origin: { x: 20, y: 20 } } ]
그리고 AnimationDefs
에도 아래 내용을 추가하자.
"item": { 'potion': [{ frame: 0, duration: 1 }], 'mana': [{ frame: 1, duration: 1 }], 'coin': [{ frame: 2, duration: 0.1 }, { frame: 3, duration: 0.1 }, { frame: 4, duration: 0.1 }, { frame: 5, duration: 0.1 }, { frame: 6, duration: 0.1 }, { frame: 7, duration: 0.1 }], 'magnet': [{ frame: 8, duration: 1 }] }
사실 물약이나 자석에는 애니메이션이 없지만 동전은 애니메이션이 있으니까, 귀찮으니 다 애니메이션으로 처리하려는 썩은 근성으로 이렇게 됐는데, 이게 께름칙하면 동전만 애니메이션으로 직접 처리하도록 하자 헤헿
아이템 관리자
아이템은 한 번에 아주 여러개의 인스턴스가 존재한다. 이걸 각자 다 GameScene
의 child
로 직접 관리하는 것도 사실 별 상관이야 없겠지만 뭔가 계란 후라이 다섯개를 하는 데 가스렌지 다섯개랑 후라이팬 다섯개를 쓰는 것 같은 찜찜함이 마음 한 구석을 저며오는 것 같지 않은가? 그러니까 아이템들을 모아서 관리해주는 아이템 관리자 클래스를 만들고 아이템 인스턴스들을 이 녀석이 관리하도록 하자. 아이템 관리자가 아이템을 생성하고, 업데이트하고, 효용이 끝난 아이템을 삭제하는 작업을 도맡아서 처리하게 되면 우리는 좀 더 편안한 마음으로 건설적인 미래를 향해 나아갈 수 있겠지? 게다가 각 아이템들이 이미지와 스프라이트시트를 매번 생성하는 것도 비효율적이니 그것도 아이템 관리자가 가지고 있다가 제공해주도록 하자!
class ItemManager extends GameObject { constructor() { super(); this.items = []; this.minX = 0; this.img = new Image(); this.img.src = "images/eclipse_sprites.png"; this.spriteSheet = new SpriteSheet(this.img, SpriteDefs.item); } init(){ super.init(); this.items = []; this.minX = 0; } update(timeDelta) { this.items.forEach((i) => { i.update(timeDelta); // 화면 밖으로 나간 아이템은 제거 대상으로.. if( i.x < this.minX - 50 ) i.destroy = true; }); // 제거 대상인 아이템들을 실제로 제거 this.items = this.items.filter((i) => { return !i.destroy; }); } render(ctx) { let maxX = this.minX + 540; this.items.forEach((i) => { i.render(ctx, maxX); }); } createItem(type, x, y) { // 나중에 여기서 타입별로 아이템을 만들도록 하자 } }
그리고 GameScene
의 생성자와 init
에도 당연히 추가해줘야겠지?
// GameScene.constructor 에 추가 this.itemManager = new ItemManager(); this.children.push(this.itemManager); // GameScene.init 에 추가 this.itemManager.init();
눈치 챘을수도 있겠지만 ItemManager.minX
도 게임씬의 업데이트시에 x 좌표값을 넣어줘야 한다. GameScene.update
에도 아래 내용을 추가하자.
this.itemManager.minX = this.cameraX - 200;
이럴 줄 알았으면 이것도 다 그냥 공용 인터페이스로 만들어버리는건데... 귀찮지만 이제 와서 별 수 없지.
아이템 공통 구현
1편의 게임 영상을 잊었다면 다시 복습하고 오도록 하자. 아이템들이 공통적으로 갖는 특징은 뭐가 있을까? 아이템들은 각각 스프라이트(혹은 애니메이션)을 가지고 있고, 각자 자기 자리를 지키고 있으며, 자기 자리에서 위아래로 둥실둥실 떠있다. 동전을 제외한 아이템들은 화면 밖에 있는 동안은 자기 높이를 말풍선으로 표시한다. 또, 캐릭터와 부딪히는 순간 아이템은 사라지고 각 아이템에 걸맞는 효과가 캐릭터에 반영된다. 각 효과는 아이템마다 다르지만 그 외의 것들은 공통이다. 그럼 공통되는 부분을 모아서 일단 클래스를 만들어보자.
class Item extends GameObject { constructor(x, y, anim) { super(); this.x = x; this.originY = y; // 원래의 y 좌표 this.ty = 0; // 원래의 y 좌표에서 위아래로 이동한 상대적인 값 this.radius = 20; // 충돌 범위 this.elapsed = 0; this.destroy = false; this.anim = anim; } get y() { // 충돌체크에 편하도록 y 를 그냥 이렇게 얻어갈 수 있도록 하자. return this.originY + this.ty; } update(timeDelta) { super.update(timeDelta); this.elapsed += timeDelta; this.ty = (Math.sin(this.elapsed) * 80) - 40; // -40 ~ 40 까지 오락가락하도록... this.anim.update(timeDelta); } isHit(char){ // 충돌 체크 if( Math.distance(char, this) <= (char.radius + this.radius)){ this.action(); } } render(ctx, maxX) { // maxX 는 화면의 오른쪽 경계를 넘겨받는다 if (this.x > maxX + this.radius*2) { // 아직 화면에 나오기 전이다. 높이를 표시해주자. ctx.save(); ctx.fillStyle = "rgba(255,255,255,0.7)"; ctx.fillRect(maxX - 40, this.y - 20, 30, 40); ctx.fillRect(maxX - 45, this.y - 15, 40, 30); ctx.fillRect(maxX - 5, this.y - 2, 5, 5); this.anim.draw(ctx, maxX - 25, this.y, {scale:0.5}); ctx.restore(); } else { this.anim.draw(ctx, this.x, this.y, {}); // 애니메이션 그려주기 } } action(){ // 아이템의 효과를 나타내는 부분. 각 아이템 종류별로 서로 다른 처리가 필요하다. // 그치만 아이템이 사라진다는 건 공통적이지! this.destroy = true; } }
아이템 생성 및 표시
아이템들은 캐릭터가 앞으로 나아감에 따라서 계속해서 생성된다. 그리고 화면 밖으로 지나간 아이템은 삭제된다. 아이템이 화면 내에 있을 때는 화면에 아이템이 표시되어야 한다. 그리고 아이템이 화면 밖에 있는 동안에도 물약이나 자석의 경우 높이가 표시된다. 화면 밖으로 나간 아이템이 삭제되는 부분은 이미 잘 해주고 있으니 나머지를 생각해보자.
아이템은 각각 생성되는 시점이 다르다. 일단 게임을 보면 일정 간격으로 동전 아이템이 늘어서있다. 그리고 그 사이사이에 일정 간격으로 파란 물약/빨간 물약/자석 중 하나가 생겨난다. 우선 아이템을 각각에 맞춰서 생성하는 부분 부터 만들어야겠다.
각각의 아이템은 아래와 같이 정의해주자.
class Potion extends Item { action(){ super.action(); return "jump"; } } class Mana extends Item { action(){ super.action(); return "mana"; } } class Magnet extends Item { action(){ super.action(); return "magnet"; } } class Coin extends Item { action(){ super.action(); return "coin"; } render(ctx){ // 동전은 화면 밖에 있어도 높이를 표시해줄 필요가 없기 때문에 render 를 재정의하자. this.anim.draw(ctx, this.x, this.y, {}); } update(timeDelta){ this.elapsed += timeDelta; this.ty = (Math.sin(this.elapsed) * 10) - 5; // 동전은 예외적으로 위아래 움직임이 작다. this.anim.update(timeDelta); } }
그리고 ItemManager
의 createItem
메소드를 채워보자. 이 메소드는 사실 하는 일이 별로 없다. 요구받은 위치에 적절한 아이템을 생성해서 items
에 넣어주기만 하면 된다.
createItem(type, x, y) { let item = null; switch(type){ case "potion": item = new Potion(x, y, new Animation(this.spriteSheet, AnimationDefs.item.potion)); break; case "mana": item = new Mana(x, y, new Animation(this.spriteSheet, AnimationDefs.item.mana)); break; case "coin": item = new Coin(x, y, new Animation(this.spriteSheet, AnimationDefs.item.coin)); break; case "magnet": item = new Magnet(x, y, new Animation(this.spriteSheet, AnimationDefs.item.magnet)); break; } if( item ) this.items.push(item); }
createItem
이 생겼으니 이제 아이템을 생성해볼 수 있겠지? 아이템은 언제 생성해야 할까?
우선 게임이 시작할 때 기본적으로 화면 전체에 아이템이 생성되어 있어야 한다. 그리고 앞으로 갈 때마다 더 앞쪽에 있는 아이템을 계속해서 생성해줘야 캐릭터가 진행하면서 아이템이 끊기지 않고 나오겠지?
init(){ super.init(); this.items = []; this.minX = 0; this.lastItemX = 0; this.coinY = 260; let cnt = 0; let types = ["] while(this.lastItemX < 540 + 100){ this.lastItemX += 80; // 임의의 간격 this.coinY += Math.floor(Math.random() * 50) -25; this.createItem("coin", this.lastItemX, this.coinY); cnt++; if( cnt%10 == 0 ){ let tp = } } }
this.lastItemX
라는 변수는 아이템의 최종 생성 위치를 나타내는 쓸모있는 변수다. this.coinY
는 동전들을 질서 정연하게 생성하기 위한 칭구다. 얘들을 생성자에도 넣어주자.
/// ItemManager.constructor 에 추가 this.lastItemX = 0; this.coinY = 260;
여기까지 하면 이제 아이템들이 잘 생성되어서 잘 표시되는 것을 볼 수 있다. 꺅 뿌듯행!
충돌 체크
하지만 캐릭터가 아이템에 닿아도 아무 일도 일어나질 않는다. 이래선 곤난하지.. 아이템과 캐릭터의 충돌 처리를 해주도록 하자. 먼저 아이템 클래스에 충돌 처리를 하는 메소드를 만들자.
// Item 클래스에 추가 // 캐릭터를 인자로 받아서 아이템이 충돌했는지 확인한다. // 충돌했으면 캐릭터에 이 아이템의 효과를 전달해준다. // 알다시피 this.action 내에서 이 아이템을 삭제처리하는 부분이 있으니 이정도면 처리는 끗! isHit(char){ if( Math.distance(char, this) <= (char.radius + this.radius)){ // 아이템의 중심점과 캐릭터간의 거리가 둘의 반지름을 합친 것 보다 짧을때.. char.gotItem(this.action()); // 캐릭터에게 아이템 효과를 적용 } }
캐릭터에 gotItem
이라는 메소드가 없지만 이건 조금 있다가 만들기로 하고 이어서 ItemManager
에 충돌체크를 추가하자
// ItemManager 클래스에 추가 checkCollision(char){ // 캐릭터와 충돌을 판정 this.items.forEach((i)=>{ i.isHit(char); }); }
그리고 나서 이제 GameScene.update
에서 이 메소드를 호출해주면 되겠지? GameScene.update
에 아래 내용을 추가하자.
this.itemManager.checkCollision(this.character);
이제 캐릭터의 gotItem
메소드를 만들자
// Character 클래스에 추가 gotItem(item){ switch(item){ case "jump": // 점프 물약만 먼저 구현해보자. this.pivot = null; this.force.y -= 8; this.force.x += 10; break; case "mana": break; case "magnet": break; case "coin": break; } }
이렇게 구현하고 다시 게임을 실행해보면 이제 아이템이 먹어지고, 빨간 물약을 먹으면 냅다 점프하는 걸 볼 수 있다.
아이템별 구현
각 아이템들은 서로 다른 효과를 나타낸다. 이 중에 점프 물약은 이미 구현이 완료되었으니 내버려두고, 나머지 아이템들의 구현을 생각해보자.
동전
동전은 소지금을 올려주면 된다. 아직 소지금을 쓸 데가 없긴 하지만 그게 뭐 중요한가? 로직도 단순하다. Character
클래스에 money
라는 멤버 변수를 추가하고 동전을 먹었을 때 올려주기만 하면 된다.
// Character 클래스의 생성자와 init 메소드에 추가 this.money = 0; // Character 클래스의 gotItem 메소드 중 coin 부분에 추가 this.money += 100;
마나포션
파란 물약은 마나 포션이다. 그런데 우리는 마나 같은거 안 키우고 있다. 여기서 이 게임의 마나가 어떤건지를 먼저 살펴보자.
이 게임에서 캐릭터가 줄을 던질 수 있는 횟수는 마나에 의존한다. 줄을 던질 때 마다 마나를 소모하고, 마나가 다 떨어지면 줄을 던질 수 없다. 그래서 유저는 마나 포션을 잘 먹으면서 이동해야 한다. 따라서 이 게임의 긴장감은 땅에 부딪히지 않는 것과, 마나가 다 떨어지지 않도록 마나포션을 잘 챙겨 먹는 것 두 가지에서 오게 된다. 그럼 위와 같은 내용을 어떻게 구현할까?
우선 캐릭터에 마나를 나타내는 멤버 변수를 추가하도록 하자. 마나 포인트의 줄임말인 mp
라고 쓰도록 하겠다.
// Character 클래스의 생성자와 init 에 추가 this.mp = 10;
그리고 gotItem
에서 마나 물약에 대한 효과를 추가하자. 일단 물약을 하나 먹을 때마다 3씩 채워주도록 하자.
// Character 클래스의 gotItem 에 추가 this.mp += 3;
그리고 줄을 던질 때 마다 mp
를 소모시키고, mp
가 없을 때는 줄을 던지지 못하게 하자. Character.setPivot
메소드를 아래처럼 변경하면 된다.
setPivot(point){ if(this.pivot === null && this.mp > 0){ // 줄이 안 걸려있고 mp 도 있어야 줄을 건다. this.mp--; // 이하 생략
이걸로 마나포션은 간단히 구현 완료!
그런데 마나가 없는 상태에서 줄에 매달려 있을 때 점프포션을 먹을 경우 날아가긴 하는데 더이상 줄을 뻗을 수 없어서 죽게 되는 문제가 있다. 점프 포션을 먹었을때도 한 번은 줄을 뻗을 수 있도록 mp+1 을 해주도록 하자.
// gotItem 의 jump 부분에 추가 this.mp++;
자석
이 게임의 가장 중요한 아이템인 자석 차례다. 다들 알고 있겠지만 자석은 일정시간동안 주변의 모든 아이템을 캐릭터쪽으로 끌어당기는 역할을 한다. 이걸 어떻게 구현하면 좋을까? 그리고 자석 효과가 발동된 동안을 어떻게 사용자에게 보여줄 수 있을까? 먼저 캐릭터에 현재 자석아이템이 활성화 상태인지 나타내는 변수를 만들자.
// Character.construct 와 init 에 추가 this.magnet = 0; // 자석 효과가 남은 시간을 나타낸다.
그러면 일단 자석의 아이템 효과는 간단히 넣을 수 있다. 단순히 자석 지속 시간을 3초로 바꿔주면 된다.
// Character.gotItem 의 magnet 부분에 추가 this.magnet = 3;
그냥 3으로만 해주면 영원히 3인채로 자석이 적용되어버릴테니 character.update
에서 아래처럼 자석의 지속시간을 지속적으로 줄여주자.
this.magnet = Math.max(0, this.magnet - timeDelta);
이제 자석을 먹고 지속시간까지 잘 적용했지만 그게 눈에 보이질 않으니 도무지 잘 되고 있는지를 알 수가 없다. 자석의 효과가 지속되는 동안 자석 표시를 해주도록 하자. 지난 번에 추가했던 AnimationDefs
에 보면 magnet_field
라는 항목이 있다. 요 녀석은 자기장을 표시하는 애니메이션이다. 이걸 magnet
이 0보다 큰 동안 계속 표시하도록 하자. Character.render
의 캐릭터를 표시하는 부분 바로 전에 아래와 같은 내용을 추가하자.
if( this.magnet > 0 ) { this.animations['magnet_field'].draw(ctx, this.x, this.y, {}); }
그리고 자기장 애니메이션이 움직일 수 있도록 Character.update
에도 아래 내용을 추가해주자.
this.animations['magnet_field'].update(timeDelta);
이제 다시 게임을 실행해서 자석을 먹어보면 자기장이 캐릭터 주위에 물결치는 모습을 볼 수 있다. 그치만 자기장만 움직이면 뭘 하나... 아이템들이 딸려오질 않는데... 아이템들을 어떻게 딸려오게 할까? 아이템을 모조리 관리하는 ItemManager
에서 해주면 간단하겠지? 하지만 ItemManager
는 캐릭터가 현재 자석을 먹은 상태인지 아닌지를 알 방법이 없다. 그치만 뭐 간단한 문제다. update
에다가 캐릭터를 넘겨주면 되겠지? 물론 checkCollision
메소드는 이미 캐릭터를 넘겨받고 있으니 거기서 해주면 되지 않겠냐고 생각할 수 있겠지만 그건 너무 근본없어 보인다는 단점을 제외하고도 timeDelta
를 받을 수 없다는 문제점까지 있다. GameScene
의 update
를 아래와 같이 고쳐주자.
// super.update(timeDelta) 줄을 삭제하고 아래 내용으로 대체한다. this.elapsed += timeDelta; this.children.forEach((ch)=>{ ch.update(timeDelta, this.character); // update에 캐릭터를 넘겨주긔 });
그리고 ItemManager.update
에서는 캐릭터를 받아서 자석에 대한 처리를 해주도록 하자.
update(timeDelta, char) { let inMagnet = char.magnet > 0; // 현재 자석상태인지 this.items.forEach((i) => { i.update(timeDelta, inMagnet); if( inMagnet ){ // 자석처리 i.originY = i.y; i.ty = 0; let ang = Math.atan2(char.y - i.y, char.x - i.x); //각도를 구해서 let pt = Math.getPoint(i, ang, 540 * timeDelta); //해당 각도로 가까워진 지점에 i.x = pt.x; //아이템의 좌표를 이동시켜준다. i.originY = pt.y; } // 화면 밖으로 나간 아이템은 제거 대상으로.. if (i.x < this.minX - 50) i.destroy = true; }); /// 이하 생략
item.update
에 inMagnet
을 넘겨주는데 item.update
에는 받아주는 부분이 아직 없다. 이 부분도 만들어주자.
item.update
를 아래처럼 바꿔주면 된다. 이런 처리가 필요한 이유는 자석에 끌리는동안은 위아래로 움직여서는 안되기 때문이다. 이걸 안해주면 아이템들이 자석에 끌려오지 않고 널뛰기를 하게 된다.
update(timeDelta, magnet) { super.update(timeDelta); this.elapsed += timeDelta*2; if( magnet ){ this.ty = 0; this.elapsed = 0; } else this.ty = (Math.sin(this.elapsed) * 80) - 40; // -40 ~ 40 까지 오락가락하도록... this.anim.update(timeDelta); }
물론 까먹지 말고 coin
에서 재정의한 update
에도 같은 코드를 넣어주자.
정말 피곤한 일이었지만 이걸로 아이템까지 드디어 만들고야 말았다! 해냈엉!
그럼 여기까지 만들면서 빼먹은 사소한 것들을 다듬고 넘어가도록 하자.
캐릭터의 동작들
MP가 오링났옹!
우리의 캐릭터는 이제 MP 를 가지고 있다. 그런데 MP가 다 떨어지는 걸 현재로서는 알 방법이 없다. 나중에 UI를 만들면서 남은 MP를 보여주는 것도 만들겠지만, 게임이니까 캐릭터의 모습에서 직관적으로 알 수 있는 방법이 있으면 더 좋겠지? mp가 없을 때는 평소와 달리 허우적대며 떨어지는 모습으로 애니메이션을 바꾸도록 하자.
// Character.update의 if (this.pivot === null) { 아래를 아래처럼 this.currentAnimation = (this.mp==0)?"fall":"spin"; // mp 가 없을 때는 fall 로 바꾸자.
공중제비를 돌고싶옹!
그러고보면 캐릭터가 공중에 있을 때는 웅크린 모션인데 빙글빙글 돌면 더 좋을 것 같다. 캐릭터에 rotation
이라는 변수를 추가하고 이걸로 캐릭터를 돌려주자.
// Character의 생성자와 init 에 추가 this.rotation = 0;
그리고 update
에서 이 변수를 야금야금 변경시키자.
// pivot 이 null 일때에 추가 this.rotation += -360 * timeDelta;
왜 그냥 360 이 아니고 -360이냐면, 양수를 주면 시계방향으로 돌아가기 때문이다. 그러면 이상하잖아? 마지막으로 Character.render
를 수정해서 rotation 을 적용해주자.
this.animations[this.currentAnimation] .draw(ctx, this.x, this.y, {rotate:(this.currentAnimation=='spin')?this.rotation:0});
그러면 이제 우리의 캐릭터가 빙글빙글 정신사납게 돌아가는 것을 볼 수 있다.
저 하늘의 별이 될 고 같옹!
캐릭터가 너무 높이 올라가면 화면에 캐릭터의 모습이 보이지 않는다. 그치만 이래서는 언제 떨어질지도 모르는 불안한 상태가 계속 이어지는 단점이 있다. 캐릭터가 너무 높이 올라갔을때는 캐릭터의 높이를 표시해주자. 아이템에 썼던 말풍선과 비슷한 방법으로 해주면 되니까 간단하겠지?
Character.render
에서 처리해주도록 하자. 바로 위에서 고쳤던 부분을 다시 아래와 같이 고쳐주자.
if( this.y < -20 ){ ctx.save(); ctx.translate(this.x, 0); ctx.fillStyle = "rgba(170,0,0,0.8)"; ctx.fillRect(-35, 10, 70, 30); ctx.fillRect(-30, 5, 60, 40); ctx.fillRect(-2, 0, 5, 5); ctx.fillStyle = "white"; ctx.textAlign = "center"; ctx.font = "bold 16px sans-serif"; ctx.fillText(Math.abs( (this.y/10)|0 ) + "m", 0, 30); ctx.restore(); }else{ this.animations[this.currentAnimation] .draw(ctx, this.x, this.y, {rotate:(this.currentAnimation=='spin')?this.rotation:0}); }
솔직히 이제 필요 없잖아?
그동안 나름대로 도움이 되었지만 이제는 필요가 없어진 캐릭터의 원과 막대기들을 그리는 코드들을 깔끔하게 삭제해 치우도록 하자. 아 상쾌해!
그대는 너무 빨라요
우리의 캐릭터는 넘나 조신하지만 빨간 물약 한두개만 먹으면 발정난 망아지마냥 끝도없이 넘나 빨라지는 문제가 있다. 세상엔 공기로 가득한데 어째서 우리의 캐릭터는 이렇게 끝간데를 모르고 가속하기만 하는걸까? 공기 저항이 얼마나 무서운건지 맛을 보여주도록 하자.
Character.update
의 pivot 이 null 인 경우의 코드에 아래 내용을 추가하자.
this.force.x *= 0.99;
그러면 이제 아래와 같은 모양새가 되겠지? jsfiddle 이 너무 스크롤을 퍼먹는 것 같아서 이번엔 codepen을 이용해봤다. 헤헿
See the Pen BpJxBo by LazyGyu (@lazygyu) on CodePen.
그럼 이번 편도 여기까지에서 접고 다음편에는 게임에 MSG를 쳐보도록 하자. 깜찍한 이펙트와 사운드를 입히면 게임이 한층 더 게임같아질 거다.