본문 바로가기
자바스크립트&캔버스

자바스크립트와 캔버스 3번째 시간, 공튀기기 놀이

1. HTML 표준 CANVAS 기술 소개 / https://itadventure.tistory.com/130

2. 자바스크립트와 CANVAS 두번째시간. 캔바스에 눈을 내리자 / https://itadventure.tistory.com/131

◐ 3. 자바스크립트와 캔버스 3번째 시간, 공튀기기 놀이 ◑


매력덩어리 캔버스 3번째 시간입니다 :)

오늘은 공놀이를 할까 합니다.
떨어지는 공을 받아치며 벽돌을 깨뜨리는 게임을 해보신 적이 있다면 아주 친근하실텐데요.
벽돌까지는 안 나오고 공만 나옵니다.

사방이 꽉 막힌 공간이 있습니다.
그 속에서 하나의 공이 등장하며 여기저기를 떠돌아 다닙니다.
벽에 닿을때마다 반사되어 반대쪽으로 튀어다니긴 하지만 마찰력이 없기 때문에
튀어다니는 힘은 전혀 줄어듬이 없습니다.

아래 URL에서 미리보기를 하실 수 있습니다.
PC든 모바일이든 모두 잘 작동합니다.

http://dreamplan7.cafe24.com/canvas/cray03.php

우선 공이 3개 튀어다니는 소스를 먼저 살펴보실까요?

<!DOCTYPE html>
<html lang="ko">
<head>
	<meta charset="UTF-8">
	<title>캔바스 샘플#3</title>
</head>
<body>
https://blog.naver.com/ephraimdrlee 
/ 크레이의 세컨드라이프 탐구생활 네이버 블로그<br/>
<script src="https://code.jquery.com/jquery-3.4.1.min.js"></script>
<script>
// 기본 초기화
var init=false;
var myCanvas;
var Context;

// 공의 갯수
var ball_max =1;
var balls=Array();
var ball_image = new Image();

// 초기화
function Init()
{
	if(init==false)
	{
		myCanvas=document.getElementById("MyCanvas");
		Context=myCanvas.getContext("2d");
		ball_image.src = "https://i.imgur.com/wPPFCbU.png";
		for(i=0;i<ball_max;++i)
		{
			var obj=new Object();
			obj.x=Math.random()*myCanvas.width;
			obj.y=Math.random()*myCanvas.height;
			obj.size=Math.random()*32+16;
			obj.xdir=Math.random()*16+3;
			obj.ydir=Math.random()*16+3;
			balls.push(obj);
		}
		init=true;
	}
}

function Run()
{
	for(i=0;i<ball_max;++i)
	{
		balls[i].x+=balls[i].xdir;
		if(balls[i].x<balls[i].size/2)
		{
			balls[i].x=balls[i].size/2;
			balls[i].xdir=-balls[i].xdir;
		}
		if(balls[i].x>myCanvas.width-balls[i].size/2)
		{
			balls[i].x=myCanvas.width-balls[i].size/2;
			balls[i].xdir=-balls[i].xdir;
		}
		balls[i].y+=balls[i].ydir;
		if(balls[i].y<balls[i].size/2)
		{
			balls[i].y=balls[i].size/2;
			balls[i].ydir=-balls[i].ydir;
		}
		if(balls[i].y>myCanvas.height-balls[i].size/2)
		{
			balls[i].y=myCanvas.height-balls[i].size/2;
			balls[i].ydir=-balls[i].ydir;
		}
	}
	onDraw();
}

// draw 이벤트
function onDraw()
{
	if(init==false)return;
	Context.strokeStyle="#000";
	Context.lineWidth=1;
	Context.strokeRect(0, 0, myCanvas.width-1, myCanvas.height-1);
	Context.fillStyle="#fcfcfc";
	Context.fillRect(1, 1, myCanvas.width-2, myCanvas.height-2);
	for(i=0;i<ball_max;++i)
	{
		Context.drawImage(ball_image, 
			balls[i].x-balls[i].size/2, 
			balls[i].y-balls[i].size/2,
			balls[i].size,
			balls[i].size
		);
	}
	Context.filter = "none";
}

$(document).ready(function(){
	Init();
	setInterval(Run, 20);
});
</script>

<canvas id="MyCanvas" width=800 height=600>
Canvas is not supported.
</canvas>

</body>
</html>

지난번 소스와 거의 비슷합니다.

거의 3단계인데요. 아래와 같습니다.

첫번째 / 공의 속성 정보 초기화,

두번째 / 시간에 따른 공의 이동 시뮬레이션 처리

세번째 / 실제적인 공들의 화면 표시

우선 소스를 살펴보기 전에 먼저 상상을 해보실까요?

공들이 각각 하나의 작은 살아있는 생명체라고 생각한다면

공이 튀어다니려면 공은 어떤 속성들을 가지고 있어야 할까요?

전체적으로는 공의 갯수를 생각해볼 수 있습니다.

공이 몇개나 튀어다니면 될까요?

그건 자유롭게 변경할 수 있도록 전역변수를 생성해놓으면 됩니다.

ball_max 라고 이름지어보도록 할까요?

기본값은 3 정도로 주어보도록 하겠지만 영상에서 보셨듯이 자유롭게 변경할 수 있습니다.

그 다음으로 각각의 공들을 생각함에 있어서 쉽게 생각하려면 하나의 공을 먼저 생각해야 합니다.

우선, 하나의 공은 캔바스 내에서의 위치값을 가지고 있어야 합니다.

공 자신이 어느 위치에 있는지를 알아야 캔바스 안에서 표현이 가능하겠지요.

캔바스는 2차원 좌표를 가지고 있기 때문에 이 좌표값을 x, y 로 표현할 수 있습니다.

또한 공들마다 크기가 다른 것을 보실 수 있지요.

각각의 공들은 크기 값을 가지고 있습니다.

이 크기를 size 라는 이름으로 주어보도록 하지요

아울러 공들은 이동을 하는데 방향값을 가지고 있습니다.

이를 벡터라고 생각해볼까요? x 방향으로의 이동량y 방향으로의 이동량을 가지고 있는데 한번턴마다 그 이동량만큼 동시에 이동하는 것이지요.

x방향으로의 이동량을 xdir, y방향으로의 이동량을 ydir 로 표현해보겠습니다.

그 밖에도 욕심을 내자면 공이 벽에 튕길 때 바로 튕기는 것이 아니라

약간의 지연시간과 함께 공이 찌그러지는 모습을 표현할 수도 있고,

공끼리 부딪힐 때 서로 반대쪽으로 튕긴다든가 다양한 상황을 생각할 수도 있겠지만,

오늘은 이 정도 선까지만 생각하겠습니다 :)

우선 페이지 로딩이 완료되면,

jquery 로 Init() 함수를 1회 실행, setInterval 기능으로 Run() 함수를 지속 실행해주는 부분은 지난 시간과 동일합니다.

$(document).ready(function(){
	Init();
	setInterval(Run, 20);
});

전역변수로는 공의 총 갯수를 선언하고 공 이미지를 담을 ball_image 변수,

그리고 각각의 공을 담을 배열을 미리 정의해 놓습니다.

// 기본 초기화
var init=false;
var myCanvas;
var Context;

// 공의 갯수
var ball_max =3;
var balls=Array();
var ball_image = new Image();

초기화 함수 Init 에서는 캔바스에 대한 기본 정보를 받아오는 부분과 함께

// 초기화
function Init()
{
	if(init==false)
	{
		myCanvas=document.getElementById("MyCanvas");
		Context=myCanvas.getContext("2d");

공의 이미지 정보를 받아 옵니다.

공의 이미지는 주소는 크레이가 준비한 전용 url 을 사용해도 되지만 독자 여러분께서 준비한 이미지를 사용하셔서 구현하셔도 더 뿌듯하시겠지요 :)

		ball_image.src = "https://i.imgur.com/wPPFCbU.png";

이제 공의 갯수만큼 오브젝트를 만들어서 공 배열에 집어넣을 차례입니다.

공의 갯수만큼 for 문을 이용하여 반복루프를 만들어 준 다음에,

		for(i=0;i<ball_max;++i)
		{
                :
		}

반복 루프안에서 공 오브젝트를 하나 생성하고 속성을 정해줍니다.

			var obj=new Object();

x, y좌표는 캔바스 내의 임의의 위치로 랜덤하게 정해주고

			obj.x=Math.random()*myCanvas.width;
			obj.y=Math.random()*myCanvas.height;

공의 크기를 정해줍니다. 너무 작으면 좀 이상하니까 최소 16에서 48까지의 크기로 랜덤하게 정해줍니다. 0~32의 랜덤 수치를 발생하고 거기에 16을 더해주는 방법을 사용합니다.

			obj.size=Math.random()*32+16;

그리고 공의 속도를 정해주는데, X방향 3~19, Y방향 3~19의 벡터로 정해줍니다.

			obj.xdir=Math.random()*16+3;
			obj.ydir=Math.random()*16+3;

이렇게 생성된 이후 속성들이 정해진 공 오브젝트를 공 배열에 집어넣는 것으로 하나 단위의 공 생성이 반복 루프 안에서 완성됩니다.

			balls.push(obj);

반복문이 완료된 후 초기화가 끝났다는 표식으로,

init 값을 true 로 바꾸어 줍니다.

		init=true;

다음으로 0.02초 단위로 실행되는 Run 함수를 살펴보면,

공들 각각을 시뮬레이션 처리하기 위해 반복문을 수행합니다.

	for(i=0;i<ball_max;++i)
	{
              :
    }

먼저 공의 x좌표를 xdir 방향만큼 이동하고 y좌표 또한 ydir 방향만큼 이동합니다.

xdir 는 양수일 수도 있고 음수일 수도 있는데, 처음 초기값은 양수이기 때문에 양수 방향으로 이동합니다.

하지만 중간에 음수가 되기도 하고 다시 양수가 되기도 합니다.

		balls[i].x+=balls[i].xdir;
                  :
		balls[i].y+=balls[i].ydir;

그러면서 만약 공이 화면의 경계에 닿거나 넘칠 경우 공의 방향을 바꿔줍니다.

이 때 약간의 예외 처리 또한 병행됩니다.

		if(balls[i].x<balls[i].size/2)
		{
			balls[i].x=balls[i].size/2;
			balls[i].xdir=-balls[i].xdir;
		}
		if(balls[i].x>myCanvas.width-balls[i].size/2)
		{
			balls[i].x=myCanvas.width-balls[i].size/2;
			balls[i].xdir=-balls[i].xdir;
		}

만일 공의 끝이 캔버스의 왼쪽 경계를 넘어가는지 여부를 판단하려면,

공의 중심좌표를 기준으로 공의 크기의 절반을 빼준 위치가 캔버스 왼쪽 경계선을 벗어나는지를 판단해주면 됩니다.

이 부분을 판단하는 비교문은 아래와 같은데, 처음에는 은근 헷갈릴수 있습니다만

반복해서 보다 보면 어느 순간 이해가 됩니다 :)

if(balls[i].x<balls[i].size/2)
       :
if(balls[i].x>myCanvas.width-balls[i].size/2)
       :
if(balls[i].y<balls[i].size/2)
       :
if(balls[i].y>myCanvas.height-balls[i].size/2)

공이 너무 캔버스 바깥쪽으로 깊이 벗어나면 느낌이 이상하기 때문에

경계를 넘을 때마다 공의 위치를 벽에 딱 붙입니다.

각각 4개의 if문에 따라 각각 붙이는 위치가 다르지요.

			balls[i].x=balls[i].size/2;
                      :
			balls[i].x=myCanvas.width-balls[i].size/2;
                      :
			balls[i].y=balls[i].size/2;
                      :
			balls[i].y=myCanvas.height-balls[i].size/2;

그리고 다음번에는 공이 반대 방향로 이동하도록 방향을 바꾸어 줍니다.

			balls[i].xdir=-balls[i].xdir;
                     :
			balls[i].xdir=-balls[i].xdir;
                     :
			balls[i].ydir=-balls[i].ydir;
                     :
			balls[i].ydir=-balls[i].ydir;

이 과정 전체를 합쳐서 보면 아래와 같지요

	for(i=0;i<ball_max;++i)
	{
		// 공의 x좌표를 x방향벡터만큼 이동합니다.
		balls[i].x+=balls[i].xdir;
		// 공이 왼쪽 경계를 넘은 경우
		if(balls[i].x<balls[i].size/2)
		{
			// 왼쪽 경계에 딱 붙이고
			balls[i].x=balls[i].size/2;
			// 다음번 이동 방향을 반대쪽(오른쪽)으로 바꿉니다.
			balls[i].xdir=-balls[i].xdir;
		}
		// 역시 공이 오른쪽 경계를 넘은 경우
		if(balls[i].x>myCanvas.width-balls[i].size/2)
		{
			// 공을 오른쪽 경계선에 딱 붙이고
			balls[i].x=myCanvas.width-balls[i].size/2;
			// 다음번에는 왼쪽으로 이동하게 합니다.
			balls[i].xdir=-balls[i].xdir;
		}
		// 공의 y좌표 또한 y방향 벡터만큼 이동합니다.
		balls[i].y+=balls[i].ydir;
		// 공이 위쪽 경계선을 넘으면
		if(balls[i].y<balls[i].size/2)
		{
			// 위쪽 경계선에 붙이고
			balls[i].y=balls[i].size/2;
			// 다음번 이동할때는 공이 아랫쪽으로 향하게 합니다.
			balls[i].ydir=-balls[i].ydir;
		}
		// 반대로 공이 아랫쪽 경계선을 넘는다면
		if(balls[i].y>myCanvas.height-balls[i].size/2)
		{
			// 아랫쪽 경계선에 붙이고
			balls[i].y=myCanvas.height-balls[i].size/2;
			// 다음번에는 위쪽으로 이동하도록 합니다.
			balls[i].ydir=-balls[i].ydir;
		}
	}

그 다음으로는 공을 실제 캔버스에 그려주는 부분인데요.

// draw 이벤트
function onDraw()
{
	if(init==false)return;
	Context.strokeStyle="#000";
	Context.lineWidth=1;
	Context.strokeRect(0, 0, myCanvas.width-1, myCanvas.height-1);
	Context.fillStyle="#fcfcfc";
	Context.fillRect(1, 1, myCanvas.width-2, myCanvas.height-2);
	for(i=0;i<ball_max;++i)
	{
		Context.drawImage(ball_image, 
			balls[i].x-balls[i].size/2, 
			balls[i].y-balls[i].size/2,
			balls[i].size,
			balls[i].size
		);
	}
}

첫번째 시간과 크게 다를 건 없고,

유심히 보실 부분은 공을 캔바스에 그릴때, 약간의 계산식이 들어간다는 점이지요.

왜냐하면 캔바스는 공 이미지의 왼쪽 상단 좌표를 기준으로 기본적으로 그리기 때문에

공의 중심좌표를 기준으로 그리려면 아래와 같은 소스를 사용하여야 합니다.

		Context.drawImage(ball_image,  // 공 이미지
			// 공의 x좌표에서 공의 크기의 절반만큼을 뺀 x좌표
			balls[i].x-balls[i].size/2,  
			// 공의 y좌표에서 공의 크기의 절반만큼을 뺀 y좌표
			balls[i].y-balls[i].size/2,
			// 공의 x크기
			balls[i].size,
			// 공의 y크기
			balls[i].size
		);

 



대략적인 소스 흐름은 위와 같습니다.
초심자분에게는 난이도가 꽤 있는 편이기 때문에 여러번 반복해서 읽어주셔야 어느정도 감이 잡히실 겁니다.
여기까지 읽어 주셔서 감사합니다 :)

 

다음강좌 / https://itadventure.tistory.com/133

 

자바스크립트와 캔바스 4번째 시간. 마우스의 파동을 느껴봐!

자바스크립트와 CANVAS 두번째시간. 캔바스에 눈을 내리자 1. HTML 표준 CANVAS 기술 소개 / https://itadventure.tistory.com/130 2. 자바스크립트와 CANVAS 두번째시간. 캔바스에 눈을 내리자 / https://itadve..

itadventure.tistory.com