Web Dev Simpified : If You Want To Be An Advanced Game Developer Build This Project
본 포스팅은 위 유튜브 튜토리얼 영상을 보고 쓴 클론코딩(깃허브)입니다.
가독성을 위해서 기타 일반 함수로 작성된 부분도 클래스 안에 넣고, event 부분도 따로 파일로 빼서 클래스로 작성하였습니다.
점수 표시도 추가하였습니다.
새로 배웠던 개념
- * $elem.style.setProperty('--var', value);
- * get, set
- * https://ko.javascript.info/property-accessors
- * private field(#)
- * https://ko.javascript.info/private-protected-properties-methods
- * power 제곱근 : Math.log2(number)
- * $elem.addEventListener('event',func,{once:true});
애니메이션은 전부 CSS의 사용자지정속성(--x 이런거) 과 transition으로 처리하고 있음. js로 사용자 지정 속성을 동적으로 수정하고 반영함으로써 움직이는 효과를 구현하고 있음.
아직 제대로 이해 못한거 : moveTiles 부분의 cellsByColumn과 cellsByRow...
이 개념이..머릿속에 잘 안 잡힘 reduce로 하는게
HTML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>2048 mine</title>
<link rel="stylesheet" type="text/css" href="./css/main.css"/>
<script src="./js/main.js" type="module"></script>
</head>
<body>
<h1>2048</h1>
<h2 id="score">점수</h2>
<section id="game-board">
</section><!-- game-board -->
</body>
</html>
|
cs |
CSS
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
|
@charset "utf-8";
:root{
--bg:#EEF1F7;
--font:Arial;
--fSize:7.5vmin;
--bdr:min(12px,2vmin);
--grid-bg:rgb(218, 222, 233);
--cell-bg:rgb(168, 174, 191);
}
*{margin:0;padding:0;box-sizing:border-box;}
html,body{
display:flex; flex-flow:column nowrap;
justify-content:center; align-items:center;
gap:3vmin;
width:100%;min-height:100vh;
background:var(--bg);
font-family:var(--font); font-size:var(--fSize);
}
h1{font-size:3vmin;}
#score{color:hsl(200,25%,50%);}
#game-board{
/* --grid-size:4;
--cell-size:15vmin;
--cell-gap:1.5vmin; */
display:grid; overflow:hidden;
grid-template-columns:repeat(var(--grid-size),var(--cell-size));
grid-template-rows:repeat(var(--grid-size),var(--cell-size));
grid-gap:var(--cell-gap);
position:relative;
padding:var(--cell-gap);
background-color:var(--grid-bg);
border-radius:var(--bdr);
}
.cell{
background:var(--cell-bg);
border-radius:var(--bdr);
}
.tile{
/* --x:0; --y:0; */
/* --cell-bg-lightness:20%; */
/* --cell-text-lightness:80%; */
position:absolute;
width:var(--cell-size); aspect-ratio:1/1;
border-radius:var(--bdr);
text-align:center;line-height:var(--cell-size); font-weight:bold;
left:calc(
var(--x) *
(var(--cell-size) + var(--cell-gap)) +
var(--cell-gap));
top:calc(
var(--y) *
(var(--cell-size) + var(--cell-gap)) +
var(--cell-gap));
background:hsl(200,50%,var(--cell-bg-lightness));
color:hsl(200,25%,var(--cell-text-lightness));
animation:show 200ms ease-in-out;
transition:100ms ease-in-out;
box-shadow:2px 2px 0 rgba(0,0,0,.2);
}
@keyframes show {
from{opacity:0; transform:scale(0);}
to{opacity:1;transform:scale(1)}}
|
cs |
main.js
1
2
3
4
5
6
7
8
9
10
11
12
|
import Grid from "./Grid.js";
import Play from "./Play.js";
import Tile from "./Tile.js";
const $board = document.getElementById('game-board');
const GRID = new Grid($board);
GRID.randomEmptyCell().tile = new Tile($board);
GRID.randomEmptyCell().tile = new Tile($board);
const PLAY = new Play($board, GRID);
PLAY.setupInput();
|
cs |
Grid.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
|
import Cell from "./Cell.js";
export default class Grid{
#CELLS
constructor($board){
this.$board = $board;
this.SETTING = Object.freeze({
gridSize : 4,
cellSize : 15,
cellGap : 1.5
});
this.init();
}//constructor
/* 💥01. 그리드 그려주고 Cell 클래스 개별 생성 관련 */
/* init all */
init(){
const CELLS = this.draw_grid();
this.#CELLS = CELLS.map(($cell,idx)=>{
const X = idx % this.SETTING.gridSize;
const Y = Math.floor(idx / this.SETTING.gridSize);
return new Cell($cell,X,Y);
});
}//init
/* draw grid */
draw_grid(){
const {gridSize,cellSize,cellGap} = this.SETTING;
const CELLS = [];
this.$board.style.setProperty('--grid-size',gridSize);
this.$board.style.setProperty('--cell-size',`${cellSize}vmin`);
this.$board.style.setProperty('--cell-gap',`${cellGap}vmin`);
const $frag = document.createDocumentFragment();
for(let i=0; i < gridSize ** 2; i++){
const $cell = document.createElement('DIV');
$cell.classList.add('cell');
$frag.appendChild($cell);
CELLS.push($cell);
}//for
this.$board.appendChild($frag);
return CELLS;
}//draw_grid
/* get a random EMPTY tile */
randomEmptyCell(){
const rIdx = Math.floor(Math.random() * this.#EmptyCells.length);
const emptyCell= this.#EmptyCells[rIdx];
return emptyCell;
}//randomEmptyCell
/* GETTER, SETTER */
get #EmptyCells(){
return this.#CELLS.filter($cell => $cell.tile == null);
}//#EmptyCells
/* 💥02. 키보드 이벤트 핸들러 관련 */
get cellsByColumn(){
return this.#CELLS.reduce((cellGrid,cell)=>{
console.log(cellGrid,cell);
cellGrid[cell.x] = cellGrid[cell.x] || [];
cellGrid[cell.x][cell.y] = cell;
return cellGrid;
},[]);
}//cellsByColumn
get cellsByRow(){
return this.#CELLS.reduce((cellGrid,cell)=>{
cellGrid[cell.y] = cellGrid[cell.y] || [];
cellGrid[cell.y][cell.x] = cell;
return cellGrid;
},[]);
}//cellsByRow
get cells(){return this.#CELLS;}
}//class-Grid
|
cs |
Cell.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
|
export default class Cell{
#CELL
#X
#Y
#TILE
#MERGETILE
constructor($cell,X,Y){
this.#CELL = $cell;
this.#X = X;
this.#Y = Y;
}//constructor
/* 💥 01.Cell의 기본 세팅 - Tile 값에 영향 */
/* [GETTER, SETTER] */
get x(){return this.#X}
get y(){return this.#Y}
get tile(){
// console.log('class:',this.#TILE);
return this.#TILE;}
set tile(newTile){
this.#TILE = newTile;
if(newTile == null){return;}
this.#TILE.x = this.#X;
this.#TILE.y = this.#Y;
}//tile
/* 💥02. 키보드 이벤트 관련 */
get mergeTile(){return this.#MERGETILE;}
set mergeTile(tile){
this.#MERGETILE = tile;
if(tile == null){return;}
this.#MERGETILE.x = this.#X;
this.#MERGETILE.y = this.#Y;
}
canAccept(tile){
return (
this.tile == null ||
(this.mergeTile == null && this.tile.value === tile.value)
);
}//canAccept
mergeTiles(PLAY){
if(this.tile == null || this.mergeTile == null){return;}
this.tile.value = this.tile.value + this.mergeTile.value;
PLAY.score += this.tile.value;
this.mergeTile.remove();
this.mergeTile = null;
}//mergeTiles
}//class Cell
|
cs |
Tile.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
|
const bgColor = "--cell-bg-lightness";
const txtColor = "--cell-text-lightness";
export default class Tile{
#TILE
#x
#y
#VALUE
constructor($board, vnum = Math.random() > 0.5 ? 2 : 4){
this.$board = $board;
this.#TILE = this.addTileElement();
this.value = vnum; //set에서 실행됨
}//constructor
/* 💥 01. 랜덤한 타일 2개 생성 관련 */
addTileElement(){
const $tile = document.createElement('DIV');
$tile.classList.add('tile');
this.$board.appendChild($tile);
return $tile;
}//addTileElement
/* [GETTER, SETTER] */
set x(xnum){
this.#x = xnum;
this.#TILE.style.setProperty('--x',xnum);
}//set-x
set y(ynum){
this.#x = ynum;
this.#TILE.style.setProperty('--y',ynum);
}//set-x
get value(){return this.#VALUE;}
set value(vnum){
//숫자 설정
this.#VALUE = vnum;
this.#TILE.textContent = vnum;
//배경색 및 글씨색
const power = Math.log2(vnum);
const bgLight = 100 - (power * 9);
this.#TILE.style.setProperty(bgColor,`${bgLight}%`);
this.#TILE.style.setProperty(txtColor,`${bgLight <= 50 ? 90 : 10}%`);
}//set-value
/* 💥 02. 이벤트 관련 */
remove(){this.#TILE.remove();}
waitForTransition(animation = false){
return new Promise(res=>{
const evt = animation ? "animationend" : "transitionend";
this.#TILE.addEventListener(evt,res,{once:true});
});
}//waitForTransition
}//class-Tile
|
cs |
Play.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
|
import Tile from "./Tile.js";
export default class Play{
#SCORE
constructor($board,GRID){
this.$board = $board;
this.GRID = GRID;
this.KEYS = Object.freeze({
up : "ArrowUp",
down : "ArrowDown",
left : "ArrowLeft",
right : "ArrowRight",
});
this.$score = document.getElementById('score');
this.score = 0;
}//constructor
setupInput(){
window.addEventListener('keydown', this.handler, {once:true});
}//setupInput
handler = async(e)=>{
const {up,down,left,right} = this.KEYS;
//Move Tiles
switch(e.key){
case up :
if(!this.canMoveUp()){this.setupInput(); return;}
await this.moveUp();
break;
case down :
if(!this.canMoveDown()){this.setupInput(); return;}
await this.moveDown();
break;
case left :
if(!this.canMoveLeft()){this.setupInput(); return;}
await this.moveLeft();
break;
case right :
if(!this.canMoveRight()){this.setupInput(); return;}
await this.moveRight();
break;
default:
this.setupInput();
return;
}//switch
//Merge
this.GRID.cells.forEach($cell => $cell.mergeTiles(this));
//Make New Random Tile
const newTile = new Tile(this.$board);
this.GRID.randomEmptyCell().tile = newTile;
//Game Over?
if(!this.canMoveUp() && !this.canMoveDown() && !this.canMoveLeft() && !this.canMoveRight()){
newTile.waitForTransition(true).then(()=>{
console.log('패배');
alert('YOU LOST');
});
return;
}//if
//reset-setupInput
this.setupInput();
}//handler
/* [움직일 수 있는가?] */
canMove(cells){
return cells.some(colOrRow =>{
return colOrRow.some(($cell,idx)=>{
if(idx === 0) return false;
if($cell.tile == null) return false;
const $moveToCell = colOrRow[idx - 1];
return $moveToCell.canAccept($cell.tile);
});//some- colOrRow
});//some-cells
}//canMove
canMoveUp(){return this.canMove(this.GRID.cellsByColumn);}
canMoveDown(){return this.canMove(this.GRID.cellsByColumn.map(col=>[...col].reverse()));}
canMoveLeft(){return this.canMove(this.GRID.cellsByRow);}
canMoveRight(){return this.canMove(this.GRID.cellsByRow.map(row=>[...row].reverse()));}
/* [움직이기] */
slideTiles(cells){
return Promise.all(
cells.flatMap(group => {
const promises = [];
for(let i=1; i<group.length; i++){
const $cell= group[i];
if($cell.tile == null) continue;
let $lastValidCell;
for(let j=i-1; j>=0; j--){
const moveToCell = group[j];
if(!moveToCell.canAccept($cell.tile)) break;
$lastValidCell = moveToCell;
}//for-j
if($lastValidCell != null){
promises.push($cell.tile.waitForTransition());
if($lastValidCell.tile != null){
$lastValidCell.mergeTile = $cell.tile;
}else{
$lastValidCell.tile = $cell.tile;
}//if-else
$cell.tile = null;
}//if
}//for-i
return promises;
})//flatMap
);//return-Promise
}//slideTiles
moveUp(){return this.slideTiles(this.GRID.cellsByColumn);}
moveDown(){
return this.slideTiles(this.GRID.cellsByColumn.map(col=>[...col].reverse()));}
moveLeft(){
return this.slideTiles(this.GRID.cellsByRow);}
moveRight(){
return this.slideTiles(this.GRID.cellsByRow.map(row=>[...row].reverse()));}
/* 💥03. 점수 */
get score(){return this.#SCORE;}
set score(num){
this.#SCORE = num;
this.$score.textContent = this.#SCORE;
}//set-scroe
}//class-Play
|
cs |
'CSS&JS > 👀Study and Copy' 카테고리의 다른 글
Online Tutorials의 Circular ProgressBar (0) | 2022.07.01 |
---|---|
[vanilla JS] 자바스크립트로 테트리스 만들기 (1) (0) | 2022.06.10 |
getter,setter (0) | 2022.06.03 |
[JS]바닐라 자바스크립트로 지뢰찾기 만들기 (제로초 렛츠기릿 자바스크립트 강의) (0) | 2022.02.07 |
[JS]class에서 this를 할당하는 방법 (0) | 2022.01.26 |