brunch

You can make anything
by writing

C.S.Lewis

by 앵버박사 May 14. 2016

처음 만드는 온라인 게임 05-02

클라이언트 개발



이제 본격적인 클라이언트 코딩을 시작해볼 텐데, 클라이언트 코드는 아래와 같이 3개의 파일에 나눠져 있습니다.



1. index.html
    - 게임의 진입점이고, UI 컴포넌트를 가짐

2. simple_online_game.js
    - 클라이언트 개발에 필요한 함수, 객체 및 로직을 포함

3. simple_utils.js
    - simple_online_game.js 를 돕기 위한 유틸리티로 구성



index.html은 굉장히 단순합니다. 필요한 자바스크립트 파일을 포함하고, 캔버스와 종료 버튼을 가지고 있습니다.




<!DOCTYPE html>

<html>

<head>

    <script type='text/javascript'src='static/js/simple_utils.js'></script>

    <script type='text/javascript'src='static/js/simple_online_game.js'></script>

</head>

<body>

    <canvas id='canvas'width='500'height='400'style='border: 1px solid red;'>Sorry, not supported for the current browser.</canvas>

    <br/>

    <input id='exit'type='button'value='Exit'style='border: 1px solid gray; width: 50px; height: 25px; background-color:# white; cursor: pointer;'/>

</body>

</html>




브라우저에서 캔버스를 지원하지 않는 경우 canvas 태그 사이에 있는 글귀가 보이게 됩니다. 간단하죠? 바로 자바스크립트로 넘어가겠습니다. simple_online_game.js 의 경우 큰 구조는 다음과 같습니다.




(function() {

'use strict';


    // 1) 필요한 전역 변수 선언

var

    CHARACTER_SIZE =96,

    CORRECT_LVALUE =11 *3,

    CORRECT_TVALUE =8*3,


    initialize, painters, requestId, sog, Game, Server, Sprite, Painter, Actors, PainterFactory,


    moveUp = newImage( CHARACTER_SIZE, CHARACTER_SIZE ),

    moveDown = newImage( CHARACTER_SIZE, CHARACTER_SIZE ),

    moveLeft = newImage( CHARACTER_SIZE, CHARACTER_SIZE ),

    moveRight = newImage( CHARACTER_SIZE, CHARACTER_SIZE ),

    attackUp = new Image( CHARACTER_SIZE, CHARACTER_SIZE ),

    attackDown = newImage( CHARACTER_SIZE, CHARACTER_SIZE ),

    attackLeft = newImage( CHARACTER_SIZE, CHARACTER_SIZE ),

    attackRight = newImage( CHARACTER_SIZE, CHARACTER_SIZE )

    ;


 // 2) 내부적으로 필요한 함수 정의 부분

    ...


// 3) Game, Server, Sprite, Painter, Actors, PainterFactory 클래스 정의

    ...


// 4) 초기화 및 게임 시작점

    initialize =function() {

        ...

    };


document.addEventListener( 'DOMContentLoaded', initialize, false );

})();




우선 다른 라이브러리나 환경에 영향을 주지 않기 위해 즉시 실행 함수로 감싸고, 좀 더 엄격한 코딩을 위해 strict mode를 사용하였습니다.


소스코드는 크게 4가지 부분으로 나뉘어 있습니다. 처음으로 필요한 변수 및 리소스를 선언하는 부분인데 캐릭터 사이즈와 관련된 상수, 캐릭터 이미지, 전역으로 사용할 변수 선언으로 구성돼있습니다. 두 번째는 여러 로직은 진행하는데 필요한 private 한 함수를 정의하는 공간이며, 세 번째는 필요한 클래스를 정의하고 마지막으로 코드의 시작점이 되는 initialize 함수를 정의합니다.


사용자가 페이지를 호출했을 때 initialize 함수가 어떻게 시작점이 될 수 있을까요? 맨 밑에 코드를 보시면 DOMContentLoaded 이벤트를 걸어 콜백으로 initialize 함수를 넣고 있습니다. DOMContentLoaded 이벤트는 DOM 로딩이 완료되는 시점에 발생합니다. 유명한 jQuery의 ready와 같은 기능이며 본문 코드에서 DOM을 조작해야 하므로 반드시 DOM 로딩이 완료된 시점에 우리 코드가 호출되어야 합니다.


그럼 이제 initialize 함수 안을 살펴보겠습니다.




initialize = function() {

var

    exit = document.getElementById( 'exit' ),

    context =document.getElementById( 'canvas' ).getContext( '2d' ),

    keyInfo = {

 '38' : { speedV : 0, speedH : -2, direction : 'UP', status : 'MOVE' },

'40' : { speedV : 0, speedH : 2, direction : 'DOWN', status : 'MOVE' },

'37' : { speedV : -2, speedH : 0, direction : 'LEFT', status : 'MOVE' },

'39' : { speedV : 2, speedH : 0, direction : 'RIGHT', status : 'MOVE' },

 '32' : { speedV : 0, speedH : 0, direction : 'DOWN', status : 'ATTACK' }

    };


    exit.addEventListener( 'click', function( $event ) {

        sog.server.exit();

    } );


document.addEventListener( 'keydown', function( $event ) {

if ( $event.keyCode in keyInfo ) {

var data = $util.clone( keyInfo[ $event.keyCode ] );

if ( data.status ==='ATTACK' ) {

                data.direction = sog.sprite.p1.data.direction;

            }

if ( !collision( data ) ) {

                sog.server.update( data );

            }

        }

    }, false );


document.addEventListener( 'keyup', function( $event ) {

        sog.server.update( { speedV : 0, speedH : 0, direction : sog.sprite.p1.data.direction, status : 'STAY' } );

    }, false );


    moveUp.src = 'static/img/moveUp.png';

    moveDown.src ='static/img/moveDown.png';

    moveLeft.src ='static/img/moveLeft.png';

    moveRight.src = 'static/img/moveRight.png';

    attackUp.src ='static/img/attackUp.png';

    attackDown.src = 'static/img/attackDown.png';

    attackLeft.src = 'static/img/attackLeft.png';

    attackRight.src ='static/img/attackRight.png';


    $util.syncOnLoad( [ moveUp, moveDown, moveLeft, moveRight, attackUp, attackDown, attackLeft, attackRight ], function() {

var server =new Server( { roomNo : 'ROOM1' } );

        sog =new Game( { context : context, server : server, sprite : new Sprite( PainterFactory.create( PainterFactory.DOWN ) ) } );


document.removeEventListener( 'DOMContentLoaded', initialize, false );

        sog.start();

    } );

};




먼저 우리 게임에서 쓰는 단축키 상하좌우 이동 및 공격 총 5가지 키를 keyInfo 변수에 정의합니다. speedV는 캐릭터의 left값이며, speedH는 top의 값입니다. 추가적으로 캐릭터의 방향과 상태를 지정하고 있습니다.


그다음으로는 이벤트들을 정의하고 있는데, keydown 이벤트에서는 정의된 키가 눌렸을 경우 해당 키 정보 객체를 복사하고 공격이면 기존 캐릭터의 방향을 유지하기 위해 방향 데이터를 조작하고, 충돌처리( collision )를 한 후 결과가 false( 충돌 X )이면 서버에 해당 키 정보를 업데이트합니다.


keyup 이벤트에서는 키에서 손을 띄었으므로 현재 방향 그대로 대기상태로 업데이트해줍니다.


이벤트 정의가 끝나고 필요한 이미지 리소스 들을 로딩합니다. 이미지 리소스 들이 전부 로딩되면 Server와 Game 객체를 만들고 게임을 시작시키게 되는데, 여기서 주의할 점은 이미지가 전부 로딩되고 나서 게임을 시작해야 된다는 점입니다. 그렇지 않으면 게임은 시작됐는데 캐릭터가 안 보인다던지 배경이 안 보일 수 도 있을 것입니다. 소스코드는 xxx.src = ''; 가 sog.start(); 보다 위에 있지만 이미지가 로딩되는 것은 비동기 처리되기 때문에 반드시 이미지 로딩보다 게임 시작이 나중에 된다는 보장이 없습니다. 따라서 $util.syncOnLoad 함수로 모든 이미지가 다 로딩되고 난 후 등록된 callback 함수를 호출하여 게임을 시작하게 작성하였습니다.


참고로 이 게임 작성에 필요한 유틸리티는 index.html 에 등록한 simple_utils.js 내에 들어있으며, $util 객체를 통해 사용할 수 있습니다. 소스코드는 간단한 편이니 한 번 살펴보시기 바랍니다. [ Github 바로가기 ]


initialize 함수에서 Game 인스턴스를 만들고 옵션으로 Canvas 컨텍스트와 Server 그리고 Sprite 객체를 넘겨주고 있습니다. 이런 각 객체들이 상호작용하여 게임이 만들어지는데, 전체적인 구조를 보면 다음과 같습니다.



클래스 구조 다이어그램



Game은 가장 상위에서 모든 객체들을 가지고 컨트롤합니다. Game은 Server를 가지는데, Server는 우리가 만든 파이썬 웹 소켓 서버와 직접 연결하여 데이터를 주고받는 역할을 해줍니다. Painter는 우리가 이전 글에서 만든 스프라이트 이미지를 화면에 그려주고, Actor는 스프라이트 이미지에 자연스러운 움직임을 부여합니다. Sprite는 한 개의 Painter와 여러 개의 Actor를 가지고, 하나의 캐릭터 스프라이트가 됩니다.


먼저 Sprite를 한번 보겠습니다.




Sprite = function( $painter, $actors ) {

this.painter = $painter;

this.actors = $actors || {};

this.data =null;

this.left =0;

this.top =0;

};


Sprite.prototype.paint =function( $context ) {

this.painter.paint( this, $context );

};


Sprite.prototype.update = function( $data, $time ) {

this.data = $data;


for ( var name in this.actors ) {

this.actors[ name ].execute( this, $data, $time );

    }

};


Sprite.prototype.advance = function() {

this.painter.advance();

};




Sprite는 쉽습니다. 하나의 Painter와 여러 Actor를 가지고 이들을 실행해주는 메서드들을 정의하고 있습니다. 다음은 Painter를 볼까요.




Painter = function( $image, $active ) {

this.image = $image;

this.active = $active;

this.index = 0;

};


Painter.prototype.paint = function( $sprite, $context ) {

var

    i, r, g, b, imageData,

    active =this.active[ this.index ];


    $context.drawImage(

this.image,

        active.left, active.top, active.width, active.height,

        $sprite.left, $sprite.top, this.image.width, this.image.height

    );


    imageData = $context.getImageData(

        $sprite.left + CORRECT_LVALUE,

        $sprite.top + CORRECT_TVALUE,

this.image.width - CORRECT_LVALUE *2,

this.image.height - CORRECT_LVALUE - CORRECT_TVALUE

    );


  if ( $sprite === sog.sprite.p2 ) {

for ( i =0; i < imageData.data.length; i +=4 ) {

            r = imageData.data[ i ],

            g = imageData.data[ i +1 ],

            b = imageData.data[ i +2 ];


if ( r === 202 && g ===16&& b ===16 ) {

                imageData.data[ i ] = b;

                imageData.data[ i +2 ] = r;

            }

        }


        $context.putImageData( imageData, $sprite.left + CORRECT_LVALUE, $sprite.top + CORRECT_TVALUE );

    }


var target = $sprite === sog.sprite.p1 ? sog.sprite.p2 : sog.sprite.p1;


if ( target && target.data.attackStatus ==='success' ) {

for ( i = 3; i < imageData.data.length; i +=4 ) {

            imageData.data[ i ] = imageData.data[ i ] /2;

        }


        $context.putImageData( imageData, $sprite.left + CORRECT_LVALUE, $sprite.top + CORRECT_TVALUE );

    }

};


Painter.prototype.advance = function() {

this.index++;


 if ( this.index >this.active.length -1 ) {

this.index =0;

    }

};




Painter의 생성자에서는 앞에서 new Image로 만들었던 스프라이트 이미지를 인자로 받습니다. 그리고 스프라이트 이미지는 여러 이미지가 하나의 파일에 모여있기 때문에 각 이미지를 분리할 수 있는 위치정보가 필요한데, 바로 그 위치정보( active 변수 )를 받습니다.


메서드로는 paint와 advance를 가집니다. 우선 advance는 스프라이트 이미지의 다음 이미지 위치정보를 가리키도록 index를 증가시키는 기능입니다.


그리고 paint는 이름 그대로 현재 가리키는 위치정보의 이미지를 그려주는 역할을 합니다. 소스코드를 보면 현재 index의 위치정보를 가지고 와 drawImage 함수로 이미지를 캔버스에 그립니다. drawImage는 캔버스 기능으로 각 인자의 의미는 [이미지 객체, 이미지 내 left 위치, 이미지 내 top 위치, left로부터 너비, top으로부터 높이, 현재 캐릭터의 left 위치, 현재 캐릭터의 top 위치, 캐릭터 너비, 캐릭터 높이] 입니다.


그리고 캔버스 컨텍스트의 getImageData 함수를 이용하여 현재 캐릭터 스프라이트의 색상 정보를 취득하고 있습니다. CORRECT_ 로 시작하는 상수는 이미지가 캐릭터를 중심으로 상하좌우로 투명 배경이 꽤 크게 퍼져있기 때문에 정확히 캐릭터 사이즈의 색상 정보를 얻어오기 위해 위치정보를 보정하는 데 사용합니다.



실제 테두리와 캐릭터 테두리 사이의 간극



계속해서 플레이어 2의 경우 빨간 색상의 R과 B값을 서로 바꿔 파란 색상으로 변경해줍니다. 그 이후 캐릭터 공격 성공 여부를 체크하여 공격이 성공한 경우 공격을 당한 캐릭터의 색상을 반투명으로 변경하여 타격감을 주었습니다.


Painter의 경우 각 플레이어별로 당시 index 값이 다를 것이므로 하나의 인스턴스로 공유해서 쓸 순 없습니다. 따라서 인스턴스를 조금 더 편하게 생성하기 위해 PainterFactory를 별도로 두고, 이미지와 위치정보를 미리 세팅하여 상태 값에 따라 알맞은 Painter의 인스턴스를 반환해주도록 만들었습니다.




PainterFactory = {

    UP : 'UP',

    DOWN : 'DOWN',

    LEFT : 'LEFT',

    RIGHT : 'RIGHT',

    ATTACK_UP : 'ATTACK_UP',

    ATTACK_DOWN : 'ATTACK_DOWN',

    ATTACK_LEFT : 'ATTACK_LEFT',

    ATTACK_RIGHT : 'ATTACK_RIGHT',

    create : function( $status ) {

switch ( $status ) {

case this.UP:

return new Painter( moveUp, createActive( 1024, 3 ) );

break;

case this.DOWN:

 return new Painter( moveDown, createActive( 1024, 3 ) );

break;

case this.LEFT:

return new Painter( moveLeft, createActive( 1024, 8 ) );

break;

case this.RIGHT:

return new Painter( moveRight, createActive( 1024, 8 ) );

break;

case this.ATTACK_UP:

return new Painter( attackUp, createActive( 512, 6 ) );

break;

case this.ATTACK_DOWN:

 return new Painter( attackDown, createActive( 512, 6 ) );

break;

case this.ATTACK_LEFT:

return new Painter( attackLeft, createActive( 512, 6 ) );

break;

case this.ATTACK_RIGHT:

return new Painter( attackRight, createActive( 512, 6 ) );

break;

        }

    }

};


function createActive( $interval, $length ) {

var active = [], i;


for ( i =0; i < $length; i++ ) {

        active.push( { left : i * $interval, top : 0, width : $interval, height : $interval } );

    }


return active;

}




다음으로 Server부터 시작해 Game까지 보겠습니다.




Server = function( $params ) {

this.userId =null;

this.roomNo = $params.roomNo;

this.socket =null;

this.command = { REGISTER :'register', UPDATE :'update', DATA :'data' };

 };


Server.prototype.connect = function( $registerCB, $dataCB ) {

var self = this;

this.socket = new WebSocket('ws://'+ ( window.location.hostname ||'localhost' ) +':8080');

this.socket.addEventListener( 'open', function() { self.register(); } );

this.socket.addEventListener ( 'message', function( $event ) {

var result =JSON.parse( $event.data ),

                data = result.data;


 if ( result.code === 0 ) {

if ( result.status === self.command.REGISTER ) {

                $registerCB.apply( sog, [ data ] );

            } else if ( result.status === self.command.DATA ) {

                $dataCB.apply( sog, [ data, result.time ] );

            }

        } else {

            self.exit();

alert( data.message );

        }

    } );

this.socket.addEventListener( 'close' , function( $event ) {

document.getElementById( 'exit' ).style.display = 'none';

        sog.context.clearRect( 0, 0, sog.context.canvas.width, sog.context.canvas.height );

    } );

};


Server.prototype.data = function( $time ) {

this.socket.send( this.command.DATA +'::'+JSON.stringify( { roomNo :this.roomNo, time : $time } ) );

};


Server.prototype.register = function() {

this.socket.send( this.command.REGISTER + '::' +JSON.stringify( { roomNo : this.roomNo } ) );

};


Server.prototype.update = function( $data ) {

this.socket.send( this.command.UPDATE +'::' +JSON.stringify( {

        roomNo : this.roomNo,

        userId :this.userId,

        speedV : $data.speedV,

        speedH : $data.speedH,

        left : sog.sprite.p1.left + $data.speedV,

        top : sog.sprite.p1.top + $data.speedH,

        direction : $data.direction,

        status : $data.status,

        attackStatus : $data.attackStatus ||'none'

    } ) );

};


Server.prototype.exit = function( $target ) {

cancelAnimationFrame( requestId );

    requestId =null;

this.socket.close();

};




Server는 크게 웹 소켓 서버와 연결하고, 데이터를 주고받을 때 흐름을 정의하는 connect 메서드와 그 내부에서 상세 처리에 사용되는 data, register, update, exit 메서드가 있습니다. data 메서드는 서버로부터 데이터를 요청하고, register 메서드는 처음 게임에 진입하였을 때 서버에 캐릭터를 등록하고, update 메서드는 캐릭터 정보의 변경사항을 서버에 반영하는 기능을 합니다. 이들이 서버로 보내는 메시지 구조는 [ Command::JSON 형식 문자열 파라미터 ] 이며, Command는 Server의 생성자에 정의되어 있습니다. 마지막 exit 메서드는 서버와 접속에 문제 있을 경우 게임을 정상 종료시킵니다.


connect 메서드에서는 WebSocket 서버를 만들고, 서버와 연결됐을 경우 발생하는 이벤트 open에서 register 메서드를 호출하여 캐릭터 등록을 진행하고, 서버로부터 메시지를 받을 때 발생하는 이벤트 message에서는 서버가 넘겨준 데이터를 뽑아 REGISTER 커맨드의 데이터인 경우 Game 객체가 넘겨준 $registerCB 함수에 데이터를 넘겨 호출하고, DATA 커맨드의 경우 $dataCB 함수에 데이터를 넘겨 호출하여줍니다. 그러면 각 CB함수에서 게임 진행과 관련된 처리를 하게 됩니다.


계속해서 CB함수를 가지고 있는 Game 클래스를 살펴보겠습니다.




Game = function( $params ) {

    this.context = $params.context;

    this.server = $params.server;

    this.sprite = {};

    this.sprite.p1 = $params.sprite;

    this.sprite.p2 = null;

};


Game.prototype.start = function() {

    painters = createPlayerPainters();

this.server.connect( this.registerCB, this.dataCB );

};


Game.prototype.progress = function( $time ) {

    this.server.data( $time );

};


Game.prototype.registerCB = function( $data ) {

this.server.userId = $data.userId;

    requestId =requestAnimationFrame( $util.fn( this.progress, this ) );

};


Game.prototype.dataCB = function( $data, $time ) {

var p2, id;


  if ( $data[ this.server.userId ].energy < 1 ) {

this.server.exit();

    }


this.context.clearRect( 0, 0, this.context.canvas.width, this.context.canvas.height );

    setSpriteData( this.sprite.p1, $data[ this.server.userId ] );

this.sprite.p1.update( $data[ this.server.userId ], $time );

this.sprite.p1.paint( this.context );


for ( id in $data ) {

if ( id != this.server.userId ) {

            p2 = id;

        }

    }


if ( p2 ) {

if ( !this.sprite.p2 ) {

this.sprite.p2 = new Sprite;

this.sprite.p2.left = $data[ p2 ].left;

this.sprite.p2.top = $data[ p2 ].top;

        }


        setSpriteData( this.sprite.p2, $data[ p2 ] );

this.sprite.p2.update( $data[ p2 ], $time );

this.sprite.p2.paint( this.context );

    }


    requestId =requestAnimationFrame( $util.fn( this.progress, this ) );

};




Game은 캔버스의 Context, Server, Sprite를 가지고 게임 진행 로직을 구현합니다. 앞에서 봤던 dataCB, registerCB 외에 게임의 시작점인 start 메서드와 게임 진행 중 계속 반복되는 로직인 progress 메서드를 가지고 있습니다.


앞에 initialize 함수에서 Game을 만든 후 start 메서드를 호출하며 끝이 났는데, start 메서드는 createPlayerPainters 함수를 호출하여 각 플레이어의 개별 Painter를 만들고, Server의 connect 메서드에 자신의 dataCB와 registerCB를 넘겨 호출합니다.


progress 메서는 서버로부터 데이터를 받아오는 기능을 하는 Server.data 메서드를 호출하기만 합니다. 이 progress 메서드는 각 CB 메서드들에서 requestAnimationFrame을 통해 빠른 속도로 계속적으로 호출될 것입니다. 우리 게임은 매 프레임마다 빠르게 서버로부터 데이터를 얻어와 화면에 반영해야 하기 때문에 progress로 인해 Server.data가 호출되고 그러면 서버로부터 데이터를 받으면 호출되는 콜백 함수인 dataCB가 호출되고, dataCB에서 받은 데이터로 화면을 그린 후 다시 progress를 호출하면서 로직이 반복 완성됩니다.


registerCB에서는 서버로부터 받은 사용자 ID를 세팅하고 requestAnimationFrame으로 progress 메서드를 시작합니다.


Game의 마지막 메서드인 dataCB에서는 처음에 에너지가 0인지 확인하여 0일 경우 게임을 종료하고, 아니면 화면을 다 지운 후 setSpriteData 함수로 본인 Sprite에 서버에서 받은 데이터를 세팅합니다. 그리고는 Sprite를 update 하여 변경된 데이터를 반영하고, paint 하여 화면에 그립니다.


그 이후 적 플레이어( 2P )가 있는지 확인하고 있으면 같은 작업을 해주고, requestAnimationFrame으로 progress 메서드를 반복하며 끝이 납니다.


마지막으로 지금까지 소스코드를 진행하면서 필요했던 함수들입니다. 간단히 주석을 달아 놓았으니 참고하시기 바랍니다.




// 각 플레이어 별 Painter 인스턴스를 세팅한다.

function createPlayerPainters() {

    var painters = {};


    for ( var p in sog.sprite ) {

        painters[ p ] = {

            UP : {

                STAY : PainterFactory.create( PainterFactory.UP ),

                MOVE : PainterFactory.create( PainterFactory.UP ),

                ATTACK : PainterFactory.create( PainterFactory.ATTACK_UP )

            },

            DOWN : {

                STAY : PainterFactory.create( PainterFactory.DOWN ),

                MOVE : PainterFactory.create( PainterFactory.DOWN ),

                ATTACK : PainterFactory.create( PainterFactory.ATTACK_DOWN )

            },

            LEFT : {

                STAY : PainterFactory.create( PainterFactory.LEFT ),

                MOVE : PainterFactory.create( PainterFactory.LEFT ),

                ATTACK : PainterFactory.create( PainterFactory.ATTACK_LEFT )

            },

            RIGHT : {

                STAY : PainterFactory.create( PainterFactory.RIGHT ),

                MOVE : PainterFactory.create( PainterFactory.RIGHT ),

                ATTACK : PainterFactory.create( PainterFactory.ATTACK_RIGHT )

            }

        };

    }


return painters;

}


// 인자로 받은 데이터를 Sprite에 적용한다.

function setSpriteData( $sprite, $data ) {

    // 위  createPlayerPainters 함수로 만든 painters로 알맞은 Painter  객체를 뽑는다.

var painter = painters[ $sprite === sog.sprite.p1 ?'p1' : 'p2' ][ $data.direction ][ $data.status ];


if ( $sprite.painter !== painter ) {

        $sprite.painter = painter;

    }

if ( $data.status === 'MOVE' || $data.status ==='ATTACK' ) {

        setAllActors( $sprite );

    } else { // STAY

        // 상태가 STAY 일 경우, 모든 Actor를 제거하여 캐릭터가 멈추도록 한다.

        clearAllActors( $sprite );

    }

}


// Sprite에 모든 Actor를 삽입한다.

function setAllActors( $sprite ) {

for ( var name in Actors ) {

 if ( !( name in $sprite.actors ) ) {

            $sprite.actors[ name ] = new Actors[ name ];

        }

    }

}


// Sprite의 모든 Actor를 제거한다.

function clearAllActors( $sprite ) {

for ( var name in Actors ) {

delete $sprite.actors[ name ];

    }

}


// 현재 데이터로 캐릭터들의 위치 및 상태를 파악하여 벽 / 캐릭터 간 / 공격의 충돌처리를 한다.

function collision( $data ) {

var

    // 내 캐릭터의 현재 위치정보

    p1 = sog.sprite.p1,

    p1Left = p1.left + CORRECT_LVALUE,

    p1Right = p1Left + CHARACTER_SIZE - CORRECT_LVALUE *2,

    p1Top = p1.top + CORRECT_TVALUE,

    p1Bottom = p1Top + CHARACTER_SIZE - CORRECT_LVALUE - CORRECT_TVALUE,


    // 내 캐릭터의 다음 위치정보

    nextLeft = p1Left + $data.speedV,

    nextRight = p1Right + $data.speedV,

    nextTop = p1Top + $data.speedH,

    nextBottom = p1Bottom + $data.speedH,


  // 나의 공격 범위

    attackLeft = p1.left,

    attackRight = attackLeft + CHARACTER_SIZE,

    attackTop = p1.top + CORRECT_TVALUE /2 + 5,

    attackBottom = attackTop + CHARACTER_SIZE - ( CORRECT_TVALUE /2 +5 ) *2,


    p2 = sog.sprite.p2, p2Left, p2Right, p2Top, p2Bottom;


// 캐릭터가 벽과 충돌한 경우

if ( nextLeft <0|| nextRight > sog.context.canvas.width || nextTop <0 || nextBottom > sog.context.canvas.height ) {

        p1.data.direction = $data.direction;

        $util.fireEvent( document, 'keyup' );

return false;

    }


    if ( p2 ) {

       // 적 플레이어의 현재 위치정보

        p2Left = p2.left + CORRECT_LVALUE;

        p2Right = p2Left + CHARACTER_SIZE - CORRECT_LVALUE *2;

        p2Top = p2.top + CORRECT_TVALUE;

        p2Bottom = p2Top + CHARACTER_SIZE - CORRECT_LVALUE - CORRECT_TVALUE;


        // 캐릭터끼리 겹쳐지지 않았을 때

if ( ! ( ( p1Left >= p2Left && p1Left <= p2Right || p1Right >= p2Left && p1Right <= p2Right ) && ( p1Top >= p2Top && p1Top <= p2Bottom || p1Bottom >= p2Top && p1Bottom <= p2Bottom ) ) ) {

// 캐릭터끼리 충돌한 경우

   if ( ( nextLeft >= p2Left && nextLeft <= p2Right || nextRight >= p2Left && nextRight <= p2Right ) && ( nextTop >= p2Top && nextTop <= p2Bottom || nextBottom >= p2Top && nextBottom <= p2Bottom ) ) {

                p1.data.direction = $data.direction;

                $util.fireEvent( document, 'keyup' );

return false;

            }


      // 공격이 성공한 경우

if ( $data.status ==='ATTACK' ) {

 if ( ( $data.direction ==='UP'&& p1Left >= p2Left && p1Left <= p2Right && p1Top >= p2Bottom && attackTop <= p2Bottom ) ||

                ( $data.direction ==='DOWN' && p1Right >= p2Left && p1Right <= p2Right && p1Bottom <= p2Top && attackBottom >= p2Top ) ||

                 ( $data.direction === 'LEFT' && attackTop <= p2Top && attackBottom >= p2Bottom && p1Left >= p2Right && attackLeft <= p2Right ) ||

                 ( $data.direction === 'RIGHT' && attackTop <= p2Top && attackBottom >= p2Bottom && p1Right <= p2Left && attackRight >= p2Left ) ) {

                    $data.attackStatus = 'success';

                }

            }

        }

    }


return true;

}




이것으로 클라이언트 개발이 완료되었습니다. 긴 글 따라와 주셔서 고맙습니다. 다음 글에서는 마무리하지 못했던 웹 소켓 서버 로직을 완성하도록 하겠습니다.





Contents


01 주제 / 대상 독자 / 개발 원칙

02 요구사항 정의 / 설계

03 Python HTTP Server 개발

  03-01 멀티 쓰레드 지원 / POST 메서드 처리 / 로깅

  03-02 URL 라우팅 / 정적 리소스 처리

04 Python web socket server 개발
  04-01 웹 소켓 개요

  04-02 웹 소켓 서버 구현

05 클라이언트 개발

  05-01 캐릭터 스프라이트 개발

  05-02 클라이언트 로직

  05-03 서버 로직



매거진의 이전글 처음 만드는 온라인 게임 05-01
브런치는 최신 브라우저에 최적화 되어있습니다. IE chrome safari