HTML5 Tetris 게임 만들기 - (5) 다양한 모양 블럭 추가하기

|

다양한 모양 블럭 추가하기

다양한 블럭 모양을 추가하기 위해서는 블럭도 배열 형태로 바꿔줘야 합니다.

기존 코드에 다음과 같은 항목들을 추가했습니다.

const BLOCK_SHAPE = {
    shape1 : [[0, 0], [1, 0], [0, 1], [1, 1]],
};
var curShape = BLOCK_SHAPE.shape1;

블럭의 중점은 (0, 0) 이며, 그 주위로 채워진 블럭의 정보를 JSON 형태 배열로 정의했습니다.

일단 조금 쉽게 접근하기 위해서 블럭은 2x2 사이즈의 정사각형으로 정했고, 기본 틀이 만들어지면 다양한 모양의 블럭을 더 추가하면 됩니다.


function drawFallingBlock() {
    ctx.fillStyle = 'orange';

    for(var i=0; i < curShape.length; i++) {
        var canvasX = (x + curShape[i][0]) * cellWidth;
        var canvasY = (y + curShape[i][1]) * cellHeight;

        ctx.fillRect(canvasX, canvasY, cellWidth, cellHeight);
    }
}

기존의 drawBlock() 메소드 이름을 조금 더 명확하게 하기 위해서 drawFallingBlock()으로 수정했습니다.


function canBlockMoveTo(toX, toY) {
    for(var i=0; i < curShape.length; i++) {
        var shapeX = toX + curShape[i][0];
        var shapeY = toY + curShape[i][1];

        if(shapeX < 0) return false;
        if(shapeX >= COLS) return false;
        if(shapeY < 0) return false;
        if(shapeY >= ROWS) return false;
        
        if(grid[shapeY][shapeX] == 1) return false;
    }

    return true;
}

기존에 아래 방향으로 떨어질 수 있는지를 판단하던 canBlockMoveToDown() 메소드도 좀 더 범용적으로 쓸 수 있게 canBlockMoveTo(toX, toY) 형태로 수정했습니다.

따라서 이를 호출하던 부분들도 전부 수정했습니다.


function addKeyEventListener() {
    addEventListener('keydown', function(event) {
        switch(event.key) {
            case 'ArrowLeft':
                console.log("Left");
                if(canBlockMoveTo(x - 1, y)) {
                    x -= 1;
                }
                break;

            case 'ArrowRight':
                console.log("Right");
                if(canBlockMoveTo(x + 1, y)) {
                    x += 1;
                }
            break;

            case 'ArrowUp':
                console.log("Up");
                break;

            case 'ArrowDown':
                console.log("Down");
                break;
        }
    });
}

function handleBlockDown() {
    var curTime = (new Date()).getTime();
    var diff = curTime - lastBlockDownTime;

    if(diff > BLOCK_DROP_DELAY) {
        if(canBlockMoveTo(x, y + 1)) {
            y += 1;    
        }
        else {
            console.log("block down stopped(x: " + x, "y: " + y + ")");

            for(var i=0; i < curShape.length; i++) {
                var shapeX = x + curShape[i][0];
                var shapeY = y + curShape[i][1];
                grid[shapeY][shapeX] = 1;
            }
            
            initBlock();
        }
        lastBlockDownTime = curTime;
    }
}


블럭 모양 추가

이제 다양한 모양의 블럭들을 추가해봅니다.

const BLOCK_SHAPE = {
    shape1 : [[0, 0], [1, 0], [0, 1], [1, 1]],
    shape2 : [[0, -1], [0, 0], [1, 0], [0, 1]],
    shape3 : [[0, -1], [0, 0], [0, 1], [0, 2]],
    shape4 : [[0, -1], [0, 0], [0, 1], [1, 1]],
    shape5 : [[0, -1], [0, 0], [1, 0], [1, 1]],
};
var curShape = BLOCK_SHAPE.shape1;


그리고 랜덤하게 블럭이 변경되도록 initBlock() 메소드도 수정합니다.

function initBlock() {
    x = COLS / 2;
    y = 0;

    const keys = Object.keys(BLOCK_SHAPE);
    const randomIdx = Math.floor(Math.random() * keys.length);
    curShape = BLOCK_SHAPE[keys[randomIdx]];
}


전체 소스

var canvas;
var ctx;

var x, y;

const BLOCK_DROP_DELAY = 100;
var lastBlockDownTime = 0;

const FRAME = 60;
var drawingTimeDelay = 1000/FRAME;

const ROWS = 24;
const COLS = 12;
var grid;

var cellWidth;
var cellHeight;

const BLOCK_SHAPE = {
    shape1 : [[0, 0], [1, 0], [0, 1], [1, 1]],
    shape2 : [[0, -1], [0, 0], [1, 0], [0, 1]],
    shape3 : [[0, -1], [0, 0], [0, 1], [0, 2]],
    shape4 : [[0, -1], [0, 0], [0, 1], [1, 1]],
    shape5 : [[0, -1], [0, 0], [1, 0], [1, 1]],
};
var curShape = BLOCK_SHAPE.shape1;

//console.log("curShape size: " + curShape.length);

function init() {
    console.log("init()");
    canvas = document.getElementById('canvas');
    ctx = canvas.getContext('2d');

    initGrid(COLS, ROWS);
    initBlock();    

    addKeyEventListener();
}

function initBlock() {
    x = COLS / 2;
    y = 0;

    const keys = Object.keys(BLOCK_SHAPE);
    const randomIdx = Math.floor(Math.random() * keys.length);
    curShape = BLOCK_SHAPE[keys[randomIdx]];
}

function initGrid(cols, rows) {
    console.log("initGrid(" + cols + ", " + rows + ")");

    cellWidth = canvas.width / COLS;
    cellHeight = canvas.height / ROWS;

    grid = new Array(rows);
    for(var i=0; i < rows; i++) {
        grid[i] = new Array(cols);
    }

    for(var r=0; r < rows; r++) {
        for(var c=0; c < cols; c++) {
            grid[r][c] = 0;
        }
    }
}

function start() {
   animate(-1);
}

function draw() {
    drawBackground();
    drawGridLine();
    drawBlocks();
    drawFallingBlock();
}

function drawBackground() {
    ctx.fillStyle = 'black';
    ctx.fillRect(0, 0, canvas.width, canvas.height);
}

function drawGridLine() {
    ctx.strokeStyle = 'gray';

    for(var i=1; i < COLS; i++) {
        ctx.beginPath();
        ctx.moveTo(i * cellWidth, 0);
        ctx.lineTo(i * cellWidth, canvas.height);
        ctx.stroke();
    }

    for(var i=1; i < ROWS; i++) {
        ctx.beginPath();
        ctx.moveTo(0, i * cellHeight);
        ctx.lineTo(canvas.width, i * cellHeight);
        ctx.stroke();
    }
}

function drawBlocks() {
    for(var r=0; r < ROWS; r++) {
        for(var c=0; c < COLS; c++) {
            if(grid[r][c] == 1) {
                fillGrid(c, r);
            }
        }
    }
}

function fillGrid(x, y) {
    ctx.fillStyle = 'green';

    var canvasX = x * cellWidth;
    var canvasY = y * cellHeight;

    ctx.fillRect(canvasX, canvasY, cellWidth, cellHeight);
}

function drawFallingBlock() {
    ctx.fillStyle = 'orange';

    for(var i=0; i < curShape.length; i++) {
        var canvasX = (x + curShape[i][0]) * cellWidth;
        var canvasY = (y + curShape[i][1]) * cellHeight;

        ctx.fillRect(canvasX, canvasY, cellWidth, cellHeight);
    }
}

function addKeyEventListener() {
    addEventListener('keydown', function(event) {
        switch(event.key) {
            case 'ArrowLeft':
                console.log("Left");
                if(canBlockMoveTo(x - 1, y)) {
                    x -= 1;
                }
                break;

            case 'ArrowRight':
                console.log("Right");
                if(canBlockMoveTo(x + 1, y)) {
                    x += 1;
                }
            break;

            case 'ArrowUp':
                console.log("Up");
                break;

            case 'ArrowDown':
                console.log("Down");
                break;
        }
    });
}

function animate(lastTime) {
    var curTime = (new Date()).getTime();
    var diff = curTime - lastTime;
    
    if(diff > drawingTimeDelay) {
        draw();

        lastTime = curTime;
    }

    handleBlockDown();

    requestAnimationFrame(function() {
        animate(lastTime);
    });
}

function handleBlockDown() {
    var curTime = (new Date()).getTime();
    var diff = curTime - lastBlockDownTime;

    if(diff > BLOCK_DROP_DELAY) {
        if(canBlockMoveTo(x, y + 1)) {
            y += 1;    
        }
        else {
            console.log("block down stopped(x: " + x, "y: " + y + ")");

            for(var i=0; i < curShape.length; i++) {
                var shapeX = x + curShape[i][0];
                var shapeY = y + curShape[i][1];
                grid[shapeY][shapeX] = 1;
            }
            
            initBlock();
        }
        lastBlockDownTime = curTime;
    }
}

function canBlockMoveTo(toX, toY) {
    for(var i=0; i < curShape.length; i++) {
        var shapeX = toX + curShape[i][0];
        var shapeY = toY + curShape[i][1];

        if(shapeX < 0) return false;
        if(shapeX >= COLS) return false;
        if(shapeY < 0) return false;
        if(shapeY >= ROWS) return false;
        
        if(grid[shapeY][shapeX] == 1) return false;
    }

    return true;
}

Kotlin Ktor start.io 사용해서 프로젝트 생성하기

|

Ktor start.io

Spring Boot같은 경우는 Spring Initializer라는 사이트를 이용해서 초기 프로젝트를 손쉽게 생성할 수 있습니다.

Ktor도 비슷한 역할을 해주는 사이트가 있습니다.

이런 사이트를 이용해서 프로젝트를 생성할 시 좋은 점은 원하는 라이브러리를 찾기가 쉬우며, 각 라이브러리간 버전 차이로 발생하는 문제를 최소화할 수 있기 때문에 가급적 사용하는 것을 추천합니다.

위 사이트를 통해서 만들어진 build.gradle 예제입니다.


build.gradle

buildscript {
    repositories {
        jcenter()
    }
    
    dependencies {
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
    }
}

apply plugin: 'kotlin'
apply plugin: 'application'

group 'com.snowdeer'
version '0.0.1-SNAPSHOT'
mainClassName = "io.ktor.server.netty.EngineMain"

sourceSets {
    main.kotlin.srcDirs = main.java.srcDirs = ['src']
    test.kotlin.srcDirs = test.java.srcDirs = ['test']
    main.resources.srcDirs = ['resources']
    test.resources.srcDirs = ['testresources']
}

repositories {
    mavenLocal()
    jcenter()
    maven { url 'https://kotlin.bintray.com/ktor' }
}

dependencies {
    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
    implementation "io.ktor:ktor-server-netty:$ktor_version"
    implementation "ch.qos.logback:logback-classic:$logback_version"
    implementation "io.ktor:ktor-server-core:$ktor_version"
    implementation "io.ktor:ktor-server-host-common:$ktor_version"
    implementation "io.ktor:ktor-gson:$ktor_version"
    testImplementation "io.ktor:ktor-server-tests:$ktor_version"
}


resources/application.conf

ktor {
    deployment {
        port = 8080
        port = ${?PORT}
    }
    application {
        modules = [ com.snowdeer.ApplicationKt.module ]
    }
}


resources/logback.xml

<configuration>
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%d{YYYY-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
        </encoder>
    </appender>

    <root level="trace">
        <appender-ref ref="STDOUT"/>
    </root>

    <logger name="org.eclipse.jetty" level="INFO"/>
    <logger name="io.netty" level="INFO"/>
</configuration>


Application.kt

package com.snowdeer

import io.ktor.application.*
import io.ktor.response.*
import io.ktor.request.*
import io.ktor.routing.*
import io.ktor.http.*
import io.ktor.http.content.*
import io.ktor.gson.*
import io.ktor.features.*
import org.slf4j.event.*

fun main(args: Array<String>): Unit = io.ktor.server.netty.EngineMain.main(args)

@Suppress("unused") // Referenced in application.conf
@kotlin.jvm.JvmOverloads
fun Application.module(testing: Boolean = false) {
    install(ContentNegotiation) {
        gson {
        }
    }

    install(CallLogging) {
        level = Level.INFO
        filter { call -> call.request.path().startsWith("/") }
    }

    install(CORS) {
        method(HttpMethod.Options)
        method(HttpMethod.Put)
        method(HttpMethod.Delete)
        method(HttpMethod.Patch)
        header(HttpHeaders.Authorization)
        header("MyCustomHeader")
        allowCredentials = true
        anyHost() // @TODO: Don't do this in production if possible. Try to limit it.
    }

    routing {
        get("/") {
            call.respondText("HELLO WORLD!", contentType = ContentType.Text.Plain)
        }

        // Static feature. Try to access `/static/ktor_logo.svg`
        static("/static") {
            resources("static")
        }

        get("/json/gson") {
            call.respond(mapOf("hello" to "world"))
        }
    }
}

HTML5 Tetris 게임 만들기 - (4) 바닥에 도착한 블럭 쌓이게 하기

|

바닥에 도착한 블럭 쌓이게 하기

js/tetris.js

var canvas;
var ctx;

var x, y;

const BLOCK_DROP_DELAY = 100;
var lastBlockDownTime = 0;

const FRAME = 60;
var drawingTimeDelay = 1000/FRAME;

const ROWS = 24;
const COLS = 12;
var grid;

var cellWidth;
var cellHeight;

function init() {
    console.log("init()");
    canvas = document.getElementById('canvas');
    ctx = canvas.getContext('2d');

    initGrid(COLS, ROWS);
    initBlock();    

    addKeyEventListener();
}

function initBlock() {
    x = COLS / 2;
    y = 0;
}

function initGrid(cols, rows) {
    console.log("initGrid(" + cols + ", " + rows + ")");

    cellWidth = canvas.width / COLS;
    cellHeight = canvas.height / ROWS;

    grid = new Array(rows);
    for(var i=0; i < rows; i++) {
        grid[i] = new Array(cols);
    }

    for(var r=0; r < rows; r++) {
        for(var c=0; c < cols; c++) {
            grid[r][c] = 0;
        }
    }
}

function start() {
   animate(-1);
}

function draw() {
    drawBackground();
    drawGridLine();
    drawBlocks();
    drawBlock();
}

function drawBackground() {
    ctx.fillStyle = 'black';
    ctx.fillRect(0, 0, canvas.width, canvas.height);
}

function drawGridLine() {
    ctx.strokeStyle = 'gray';

    for(var i=1; i < COLS; i++) {
        ctx.beginPath();
        ctx.moveTo(i * cellWidth, 0);
        ctx.lineTo(i * cellWidth, canvas.height);
        ctx.stroke();
    }

    for(var i=1; i < ROWS; i++) {
        ctx.beginPath();
        ctx.moveTo(0, i * cellHeight);
        ctx.lineTo(canvas.width, i * cellHeight);
        ctx.stroke();
    }
}

function drawBlocks() {
    for(var r=0; r < ROWS; r++) {
        for(var c=0; c < COLS; c++) {
            if(grid[r][c] == 1) {
                fillGrid(c, r);
            }
        }
    }
}

function fillGrid(x, y) {
    ctx.fillStyle = 'green';

    var canvasX = x * cellWidth;
    var canvasY = y * cellHeight;

    ctx.fillRect(canvasX, canvasY, cellWidth, cellHeight);
}

function drawBlock() {
    ctx.fillStyle = 'orange';

    var canvasX = x * cellWidth;
    var canvasY = y * cellHeight;

    ctx.fillRect(canvasX, canvasY, cellWidth, cellHeight);
}

function addKeyEventListener() {
    addEventListener('keydown', function(event) {
        switch(event.key) {
            case 'ArrowLeft':
                console.log("Left");
                if(x > 0) {
                    if(grid[y][x - 1] == 0) {
                        x -= 1;    
                    }    
                }
                break;

            case 'ArrowRight':
                console.log("Right");
                if(x < COLS - 1) {
                    if(grid[y][x + 1] == 0) {
                        x += 1;        
                    }
                }
                break;

            case 'ArrowUp':
                console.log("Up");
                break;

            case 'ArrowDown':
                console.log("Down");
                break;
        }
    });
}

function animate(lastTime) {
    var curTime = (new Date()).getTime();
    var diff = curTime - lastTime;
    
    if(diff > drawingTimeDelay) {
        draw();

        lastTime = curTime;
    }

    handleBlockDown();

    requestAnimationFrame(function() {
        animate(lastTime);
    });
}

function handleBlockDown() {
    var curTime = (new Date()).getTime();
    var diff = curTime - lastBlockDownTime;

    if(diff > BLOCK_DROP_DELAY) {
        if(canBlockMoveToDown()) {
            y += 1;    
        }
        else {
            console.log("block down stopped(x: " + x, "y: " + y + ")");
            grid[y][x] = 1;

            initBlock();
        }
        lastBlockDownTime = curTime;
    }
}

function canBlockMoveToDown() {
    if(y >= (ROWS - 1)) return false;
    if(grid[y + 1][x] == 1) return false;

    return true;
}

HTML5 Tetris 게임 만들기 - (3) 화면을 Grid 형태로 구성하고, 격자 좌표계로 블럭 이동, 그리기

|

Grid 적용

js/tetris.js

var canvas;
var ctx;

var x, y;

const BLOCK_DROP_DELAY = 1000;
var lastBlockDownTime = 0;

const FRAME = 60;
var drawingTimeDelay = 1000/FRAME;

const ROWS = 24;
const COLS = 12;
var grid;

var cellWidth;
var cellHeight;

function init() {
    console.log("init()");
    canvas = document.getElementById('canvas');
    ctx = canvas.getContext('2d');

    initGrid(COLS, ROWS);

    x = COLS / 2;
    y = 0;

    addKeyEventListener();
}

function initGrid(cols, rows) {
    console.log("initGrid(" + cols + ", " + rows + ")");

    cellWidth = canvas.width / COLS;
    cellHeight = canvas.height / ROWS;

    grid = new Array(rows);
    for(var i=0; i < rows; i++) {
        grid[i] = new Array(cols);
    }

    for(var r=0; r < rows; r++) {
        for(var c=0; c < cols; c++) {
            grid[r][c] = 0;
        }
    }
}

function start() {
   animate(-1);
}

function draw() {
    drawBackground();
    drawGridLine();
    drawBlock();
}

function drawBackground() {
    ctx.fillStyle = 'black';
    ctx.fillRect(0, 0, canvas.width, canvas.height);
}

function drawGridLine() {
    ctx.strokeStyle = 'gray';

    for(var i=1; i < COLS; i++) {
        ctx.beginPath();
        ctx.moveTo(i * cellWidth, 0);
        ctx.lineTo(i * cellWidth, canvas.height);
        ctx.stroke();
    }

    for(var i=1; i < ROWS; i++) {
        ctx.beginPath();
        ctx.moveTo(0, i * cellHeight);
        ctx.lineTo(canvas.width, i * cellHeight);
        ctx.stroke();
    }
}

function drawBlock() {
    ctx.fillStyle = 'orange';

    var canvasX = x * cellWidth;
    var canvasY = y * cellHeight;

    ctx.fillRect(canvasX, canvasY, cellWidth, cellHeight);
}

function addKeyEventListener() {
    addEventListener('keydown', function(event) {
        switch(event.key) {
            case 'ArrowLeft':
                console.log("Left");
                x -=1;
                break;

            case 'ArrowRight':
                console.log("Right");
                x += 1;
                break;

            case 'ArrowUp':
                console.log("Up");
                y -= 1;
                break;

            case 'ArrowDown':
                console.log("Down");
                y += 1;
                break;
        }
    });
}

function animate(lastTime) {
    var curTime = (new Date()).getTime();
    var diff = curTime - lastTime;
    
    if(diff > drawingTimeDelay) {
        draw();

        lastTime = curTime;
    }

    handleBlockDown();

    requestAnimationFrame(function() {
        animate(lastTime);
    });
}

function handleBlockDown() {
    var curTime = (new Date()).getTime();
    var diff = curTime - lastBlockDownTime;

    if(diff > BLOCK_DROP_DELAY) {
        y += 1;
        lastBlockDownTime = curTime;
    }
 
}

HTML5 Tetris 게임 만들기 - (2) requestAnimationFrame 메소드 이용해서 주기적 화면 갱신하기

|

requestAnimationFrame

requestAnimationFrame과 setInterval 메소드의 차이

setInterval() 메소드를 사용하게 되면 타이머를 이용해서 화면의 갱신을 하게 되는데, 타이밍이 어긋나거나 프레임 손실이 발생해서 애니메이션이 끊어지는 현상이 발생할 수 있습니다. 또한, 시스템 리소스를 많이 사용하기 때문에 성능 저하나 배터리 소모가 크게 발생할 수 있습니다. 시스템 사양이 좋지 않은 경우 애니메이션이 느려지기도 합니다.

requestAnimationFrame() 메소드는 브라우저가 화면을 업데이트하는 경우에만 콜백 함수를 호출합니다. 브라우저가 최소화 되거나 다른 탭이 선택되는 경우 콜백 함수를 호출하지 않습니다. 콜백 호출 주기는 브라우저가 관리하기 때문에 setInterval() 메소드처럼 계속 호출되지 않습니다.

setInterval() 메소드와 requestAnimationFrame() 메소드는 사용법이 같기 때문에 쉽게 변환을 할 수 있습니다.


js/tetris.js

var canvas;
var ctx;

var x, y;

function init() {
    canvas = document.getElementById('canvas');
    ctx = canvas.getContext('2d');

    x = canvas.width / 2;
    y = 20;

    addKeyEventListener();
}

function start() {
   animate(-1);
}

function draw() {
    drawBackground();
    drawBlock();
}

function drawBackground() {
    ctx.fillStyle = 'black';
    ctx.fillRect(0, 0, canvas.width, canvas.height);
}


function drawBlock() {
    ctx.fillStyle = 'orange';
    ctx.fillRect(x-10, y-10, 20, 20);
}

function addKeyEventListener() {
    addEventListener('keydown', function(event) {
        switch(event.key) {
            case 'ArrowLeft':
                console.log("Left");
                x -=20;
                break;

            case 'ArrowRight':
                console.log("Right");
                x += 20;
                break;

            case 'ArrowUp':
                console.log("Up");
                y -= 20;
                break;

            case 'ArrowDown':
                console.log("Down");
                y += 20;
                break;
        }
    });
}

function animate(lastTime) {
    var time = (new Date()).getTime();
    console.log("time: " + time);

    draw();

    requestAnimationFrame(function() {
        animate(time);
    });
}

requestAnimationFrame() 메소드를 사용하게 되면, 그 전에 Key Event에서 화면을 갱신하던 부분을 제거해줘야 합니다. 그래서 addKeyEventListener() 메소드 내에서 draw()를 호출하는 부분을 제거했으며, 별도로 animate() 메소드를 추가했습니다. 변수로 들어오는 lastTime는 나중에 시간과 속도를 계산해서 이동한 거리를 계산하기 위한 항목입니다.

위 코드를 실행하게 되면 animate() 메소드가 계속해서 반복되면서 화면을 갱신하게 됩니다.

만약 시스템 부하나 배터리 효율 등을 고려해서 1초마다 렌더링하고 싶으먄 다음과 같이 변경할 수 있습니다.

function animate(lastTime) {
    var curTime = (new Date()).getTime();
    var diff = curTime - lastTime;
    
    if(diff > 1000) {
        console.log("render - time diff: " + diff);
        draw();

        lastTime = curTime;
    }

    requestAnimationFrame(function() {
        animate(lastTime);
    });
}


일반적으로 30프레임 또는 60프레임 렌더링을 많이 하기 때문에 다음처럼 코드를 작성할 수 있습니다.

var canvas;
var ctx;

var x, y;

const FRAME = 60;
var drawingTimeDelay = 1000/FRAME;

function init() {
    canvas = document.getElementById('canvas');
    ctx = canvas.getContext('2d');

    x = canvas.width / 2;
    y = 20;

    addKeyEventListener();
}

function start() {
   animate(-1);
}

function draw() {
    drawBackground();
    drawBlock();
}

function drawBackground() {
    ctx.fillStyle = 'black';
    ctx.fillRect(0, 0, canvas.width, canvas.height);
}


function drawBlock() {
    ctx.fillStyle = 'orange';
    ctx.fillRect(x-10, y-10, 20, 20);
}

function addKeyEventListener() {
    addEventListener('keydown', function(event) {
        switch(event.key) {
            case 'ArrowLeft':
                console.log("Left");
                x -=20;
                break;

            case 'ArrowRight':
                console.log("Right");
                x += 20;
                break;

            case 'ArrowUp':
                console.log("Up");
                y -= 20;
                break;

            case 'ArrowDown':
                console.log("Down");
                y += 20;
                break;
        }
    });
}

function animate(lastTime) {
    var curTime = (new Date()).getTime();
    var diff = curTime - lastTime;
    
    if(diff > drawingTimeDelay) {
        console.log("render - time diff: " + diff);
        draw();

        lastTime = curTime;
    }

    requestAnimationFrame(function() {
        animate(lastTime);
    });
}


1초마다 1칸씩 블럭이 떨어지도록 하기

const BLOCK_DROP_DELAY = 1000;
var lastBlockDownTime = 0;

function animate(lastTime) {
    var curTime = (new Date()).getTime();
    var diff = curTime - lastTime;
    
    if(diff > drawingTimeDelay) {
        console.log("render - time diff: " + diff);
        draw();

        lastTime = curTime;
    }

    handleBlockDown();

    requestAnimationFrame(function() {
        animate(lastTime);
    });
}

function handleBlockDown() {
    var curTime = (new Date()).getTime();
    var diff = curTime - lastBlockDownTime;

    if(diff > BLOCK_DROP_DELAY) {
        y += 20;
        lastBlockDownTime = curTime;
    }