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;
    }

HTML5 Tetris 게임 만들기 - (1) 뼈대 만들기

|

Tetris 게임 뼈대 구현

테트리스 게임을 만들기 전에 먼저 HTML5 canvas 위에 작은 박스를 그려보고, Keyboard 화살표 입력을 이용해서 이동을 할 수 있는 코드를 작성해보도록 하겠습니다.


tetris.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>;Tetris</title>

    <script src="/js/tetris.js"></script>
</head>
<body>
    <canvas id="canvas" width="240" height="480">
        이 브라우저는 HTML5 Canvas를 지원하지 않습니다.
    </canvas>

    <br>

    <script>
        window.onload = function() {
            init();

            draw();
        }
    </script>
</body>
</html>


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;
}

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;

                draw();
                break;

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

                draw();
                break;

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

                draw();
                break;

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

                draw();
                break;
        }
    });
}