1. 프로젝트 개요
웹미니 프로젝트로 html, css, javascript 그리고 JQuery를 이용해 간단한 게임을 개발하는 프로젝트를 진행했다.
https://developer.mozilla.org/en-US/docs/Games/Tutorials/2D_Breakout_game_pure_JavaScript
위의 Mozilla developer 사이트에서 공부하며 참고를 많이 했고, 해당 튜토리얼 이외에
1) 제이쿼리 사용
2) 로컬스토리지에 점수와 시간 저장
3) 저장된 데이터 중복 제거 및 정렬
4) 게임시작/ 새로고치 버튼
네 가지는 새로 고안해 기능을 추가했다.
2. 사용 기술
HTML, CSS, JavaScript
3. 메인 화면
처음에 잡았던 컨셉이 옛날 콘솔 게임 화면이었기 때문에 픽셀이 그대로 드러나는 것 같은 이미지와 아이콘을 최대한 활용했다.
게임 시작 전에는 이렇게 검은 화면에 빨간 글씨로 게임 시작을 유도하는 문구를 띄웠다. canvas 에서는 텍스트를 따로 쓸 수가 없어 fillText로 글씨를 채워줬다.
3-1. HTML 코드 작성하기
우선 처음은 html:5 기본 템플릿으로 작성했고, 필요한 스크립트가 있다면 추가했다.
제이쿼리 import (js 파일 import 이전에 제이쿼리를 먼저 import 해줘야 한다.)
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.4/jquery.min.js"></script>
부트스트랩 import (버튼 디자인 이외에는 사용하지 않았다)
<link
href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css"
rel="stylesheet"
integrity="sha384-rbsA2VBKQhggwzxH7pPCaAqO46MgnOM80zW1RWuH61DGLwZJEdK2Kadq2F9CUG65"
crossorigin="anonymous" />
나머지 js와 css 파일도 link 해주고 타이틀을 변경해주면 head 태그는 마무리된다.
<body>태그
<body>
<div class="main">
<div class="gameMain">
<h1 class="title">Breack out!</h1>
<canvas id="gamezone" width="480" height="320"></canvas>
<div class="buttons">
<button class="startButton" id="startButton">Game Start!</button>
<button class="cancelButton" id="cancelButton">
<img src="../Assets/blockgame/undo.png" />
</button>
</div>
</div>
<div class="scores">
<table id="scoreTable">
<tr>
<th>Score</th>
<th>Date</th>
</tr>
</table>
</div>
</div>
</body>
메인 화면을 나타내는 main div에 게임화면을 나타내는 Game main, 그리고 canvas 를 담을 gamezone div 들을 만들었다. 그리고 게임시작 버튼과 새로고침 버튼을 담을 buttons div를 따로 만들었다.
게임 화면 아래에는 현재까지의 스코어 기록들이 쭉 나열되야하기 때문에 table 태그를 넣어줬고, table은 제이쿼리로 append 해 줄 예정이었기 때문에 table head만 우선 작성했다.
3-2. CSS 코드 작성하기
canvas {
background: rgba(238, 238, 238, 0.618);
display: block;
margin: 0 auto;
height: 430px;
position: relative;
}
canvas란, 자바스크립트와 css 를 이용해 웹 페이지에 그래픽을 그릴 수 있게끔 해주는 도구이다. 점, 선, 면을 그리거나 이미지를 추가하는데 사용된다. 도형을 움직이게 하는데 용이하기 때문에 canvas로 2d 또는 3d 애니메이션을 만들 수 있다.
<canvas id="gamezone" width="480" height="320"></canvas>
HTML 파일에서 위와같이 선언한 다음, css와 자바스크립트를 이용해 조작해주면 된다.
게임 화면의 경우에는
.main {
height: 100vh;
width: 100vw;
background-image: url("../Assets/blockgame/background.jpg");
background-position: center;
background-size: cover;
padding-top: 45px;
}
canvas {
background: rgba(238, 238, 238, 0.618);
display: block;
margin: 0 auto;
height: 430px;
position: relative;
}
.title {
text-align: center;
}
.gameMain {
width: 800px;
height: 700px;
margin: auto;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
gap: 20px;
background-image: url("../Assets/blockgame/pinpng.com-message-box-png-4212063.png");
background-position: center;
background-size: cover;
position: relative;
}
.gamezone {
height: 480px;
width: 480px;
border-radius: 10px;
background-color: rgb(3, 155, 3);
}
.scores {
color: rgba(238, 238, 238, 0.689);
height: 300px;
width: 750px;
margin: 125px auto;
background-color: black;
border-radius: 10px;
overflow: hidden;
}
위와같이 작성했고, 최대한 flex와 margin: auto를 통한 가운데 정렬 등 최근에 배운 것들 위주로 사용했다.
scores 라는 점수를 기록하는 테이블에는 overflow: hidden/scroll 중에 고민했었다. 기능상 scroll이 편하지만 스크롤 특성상 외관을 컨셉에 맞게 커스텀하기가 어려워서 hidden으로 고쳤다.
대신에 기록들은 잘 나온 점수가 맨 위로 배치되게끔 배열을 정리해뒀기 때문에 유저가 관심있어하는 데이터를 보기에는 충분할것이라고 생각한다..!
@import url("https://fonts.cdnfonts.com/css/public-pixel");
* {
font-family: "Public Pixel", sans-serif;
}
폰트는 게임 컨셉에 맞게 픽셀 폰트로 검색해서 나온 폰트를 사용해 모든 텍스트에 적용했다.
⭐️ 3-3. 자바스크립트 코드 작성하기
먼저 선언해야하는 코드는 아래와 같다.
var canvas = document.getElementById("gamezone");
var ctx = canvas.getContext("2d"); //캔버스에 그리기 위해 실질적으로 사용되는 도구인 rendering context => 2d
var startButton = document.getElementById("startButton")
var cancelButton = document.getElementById("cancelButton")
게임을 그릴 canvas, canvas에 2d 속성 주기, 그리고 이벤트를 필요로하는 버튼 두 가지를 자바스크립트 파일로 가져왔다.
그리고 캔버스에서 사용할 도형들의 크기나 거리를 상수로 지정해 선언한다.
var ballRadius = 10;
var x = canvas.width/2;
var y = canvas.height-30;
var dx = 2;
var dy = -2;
var paddleHeight = 10;
var paddleWidth = 75;
var paddleX = (canvas.width-paddleWidth)/2;
var brickRowCount = 3;
var brickColumnCount = 5;
var brickWidth = 75;
var brickHeight = 20;
var brickPadding = 10;
var brickOffsetTop = 30;
var brickOffsetLeft = 30;
이후 진행되는 '그리는'함수들은 모두 10밀리초에 한번씩 계속해서 실행된다. (setInterval 함수 사용)
빠른 속도로 반복해서 그려지고 지워짐으로써 도형들이 움직이는것 처럼 보이게 하여 애니메이션을 나타내는 방법을 사용하면 된다.
⚽️ 공을 움직여보자
아래는 공와 패들 (맨 아래 긴 직사각형)을 그리는 함수이다.
function drawBall() {
ctx.beginPath();
ctx.arc(x, y, ballRadius, 0, Math.PI*2);
ctx.fillStyle = "#0000FF";
ctx.fill();
ctx.closePath();
}
function drawPaddle() {
ctx.beginPath();
ctx.rect(paddleX, canvas.height-paddleHeight, paddleWidth, paddleHeight);
ctx.fillStyle = "#FF00FF";
ctx.fill();
ctx.closePath();
}
이제는 움직이는 공이 게임존의 사면에 닿았을 때 다른 방향으로 튕겨내는 코드를 작성해야한다.
캔버스는 위와같은 좌표를 가지고 있기 때문에 예를 들면 0보다 작을때, 그리고 height 보다 클 때 이런 식으로 조건문을 작성해야한다.
if(x + dx > canvas.width-ballRadius || x + dx < ballRadius) {
dx = -dx;
}
if(y + dy > canvas.height-ballRadius || y + dy < ballRadius) {
dy = -dy;
}
이렇게 작성하면 사면에 공이 닿았을 때 튕겨내는 기능을 추가할 수 있다.
▬ 패들을 움직여보자
패들이 공을 잡아냈을 때는 공이 다시 튕겨져나오고, 패들이 공을 잡아내지 못하고 놓쳐서 공이 아랫면에 닿았을 경우 gameover 가 되는 코드를 작성한다.
아래 코드는 패들 기능과 동시에 벽돌을 그리는 함수도 포함되어 있다. 이 함수가 바로 setInterval 함수의 콜백함수이다.
function draw() {
ctx.clearRect(0, 0, canvas.width, canvas.height); //직전에 그린 원 지우기
drawBricks();
drawBall();
drawPaddle();
drawScore();
collisionDetection();
//공이 캔버스의 끝(모서리)에 닿았을 때 튕겨내기
if(x + dx > canvas.width-ballRadius || x + dx < ballRadius) {
dx = -dx;
}
if(y + dy < ballRadius) {
dy = -dy;
}
else if(y + dy > canvas.height-ballRadius) {
if(x > paddleX && x < paddleX + paddleWidth) {
dy = -dy;
}
else {
// clearInterval(draw);
save(score, today);
location.reload();
}
}
if(rightPressed && paddleX < canvas.width-paddleWidth) {
paddleX += 7;
}
else if(leftPressed && paddleX > 0) {
paddleX -= 7;
}
x += dx; //x와 y축을 조금씩 바꾸면서 원이 이동하는 것 처럼 보이게 함
y += dy;
}
위에서 주목해야할 코드는 공을 놓쳤을 때 else if문인데, save 함수를 같이 설명해야하기 때문에 우선 동작이 가능하도록 게임 작동 코드 먼저 설명하도록 하겠다.
벽돌을 그리는 함수는 아래코드를 참고하자.
function drawBricks() {
for(var c=0; c<brickColumnCount; c++) {
for(var r=0; r<brickRowCount; r++) {
if(bricks[c][r].status == 1) { //status = 1 (=공이 치지 않아 벽돌을 그려도 되는 상태)
var brickX = (c*(brickWidth+brickPadding))+brickOffsetLeft;
var brickY = (r*(brickHeight+brickPadding))+brickOffsetTop;
bricks[c][r].x = brickX;
bricks[c][r].y = brickY;
ctx.beginPath();
ctx.rect(brickX, brickY, brickWidth, brickHeight);
ctx.fillStyle = "#800080";
ctx.fill();
ctx.closePath();
}
}
}
}
배열을 중첩시켜 2차원 배열을 생성하고 미리 선언해뒀던 상수들을 가져와 rect()을 그려주면 된다. 이 함수도 역시 만약에 공이 부딪친 벽돌이라면 화면에서 없어져야하기 때문에 10밀리초마다 실행되는 draw()함수에 포함시켜야 한다.
패들을 이동시켜야하기 때문에 키보드의 39, 37 번에 이벤트 리스너를 추가한다.
document.addEventListener("keydown", keyDownHandler, false);
document.addEventListener("keyup", keyUpHandler, false);
각각의 코드는 방향키 양 옆을 나타낸다.
방향키를 눌렀을 때, 누른 후 뗐을 때를 나누어 코드를 작성한다.
function keyDownHandler(e) {
if(e.keyCode == 39) {
rightPressed = true;
}
else if(e.keyCode == 37) {
leftPressed = true;
}
}
function keyUpHandler(e) {
if(e.keyCode == 39) {
rightPressed = false;
}
else if(e.keyCode == 37) {
leftPressed = false;
}
}
rightPressed 와 leftPressed를 boolean 값으로 선언하고, 키가 눌림에 따라 해당 방향으로 이동할 수 있도록 x축과 y축을 이동시킨다.
if(rightPressed && paddleX < canvas.width-paddleWidth) {
paddleX += 7;
}
else if(leftPressed && paddleX > 0) {
paddleX -= 7;
}
모질라 페이지에 따르면 이 이후로는 게임오버를 alert로 구현했지만, score을 저장한 뒤 localStorage에 저장해 게임페이지를 방문할 때 마다 저장되어 있는 게임 데이터를 불러오도록 하는 것이 더 좋을 것 같아 따로 구현해보았다.
📂 localStorage에 데이터를 저장해보자
gameover 일 경우 save() 함수를 실행한다.
function save(score, today) {
var scores = {
"id": today,
"score": score,
"date" : today.toLocaleDateString(),
}
records.push(scores)
localStorage.setItem('blockgame_records',JSON.stringify(records))
}
gameover 직전의 score와 날짜를 받고, id를 설정해 하나의 오브젝트로 만든다.
그 후, 게임을 여러번 진행하고 여러번 gameover 될 때마다 하나의 배열에 해당 오브첵트를 push 한다.
push 하기 전, 로컬 스토리지에서 이미 가지고 있던 데이터 배열을 불러와야하기 때문에
배열 선언문의 가장 마지막에 아래의 코드를 추가했다.
📚 localStorage에서 데이터를 불러오자
if (localStorage.getItem("blockgame_records")) {
records = JSON.parse(localStorage.getItem("blockgame_records"))
//같은 id, 같은 score 인 객체를 하나로 합치기
records = records.reduce(function(acc, current) {
if (acc.findIndex(({ id }) => id === current.id) === -1) {
acc.push(current);
}
return acc;
}, []);
//score가 큰 객체부터 정렬하기
records.sort(function(a,b) {
return b.score - a.score
})
$("#scoreTable").empty;
//정렬된 배열을 하나씩 테이블에 append 하기
records.forEach((record) => {
let score = record.score
let date = record.date
let temp_html = `
<tr>
<td>${score}</td>
<td>${date}</td>
</tr>
`
$("#scoreTable").append(temp_html)
})
} else {
records = []
}
같은 id로 여러 데이터들이 push 되는 것을 막기 위해 id가 같은 데이터끼리는 하나로 묶는 reduce 함수를 작성했다.
이후 score 가 가장 큰 순서대로 정렬해주어 테이블에는 가장 높은 점수부터 올라오게끔 구현했다.
저장된 데이터는 크롬 개발자도구(검사)의 Application 탭에서 key:value pair 로 저장되어있는 것을 확인할 수 있다.
save()함수 이후에는
location.reload() 함수를 실행하면서 페이지라 리로드된다.
리로드 되면서 가장 처음 실행하기로 되어있는 showText() 함수를 다시 실행시킨다.
function showText() {
if (showGameStart == true) {
ctx.fillStyle = 'black'
ctx.fillRect(120, 100, 240, 60)
ctx.font = '20px Arial'
ctx.fillStyle = 'red'
ctx.fillText(startText, 130, 140)
} else {
showGameStart = false
}
}
그러면 아래와 같이 캔버스에 텍스트를 채워넣을 수 있게 된다.
🖱️ 버튼에 이벤트 리스너를 추가해보자
처음 변수를 선언할 때 각각의 버튼들을 자바스크립트 파일에 가져왔다.
var startButton = document.getElementById("startButton")
var cancelButton = document.getElementById("cancelButton")
각각의 버튼에 이벤트 리스너를 추가하고, 해당하는 이벤트의 종류와 시행할 함수를 보내면 끝이다. 게임을 시작하면 캔버스에 그림을 그리는 draw 함수가 10밀리초에 한번씩 실행되고, "Press Start Button Below" 텍스트는 사라지도록 false 로 설정한다.
//게임 시작 전 세팅
showText();
//게임 시작
function gameInit() {
showGameStart = false
setInterval(draw, 10); //10밀리초마다 draw 함수 실행
}
//게임 다시 시작하기
function reload() {
location.reload()
}
위의 코드를 맨 아래 추가하면 정상적으로 작동한다.
4. 마무리
팀 프로젝트 내에서 만든 미니프로젝트이기 때문에 아직 메인페이지 작성이 완료되지 않았다. 완료되는대로 배포 url 올려놓을 예정 ✍🏻😜
'Computer Programming > Javascript' 카테고리의 다른 글
자바스크립트 자료구조 Map과 Set Object의 사용 (0) | 2023.06.12 |
---|---|
자바스크립트의 일급 객체(First-class Function)로서의 함수 (0) | 2023.06.12 |
JavaScript의 발전 및 특징(+ AJAX | XML 용어 정리) (0) | 2023.06.12 |
HTML&CSS로 간단한 로그인 페이지 만들기 (1) | 2023.06.01 |
JQuery - $( document ).ready() (0) | 2023.05.09 |