TIL-7주차 코드

 

1. JS [23.01.09]


1) game example

  • 적기의 각각이 기능을 갖고 있도록 해야 한다.

  • 실습 코드


- main.html
<!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>Document</title>

    <!-- 순서가 중요하다.**** --> <!-- 새로운 페이지 등록 시, url 연결 시켜주기! -->
 
    <!-- <script src="./item/background.js"></script>    
    <script src="./item/boy.js"></script>
    <script src="./item/enemy.js"></script>
    <script src="./panel/game-canvas.js"></script>   -->
    <script type = "module" src="./app.js"></script>
    
    <!-- <script src="ex1-es6-var.js"></script> -->
    <!-- <script type="module" src="./ex2-es6-module.js"></script> -->
    
    <script>

        // ES6 공부 시작
        // let kor = 20;
        // let eng = 30;

        // // 기존 방식
        // let obj = {
        //     kor: kor,
        //     eng: eng,
        //     total: function(){
        //         return this.kor + this.eng;
        //     }
        // };

        // function print(exam){
        //     let kor = exam.kor;         // 코드를 연산할 때, 더 간단하게 만들어준다!!!(집중화)
        //     let eng = exam.eng;
        //     let total = exam.total; // 문제가 많다. 원래는 exam.total이여야 한다. 하지만, let 변수(let obj는 window에서 호출)에 의해서 exam이 아니라 window가 호출했다.
        //                                          // 따라서, exam이 호출했다고 인식하도록 bind를 시켜주어야 한다.

        //                                          // 호출되는 시점에 객체를 통해 호출을 한다.
        //                                          // 그래서 bind하는 것이고 let을 이용하면, 지역변수가 된다.
        //                                          // ** let으로 선언했기 때문에 print함수가 끝나면 let변수는 모두 사라진다. 

        //     //let total = exam.total();            // 굳이 bind를 이용하지 않고 이런식으로 exam.total()을 변수 total에 담아도 된다.

        //     console.log("kor:"+ kor);
        //     console.log("eng: "+ eng);
        //     console.log("total: "+ total);
        // }
        // print(obj);

        // // 새로운 방식!!([객체 뽀개기]) - 함수도 객체 뽀개기!!
        // let obj1 = {kor,eng,total(){   //  키와 값이 같다면 하나만 써도 된다. 함수를 이렇게 바로 쓸 수도 있다.
        //     return this.kor + this.eng;
        // }};

        // function print1({kor,eng}){           // 디스트턱처링!! : 객체 뽀개기. 객체를 쪼개서 3개로 나눠서 받는다.
        //                                       // 원래 데이터만 뽀개야한다. 함수는 뽀개는 것이 아니다!!******
        //     console.log("kor:"+ kor);
        //     console.log("eng: "+ eng);
        //     //console.log("total: "+ total());
        // }

        // print1(obj1);


        // // [디스트럭쳐]의 또 다른 기능
        // // 중괄호 : 오른쪽 객체 생성, 왼쪽은 변수 생성
        // // 따라서, 중괄호로 변수를 선언했으면, 더이상 선언할 수 없다.
    

        // let obj2 = {kor,eng,total(){   //  키와 값이 같다면 하나만 써도 된다. 함수를 이렇게 바로 쓸 수도 있다.
        //     return this.kor + this.eng;
        // }};

        // function print2(exam){  
        //     let {kor, eng, aa=9} = exam;      // 변수 선언만 한 것이다. 앞의 변수를 간단하게 만들어주는 것과 같다.
        //                                       // 또한, 이런 식으로 aa가 선언되면서 초기화 할 수 있다.
                                              
        //     console.log(aa);                  // 9가 출력
            
            
        //     console.log("kor: "+ kor);
        //     console.log("eng: "+ eng);
        //     // console.log("total: "+ total());
        // }


        // print2(obj2);

        // // *** ES6 정리(23.01.03) : 클래스를 정의하고 기존 프로토 타입의 속성을 함수로 재구성
        // // 변수 const, let 등장!!
        // // 데이터를 뽀개는 방식으로 지역 변수 선언 방법이 새로 생김.


    </script>


    <!-- 문서가 다 읽혀지고 읽어지는 방법 : html, js에서 해결 가능(document.addEventListener 이용) -->
    
</head>
<body>
      <!-- 주의사항 : css를 키우면 배율이 커진다. 그래서 속성을 키워야 한다. 즉, canvas를 키워야 한다.
           보통의 경우 속성을 키우고 css로 줄이는 경우는 이미지가 깨지는것을 방지해준다.  -->

      <input tabindex="2">
      <img>
      <div tabindex="1">            <!-- tabindex를 넣어야지 tab으로 이동할 수 있다. -->
            test                    <!-- 마우스가 기본이라서 마우스가 우선 순위라서 사용자가 마우스를 클릭하고나서 키의 입력을 받을 수 있다. -->
      </div>                        <!-- 그래서 키보드 입력하기 위해서 tabindex를 설정한다!! -->
      <input tabindex="0">
      <!-- style="display: none;"은 style 안보이지만 메모리에 올라가 있다 즉, dom에 올라가 있따. -->
      <img src="./image/boy.png" id ="boy" style="display: none;">
      <img src="./image/enemy.png" id ="enemy" style="display: none;">
      <img src="./image/map.png" id ="bg" style="display: none;">
      <canvas tabindex="0" class="game-canvas" width="500" height="1000"></canvas>


</body>
</html>



  • app.js


import GameCanvas from './panel/game-canvas.js';        // *** default(GameCanvas) 이름을 마음대로 상용할 수 있다!!

window.addEventListener("load", function(){

    var gameCanvas = new GameCanvas();  // 다른 파일을 가져오는 방식이 자바스크립트에서는 없다.
                                        // 그래서 사용할 모듈을 html에게 요청한다.


                                        // 에러 : GameCanvas 객체를 생성시, 참조는 소문자에 넣어준다. 
    gameCanvas.run(); 
        
    // run()은 무한 loop해야 한다. UI 스레드는 별도의 흐름을 갖게 해야 한다.      
    // vscode에선 F12로 함수 선언과 이동 가능

 
});




  • game-canvas.js


// game-canvs.js는 사용자 입력과 출력을 담당한다.(중요!!) *****

import Boy from '../item/boy.js';
import Background from '../item/background.js';
import Enemy from '../item/enemy.js';

// **** import 된것을 새롭게 뱉어내는 방식!!
export default class GameCanvas {        // 클래스로 변경  

    constructor(){                                          // **** 중요한 개념 : 
        this.dom = document.querySelector(".game-canvas");  // 여기서 this는 GameCanvas의 new된 결과이다.
                                                            // dom이 canvas이다. canvas가 this의 멤버로 들어온 것이다.
                                                            // 원래 java에서는 canvas가 canvas를 갖는 것이 이상하다. 
                                                            // 그래서, JS는 틀로 쓸 수 있는 것이 아니다.(프레임워크가 아니였다.)

                                                            // 앞으로는 this의 dom 기능을 이용한다.

        this.dom.focus();     // focus를 잡아주면 canvas에서 키보드 입력을 할 수 있다. **** 즉, 우선순위를 마우스에서 키보드로 넘겨준다.
                              // 키보드로 무브하는 방법???? 
    
        /** @type {CanvasRenderingContext2D} */ 
        
        this.ctx = this.dom.getContext('2d');   // var ctx가 아니라 this.ctx이다.(Boy의 ctx이므로)         
        
        // ----------------- [ Boy ] ----------------------------------------
        this.boy = new Boy(100, 100);

        this.boy.speed++;                       // get을 사용하지 않고도 speed로 바로 getSpeed 처럼 사용할 수 있다.
        console.log("speed:" + this.boy.speed);

        // ---------------- [ Background ] ------------------------------------------------
        this.bg = new Background();

        this.gameover = false;      // setTime을 위한 상태값!!****, 게임 오버는 아니고 애니메이션만 
                                    // 멈추게 해야 한다. 즉, 멈추면 상태값이 전부 날아가는 것이 
                                    // 아니라 정지인 상태이다.(배경과 적들은 움직임)

        this.pause = false;         // 그래서, 게임이 끝나는 것이 아니라 일시정지가 되어야 한다.

        // ---------------- [ Enemy ] ----------------------------------------------------

        this.enemy = new Enemy();
        
        // **** 다른 애가 불러내서 위임되는 함수라고 부르며 "call back function"이라고 부른다. 
        // 사용자가 클릭해달라고 위임했기 때문이다. call back function은 지금 실행하는 것이 아니라 나중에 실행된다.(위임해야지 나중에 전화할 수 있기 때문이다.(나중에 실행))
        // 전화하는 애가 주체가 된다. 
        // dom이 다시 호출해준다? 
        // this.dom.onclick = this.clickHandler;                   // ()처럼 함수를 호출한 결과 값 넣지 말기

        this.dom.onclick = this.clickHandler.bind(this);         // **** .bind() 처리하면서 .bind(this)의 this는 이벤트 핸들러를 위한 객체를 불러온다. 원래 this는 gamecanvas이다.
        // this.dom.onclick = this.clickHandler;                       // **** .bind() 처리하면서 .bind(this)의 this는 이벤트 핸들러를 위한 객체를 불러온다. 원래 this는 gamecanvas이다.

        this.dom.onkeydown = this.keyDownHandler.bind(this);
        this.dom.onkeyup = this.keyUpHandler.bind(this);
        

    }                                                            
                                                                  // **** 내장 객체의 함수와 새로만들어 지는 객체의 함수를 비교할 것.
                                                                  // clickHandler는 사용자 입력에 의한 이벤트 핸들러이기 때문에
                                                                  //  window에서 가져와야 해서 .bind(this)를 뒤에 붙여준다.
    

            // 이렇게 사용하면 위의 코드와 비교해서 dom은 이전의 생성자에서 넘겨받아서 this가 dom이 아니다.
            //console.log(this)


    run(){
        //console.log(this);
        if(this.pause)
            return;

        // 60 프레임으로 화면을 다시 그리는 코드!!
        this.update();
        this.draw();

        // // 게임 만들 때, 심박동을 위해서 setTimeOut, setInterval, AnimationFrame 심박동을 우리가 맞출 수 없다.(조정이 불가능)
        // // setTimeOut - 1회성, setInterval - 반복성, AnimationFrame - 프레임 1초에 60회로 고정
        // window.setTimeout(function(){
        //     console.log("time out");    
        // },1000);

        // window.setTimeout(this.run.bind(this), 10000);

        // window.setTimeout(this.run.bind(this), 1000);

        // // this를 알게 하기 위해서 호출하는 주체를 봐야 한다.
        // window.setTimeout(function(){
        //     this.run();      // 여기서 함수를 호출하는 것이 window라고 앞에 적혀 있어서    
        // },1000).bind(this);                        // 그래서 우리는 여기 있는 run을 실행하기 위해서 bind를 사용한다!!

        // // ************ ES6 함수 정의 방법 : Arrow Function ************
        // // 자기 this가 없으니까 밖의 this를 사용한다. (자기만의 영역이 없다. 지역화가 없다.)
        // // 자기만의 this가 없어서 bind를 사용하지 않는다.****  
        // window.setTimeout(()=>{
        //     this.run();    
        // },1000);

        window.setTimeout(()=>{                 // 알람 맞추는 것과 같다.(60 프레임에 맞추어서 = 1000/60 = 17)
            this.run();    
        },17);

        // 여기까지 하면, 무한히 반복한다. 멈추고 싶으면? 이 안에서 멈추는 도구가 필요하다. 그래서 상태변수가 필요하다.

    }

        // canvas는 UI이고 UI에 의해서 꼭두각시처럼 컨트롤된다.

    update(){   // update는 단위 계수마다 잘게 쪼개서 움직여야 한다.
                // 시간에 국한에서 잘게 짜르려면 frame을 나누어야 한다. 
                // 이동하는 속도가 일정하려면, 

        this.boy.update();              // 이동하고 
        //this.bg.update();              // 이동하고 

        //this.enemy.update();
    }
    
    draw(){
        this.bg.draw(this.ctx);         // 순서 중요!!
        this.boy.draw(this.ctx);        
        //this.bg.draw(this.ctx);

        this.enemy.draw(this.ctx);
    }

    pause(){
        
    }
    
    // ------------------------------- event handler***** --------------------------------
    // 이벤트가 발생했다!! 사용자 입력에 의해서 이벤트가 발생 // 사건의 구체적인 정보를 알아야내 한다.(어떤 위치에 마우스를 클릭했는지, 어떤 키보드를 눌렀는지)

    clickHandler(e){         // 클릭하면, 타이머가 멈춘다!!
        //this.pause = true; // 같은 함수 내부라서 함수 호출 시, 
                             // 함수를 사용하는 것이 아니라 값을 입력해준다.

        // this.boy.move(2);    // 마우스로 move하는 것은 옳지 않고 방향키로 나중에 구현할 것
                                // 마우스는 사용자 입력이다.
        //console.log(this);
        // ** 상태 변수를 이용
                                    // moveTo 메서드에서는 e.x와 e.y는 마우스 클릭할 때, 이용                        
        this.boy.moveTo(e.x,e.y);   // 이동할 때, 대각선 길이를 잘게 쪼개는데 가로 세로는 그 값이 얼마였든지간에 비율로 대각선 길이로 나눈다.
                                    // 나중에 배우자! 이벤트 함수 : screen x, event x, osell x


        // 화면 지우기 필요!
    
        this.boy.draw(this.ctx);    // 함수 호출자는 dom이다. 하지만, 우리는 this가 GameCanvas였으면 좋겠다. 서로 역할이 엇갈림.
                                    // 그래서 우리는 보따리를 쌓아 줘야한다.(위에서 bind로 묶어줌.) *******
    
    
        //console.log(this);      // 위의 dom이 앞에서 위임받아서 dom이 아니라 이전 생성자이다.
        }


    // ***** canvas나 div 태그는 키 입력을 안 받게 했다.
    keyDownHandler(e){
        
        console.log(e.key);
        switch(e.key){
            case "ArrowUp" :            // 이렇게만 만들면, 렉걸린것처럼 보인다.(한번 클릭 후 기다렸다가 여러번 클릭된다.)
                                        // 이것은 더블클릭 때문에 눌렀다가 뗄 때, 기다리는 현상이 있다.
                this.boy.move(1); 
                break;
            case "ArrowLeft" :
                this.boy.move(4); 
                break;

            case "ArrowRight" :
                this.boy.move(2); 
                break;

            case "ArrowDown" :
                this.boy.move(3); 
                break;

        }
    }

    keyUpHandler(e){
        
        console.log(e.key);             // 이렇게 해도 문제가 많다. 여러 방향키를 동시에 처리하지 못한다.(렉걸림)

        //this.boy.move(0);
        switch(e.key){
            case "ArrowUp":             // 이렇게만 만들면, 렉걸린 것처럼 보인다.(한번 클릭 후 기다렸다가 여러번 클릭된다.)
                                        // 이것은 더블클릭 때문에 눌렀다가 뗄 때, 기다리는 현상이 있다.
                this.boy.stop(1);       // 그래서, onkeyup으로 만들자!
                break;
            case "ArrowLeft":
                this.boy.stop(4); 
                break;

            case "ArrowRight":
                this.boy.stop(2); 
                break;

            case "ArrowDown":
                this.boy.stop(3); 
                break;

        }
    }
            
}


// ***** export default class : 이렇게 말고 다른 방법으로 함수를 정의할 때 사용해도 된다.


  • boy.js
// 원래 웹에서는 파일명을 가장 앞에는 소문자로 구분하고 대쉬(-)로 구분한다

// 중요!!!!**** 
// 캡슐화 vs 캡슐(데이터를 구조화한 것) 
// 캡슐화란 - 책임을 부여하는 것, 역할을 나누는 것, 기능을 부여하는 것
// 객체지향은 그림을 그릴 수 밖에 없고, 그 그림은 설계가 된다.

export default class Boy {                     // *******

    constructor(x,y){
        this.x = x || 200;      // 6. 오버로드 생성자로 사용자가 넘겨줄 값이 있을 때, 가능하게 하려고 이렇게도 가능하다.
        this.y = y || 100;      // 사용자 입력이 없을 때는, x와 y는 || 뒤의 기본값이 들어간다.
        
        this.vx = 0;            // 단위 위치(잘게 조개진 값)
        this.vy = 0;            
        
        this.dx = 0;            // 목적지 위치
        this.dy = 0;     
        
        this.dir = 0;


        var moveLeft = false;   // 처음에 FALSE로 설정
        var moveRight = false;
        var moveUp = false;
        var moveDown = false;

        this.speed = 3;         // 속성, , 이벤트

        this.img = document.querySelector("#boy");          // html에서 이미지를 이렇게 가져온다.(이젠, 생성자에서 그리기 위해서!) *************

        this.ix = 1;                                        // ix, iy는 스프라이트 사진에서 부분을 꺼낼수 있게 배열의 인덱스 값이다!
        this.iy = 2;
    
        this.walkDelay = 20;            // 프레임 낮춰주기!!

        this.sw = 106;
        this.sh = 148.25;
        this.sx = this.sw * this.ix;
        this.sy = this.sh * this.iy;    // 메인 스레드는 절차를 담당하는 기본 흐름이다. setTimeout인 보조 흐름이며 추가적으로 작동을 만든 것이 게임 스레드이다.

       
    }                                   // 이것의 차이를 알기!!****
                                        // update와 draw는 메인 스레드(게임 스레드)이다.(사용자 입력이 없더라도, 내가 호출하는 것이 아니라 초당 60번씩 계속 돌고 있다.)
                                        // 나머지는 UI 스레드이다.(사용자 입력이 있을 때만, 한 번만 움직임.)

    // set speed(value){
    //     this.#speed = value;             // *** JS에서 getter, setter를 이렇게 사용할 수 있다.
    // }

    // get speed(){
    //     return this.#speed;
    // }


    draw(ctx){

        this.sx = this.sw * this.ix;
        this.sy = this.sh * this.iy;
        ctx.drawImage(this.img, this.sx, this.sy, this.sw, this.sh,     // **** 에러발생 : img는 생성자에서 가져오기 때문에 this.img 이다!!
            this.x-this.sw/2, this.y-this.sh+15, this.sw, this.sh);     // 발이 기준이 되게 값을 조정해주자!! 
        
        // var img = new Image();	
        // img.src = './image/boy.png';    // 이미지도 생성자에서 생성하자!!*****

        // img.onload = function(){    // 이렇게 만들면 에러가 나는 이유?

        //                             // sx는 source(원본의 x크기), dx(display의 x크기)
          
        // }.bind(this);               // 여기서는 Boy에서 img로 이벤트가 위임 되었다!!(중요)
        
        // 개념적인 부분 : 위임? 다시 이벤트 위임 공부, Function.prototype.call()은 모든 함수에서 call이 있어야 한다.
        
        // 중요!! 함수가 호출한 주체가 this가 된다. apply, call
        //console.log(this);
    }
    
    update(){       // ** update는 상태값만 변경해줘야 한다.
        
        // ======================================
        
        // --- 이동을 위한 코드 -------

        if(this.moveUp)                     // 이런식으로 구현하면 여덟 방향의 이동도 가능하다.
            this.y -= this.speed;          // 방향을 이거로만 업데이트!!
        
        if(this.moveDown)
            this.y += this.speed;          // 또한, #을 붙이면, private 변수로 바뀌고 이것은 java처럼 getter, setter처럼 이용하자!
        
        if(this.moveRight)                  // getter, setter를 이용할 수 있는 색다른 방법? : C#에서 이용한다!
            this.x += this.speed;
            
        if(this.moveLeft)
            this.x -= this.speed;





        // switch(this.dir){          // 코드의 순서 주의!!(아래 코드에서 return 때문에 문제가 발생했다.)
        //     case 1: // 북쪽        // 하지만 이렇게 하면, 캐릭터가 멈추지 않아서 키보드 뗄 때, 멈추도록하자!     
        //         this.y -= 1;      
        //     break;                   // 브라우저 기준으로 시작을 생각해보면 된다.
    
        //     case 2: // 동쪽
        //         this.x += 1;
        //     break;
            
        //     case 3: // 남쪽
        //         this.y += 1;
        //     break;
            
        //     case 4: // 서쪽
        //         this.x -= 1;
        //     break;
            
        // }

        // setInterval(draw, 10);
        
        //this.draw().drawImage(this.img, this.sx*this.ix, this.sy*this.iy);
        
        // 조건 설정!! dx가 중심점, dy가 중심점이라고 가정하에 조건문 설정!
        // 정확히는 dx가 기준점이 되고 x가 움직이는 변수이다.
        //  x축 따로, y축 따로 구성해서 합쳐서 구현!!
        
        if(this.dx - 1 < this.x  && this.x < this.dx + 1 ||  this.dy - 1 < this.y && this.y < this.dy + 1){           
            this.vx = 0;
            this.vy = 0;
            
            this.ix = 1;                         // 멈출 때만 상태값을 바꿔줘도 되는가?***
            
        }
        
        if(!(this.moveLeft || this.moveRight || this.moveUp || this.moveDown || false))     // 방향키가 눌린게 없다면? 다음 조건문을 진행!
            if(this.vx == 0 && this.vy == 0 ) {                                             // 멈출 때 조건 설정 : vx 설정 (이것도 되긴 된다.)
                this.ix = 1;                                                                // 멈췄는지 안멈췄는지는 vx의 유무가 중요하다!!
                return;
            }                                                                               // 여기가 정확히 캐릭터가 멈추게 해주는 역할이다!!!!*******
                                                                                            // 그래서 여기서, 발바꿈을 해준다.
                                                                                            // *** 숙제!! : 캐릭터 화면 전환하기
        // ======================================                                           // *** 추가로 할 것 : 발속도 올리기 : speed라는 변수를 둬야 한다.
        
        this.x += this.vx;                  // 여기서 목적직 위치까지 단위 위치(벡터)로 더하면서 업데이트 시킴
        this.y += this.vy;

        // 걸음을 걷는 효과!
        this.walkDelay--;
        //this.ix=this.ix==2?0:2;   // 3항 연산 중요!!!
        if(this.walkDelay == 0){
            if(this.ix == 0){
                this.ix = 2;
                
            }
            // else if(this.ix == 1){
            //     this.ix = 0;
                
            // }
            else {
                this.ix = 0;
                
            }
            this.walkDelay = 20;
        }


        // ------------------- 이동을 위한 로직 ------------
    }

    moveTo(dx,dy){
        
        let w = dx - this.x;                // 목적지 위치 - 현재 위치(업데이트되고나서) = 남은 위치 거리 계산 
        let h = dy - this.y;

        let d = Math.sqrt(w*w+h*h);         // 대각선 길이 공식

        this.vx = w / d;                    // 여기 this도 주의해서 적기, 그 남은 위치 거리를 다시 잘게 쪼개진 비율로 나누어서 이동한다.
        this.vy = h / d;
        
        this.dx = dx;
        this.dy = dy;
    }

    move(dir){

        switch(dir){               // 코드의 순서 주의!!(아래 코드에서 return 때문에 문제가 발생했다.)
            case 1: // 북쪽        // 하지만 이렇게 하면, 캐릭터가 멈추지 않아서 키보드 뗄 때, 멈추도록하자!    
                this.moveUp = true;
                break;
                
            case 3: // 남쪽        // ** 결론 : 자연스럽게 방향키 이동을 위해서, 동서남북의 4방향 모두 각각 상태 변수로 컨트롤 되어야 한다!!!!
                this.moveDown = true;
                break;
    
            case 2: // 동쪽
                this.moveRight = true;
                break;

            case 4: // 서쪽
                this.moveLeft = true;
                break;

            default:
                console.log(this.dir);
            
        }

    }

    stop(dir){

        switch(dir){               // 코드의 순서 주의!!(아래 코드에서 return 때문에 문제가 발생했다.)
            case 1: // 북쪽        // 하지만 이렇게 하면, 캐릭터가 멈추지 않아서 키보드 뗄 때, 멈추도록하자!    
                this.moveUp = false;
                break;

            case 3: // 남쪽        // ** 결론 : 자연스럽게 방향키 이동을 위해서, 동서남북의 4방향 모두 각각 상태 변수로 컨트롤 되어야 한다!!!!
                this.moveDown = false;
                break;
    
            case 2: // 동쪽
                this.moveRight = false;
                break;

            case 4: // 서쪽
                this.moveLeft = false;
                break;

            default:
                console.log(this.dir);
            
            
            
        }

    }
}


  • background.js
export default class Background {
    
    constructor(){
        this.x=0;
        this.y=0;
        this.img = document.querySelector("#bg");

    }

    draw(ctx){
        ctx.drawImage(this.img, this.x, this.y);
    }

    update(){

    }
}


  • enemy.js
export default class Enemy{
    constructor(x,y){
        this.x = x || 200;
        this.y = y || 300;
        
        this.vx = 0;
        this.vy = 0; 
        
        this.dx = 0;
        this.dy = 0;
        
        this.img = document.querySelector("#enemy");

    }

    draw(ctx){

        ctx.drawImage(this.img, this.x, this.y, 200, 200);
    }
    
    // update(){

    // }

}



2) 은닉화 개념,

  • JS에서는 변수 앞에 #을 이용한다! 하지만, 이것은 매우 귀찮을 일이다.



3) 모듈화

  • 매번 html파일을 통해서 import하기 때문에 문제가 많다.

  • 스크립트 파일 각각이 import되기 전까지 다른 영향을 주지 않는 고립화가 되어잇어야 한다.(isolation)

  • 이제는, html파일에 영향을 받지 않고도 스크립트 파일을 생성할 수 있다.

  • export 이용하며 노출하고 싶은 함수들만 노출되도록 한다.


  • 실습 코드


- ex2-es6-module.js
// import aaa from './module1.js';     

// aaa();  // 아무거나 쓰면, default class를 가져온다.

// 이런식으로 import할 함수 2개를 한번에 가져올 수 있다.
import aaa,{test1, test2, Exam, exam2} from './module1.js'; 
import bbb,{test1 as test3} from './module2.js'; 

aaa();      // import할 함수명을 아무거나 써서 import하면, 
            // default class를 가져온다.

            // 또한, 무조건 default는 항상 갖고 있어야 한다.
test1();    
test2();
            // moduel간의 이름이 달라도 별칭을 사용하여 구분시켜 줄 수 있다.(as 이용)

bbb();
test3();    // default class 이외의 class를 생성하고 그것에서 설정한다.

let exam1 = new Exam();

console.log(exam1);         // 

console.log(exam2);         // *** "싱글톤 패턴"(객체를 1개만 만들어서 데이터를 전역에 걸쳐 공유하도록 
                            // 전역객체로 만드는 경우!!) 개념!!


- module1.js
export default function test() {
    console.log("module test");
}

export function test1() {
    console.log("module1. test1");
}

export function test2() {
    console.log("module1. test2");
}


export class Exam {     // class를 export하는 방법도 중요!

    constructor(){
        this.kor = 2;
        this.eng = 3;
        this.math = 4;

    }
}

// **** 공유해야할 데이터가 있을 경우, 전역객체를 만들고 싶을 때, 이렇게 사용한다.

// **** 전체의 객체에서 객체를 1개만 만들어야 하는 경우 : "SingleTone 패턴" 이라고 부른다. 누구도 객체를 새로 만들수 없다. 쉽게 접근할 수 있어야 한다.
export let exam2 =  new Exam();    

// 객체는 무조건 default로만 export가 된다!!
// 하지만, 선언하고 export하면 된다.


- module2.js
export default function test() {
    console.log("module2 test");
}

export function test1() {
    console.log("module2 test1");
}




2. JS [23.01.10]

1) game project test

  • 이벤트가 무엇이고 적기가 무엇인지? 판단하기!

  • game-canvas.js

// game-canvs.js는 사용자 입력과 출력을 담당한다.(중요!!) *****

import Boy from '../item/boy.js';
import Background from '../item/background.js';
import Enemy from '../item/enemy.js';

// **** import 된것을 새롭게 뱉어내는 방식!!
export default class GameCanvas {        // 클래스로 변경  

    constructor(){                                          // **** 중요한 개념 : 
        this.dom = document.querySelector(".game-canvas");  // 여기서 this는 GameCanvas의 new된 결과이다.
                                                            // dom이 canvas이다. canvas가 this의 멤버로 들어온 것이다.
                                                            // 원래 java에서는 canvas가 canvas를 갖는 것이 이상하다. 
                                                            // 그래서, JS는 틀로 쓸 수 있는 것이 아니다.(프레임워크가 아니였다.)

                                                            // 앞으로는 this의 dom 기능을 이용한다.

        this.dom.focus();     // focus를 잡아주면 canvas에서 키보드 입력을 할 수 있다. **** 즉, 우선순위를 마우스에서 키보드로 넘겨준다.
                              // 키보드로 무브하는 방법???? 
    
        /** @type {CanvasRenderingContext2D} */ 
        
        this.ctx = this.dom.getContext('2d');   // var ctx가 아니라 this.ctx이다.(Boy의 ctx이므로)         
        


        // ----------------- [ Boy ] ----------------------------------------
        this.boy = new Boy(100, 100);

        this.boy.speed++;                       // get을 사용하지 않고도 speed로 바로 getSpeed 처럼 사용할 수 있다.
        console.log("speed:" + this.boy.speed);




        
        // ---------------- [ Enemy ] ----------------------------------------------------
        
        this.enemy = [];    // Aggregation 방식!!

        this.enemyAppearDelay = 60;     // 적의 딜레이 만들기!(60프레임 기준 : 1초)
                                        // 17ms가 60프레임, 34ms가 30프레임


        // 이런식으로 적기를 만들 때, 콜백함수를 줄 수 있거나 따로 줄 수 도 있다.

        // *** 1번 Aggregation 관계에 사용
        this.enemies = [];

        // *** 2번 Composition 관계에 사용
        // this.enemies = [new Enemy(10,0), new Enemy(30,0), new Enemy(50,0), new Enemy(100,0),
        //     new Enemy(40,20), new Enemy(50,20), new Enemy(130,45)];



        // **** 다른 애가 불러내서 위임되는 함수라고 부르며 "call back function"이라고 부른다. 
        // 사용자가 클릭해달라고 위임했기 때문이다. call back function은 지금 실행하는 것이 아니라 나중에 실행된다.(위임해야지 나중에 전화할 수 있기 때문이다.(나중에 실행))
        // 전화하는 애가 주체가 된다. 
        // dom이 다시 호출해준다? 
        // this.dom.onclick = this.clickHandler;                   // ()처럼 함수를 호출한 결과 값 넣지 말기
        

        // 1. [Composition 관계]******
        // *** this를 바운더리 밖의 this를 이용하고 싶어서 람다함수를 이용한다.(bind를 안쓰려고!!)

        // *** 결론 : 객체를 new로 생성하면서 생성자에 바로 넘겨준다.
        // *** 결론 : 캔바스(객체 배열을 갖고 있는 것은 "game-canvas"라서 여기서 지운다.)에서 지우는 역할,
        // ***** enemy.js 객체(조건을 계속 감지)에서는 조건만 정해준다.

        // for(let enemy of this.enemies)                // ***** 초기 설정(랜덤 아니고 정해진 인덱스에서 출력)!!****
        //     enemy.onOutOfScreen=(en)=>{
        //         let idx = this.enemies.indexOf(en);   // en은 객체, 그것에서 index 받아서 splice이용하여 null 값 만들기
        //         this.enemies.splice(idx,1);           // 스스로 지울 수 있는 기능이 있는지 찾아보고 없으면, slice이용하기
        // };      // *** 여기서 함수(화살표 함수)가 생성되고 enemy 객체에 위임이 되고 그 함수가 호출될 때, 실행된다.(bind - this와 느낌이 비슷하다.)
        
        


        // ---------------- [ Background ] ------------------------------------------------
        
        this.bg = new Background();

        this.gameover = false;      // setTime을 위한 상태값!!****, 게임 오버는 아니고 애니메이션만 
                                    // 멈추게 해야 한다. 즉, 멈추면 상태값이 전부 날아가는 것이 
                                    // 아니라 정지인 상태이다.(배경과 적들은 움직임)

        this.pause = false;         // 그래서, 게임이 끝나는 것이 아니라 일시정지가 되어야 한다.
        this.dom.onclick = this.clickHandler.bind(this);         // **** .bind() 처리하면서 .bind(this)의 this는 이벤트 핸들러를 위한 객체를 불러온다. 원래 this는 gamecanvas이다.
        // this.dom.onclick = this.clickHandler;                       // **** .bind() 처리하면서 .bind(this)의 this는 이벤트 핸들러를 위한 객체를 불러온다. 원래 this는 gamecanvas이다.

        this.dom.onkeydown = this.keyDownHandler.bind(this);
        this.dom.onkeyup = this.keyUpHandler.bind(this);
        
        
    }                                                            
                                                                  // **** 내장 객체의 함수와 새로만들어 지는 객체의 함수를 비교할 것.
                                                                  // clickHandler는 사용자 입력에 의한 이벤트 핸들러이기 때문에
                                                                  //  window에서 가져와야 해서 .bind(this)를 뒤에 붙여준다.
    

    // 이렇게 사용하면 위의 코드와 비교해서 dom은 이전의 생성자에서 넘겨받아서 this가 dom이 아니다.
    //console.log(this)


    run(){
        //console.log(this);
        if(this.pause)
            return;

        // 60 프레임으로 화면을 다시 그리는 코드!!
        this.update();
        this.draw();
    

        window.setTimeout(()=>{                 // 알람 맞추는 것과 같다.(60 프레임에 맞추어서 = 1000/60 = 17)
            this.run();    
        },17);

        // 여기까지 하면, 무한히 반복한다. 멈추고 싶으면? 이 안에서 멈추는 도구가 필요하다. 그래서 상태변수가 필요하다.

    }

        // canvas는 UI이고 UI에 의해서 꼭두각시처럼 컨트롤된다.

    update(){   // update는 단위 계수마다 잘게 쪼개서 움직여야 한다.
                // 시간에 국한에서 잘게 짜르려면 frame을 나누어야 한다. 
                // 이동하는 속도가 일정하려면, 

        // **** [전역 객체]
        // *** [전역 객체] = [문맥 객체] = [Context 객체] : 객체 사이에서 공유할 있는 전역객체가 필요하다!
        // *** update에서 "적기"에 대해 참조하기! : 1) canvas에 대해 참조한다? : 서로 참조해서 이상하다! 
        // 2) update 인자 넘겨주는 것도 문제이다.
        // 3) 전역변수화해서 아무나 쉽게 접글 할 수 있게 하기!! ("SingleTon" 객체: 전역객체이다.)

        this.boy.update();                  // 캐릭터를 이동시키자!
        //this.bg.update();              

        for(let enemy of this.enemies)      // 적기도 이동시키자!
            enemy.update();

        
        this.enemyAppearDelay--;

        if(this.enemyAppearDelay == 0){
            
            let x = getRandomInt(-50, 50+this.dom.width); // -50 ~ this.dom.width+50
            let y = -50;
            
            let enemy = new Enemy(x, y);              // ver 2 : 
            //this.enemies.push(new Enemy(x, y));     // ver 1 : 하지만, 이렇게하면 너무 무더기로 나온다.
            
            // enemyOutOfScreenHandler를 멤버로서 넣어주기!!

            // 2. [Aggregation 관계]******
            //for(let enemy of this.enemies)           // ver 1 :
            // enemy.onRandomOutOfScreen=(en)=>{         // ver 2 :
            //     let idx = this.enemies.indexOf(enr);   // en은 객체, 그것에서 index 받아서 splice이용하여 null 값 만들기
            //     this.enemies.splice(idx,1);            // 스스로 지울 수 있는 기능이 있는지 찾아보고 없으면, slice이용하기
            // };

            
            // ** 멤버로서 인식하게 해준다!!
            enemy.onRandomOutOfScreen=this.enemyOutOfScreenHandler.bind(this);
            
            this.enemies.push(enemy);                      // ver 2 :
            this.enemyAppearDelay = getRandomInt(30, 60);  // 30프레임이라서 0.5초이다.(17ms가 60프레임, 34ms가 30프레임)
        }

        function getRandomInt(min, max) {   // mdn의 랜덤 메서드 이용
            min = Math.ceil(min);
            max = Math.floor(max);
            return Math.floor(Math.random() * (max - min)) + min; //최댓값은 제외, 최솟값은 포함
        }
    }

    draw(){

        this.bg.draw(this.ctx);             // 순서 중요!!
        this.boy.draw(this.ctx);        

        //this.bg.draw(this.ctx);

        for(let enemy of this.enemies)      // 좌표를 이동시킨 적기를 이동시킨다!
            enemy.draw(this.ctx);
    }

    pause(){
        
    }
    
    // ** 멤버로서 인식하게 해준다!!
    enemyOutOfScreenHandler(en){            
        let idx = this.enemies.indexOf(en);   // en은 객체, 그것에서 index 받아서 splice이용하여 null 값 만들기
        this.enemies.splice(idx,1);  
    }

    // ------------------------------- event handler***** --------------------------------
    // 이벤트가 발생했다!! 사용자 입력에 의해서 이벤트가 발생 // 사건의 구체적인 정보를 알아야내 한다.(어떤 위치에 마우스를 클릭했는지, 어떤 키보드를 눌렀는지)

    clickHandler(e){         // 클릭하면, 타이머가 멈춘다!!
        //this.pause = true; // 같은 함수 내부라서 함수 호출 시, 
                             // 함수를 사용하는 것이 아니라 값을 입력해준다.

        // this.boy.move(2);    // 마우스로 move하는 것은 옳지 않고 방향키로 나중에 구현할 것
                                // 마우스는 사용자 입력이다.
        //console.log(this);
        // ** 상태 변수를 이용
                                    // moveTo 메서드에서는 e.x와 e.y는 마우스 클릭할 때, 이용                        
        this.boy.moveTo(e.x,e.y);   // 이동할 때, 대각선 길이를 잘게 쪼개는데 가로 세로는 그 값이 얼마였든지간에 비율로 대각선 길이로 나눈다.
                                    // 나중에 배우자! 이벤트 함수 : screen x, event x, osell x


        // 화면 지우기 필요!
    
        this.boy.draw(this.ctx);    // 함수 호출자는 dom이다. 하지만, 우리는 this가 GameCanvas였으면 좋겠다. 서로 역할이 엇갈림.
                                    // 그래서 우리는 보따리를 쌓아 줘야한다.(위에서 bind로 묶어줌.) *******
    
    
        //console.log(this);      // 위의 dom이 앞에서 위임받아서 dom이 아니라 이전 생성자이다.
        }


    // ***** canvas나 div 태그는 키 입력을 안 받게 했다.
    keyDownHandler(e){
        
        console.log(e.key);
        switch(e.key){
            case "ArrowUp" :            // 이렇게만 만들면, 렉걸린것처럼 보인다.(한번 클릭 후 기다렸다가 여러번 클릭된다.)
                                        // 이것은 더블클릭 때문에 눌렀다가 뗄 때, 기다리는 현상이 있다.
                this.boy.move(1); 
                break;
            case "ArrowLeft" :
                this.boy.move(4); 
                break;

            case "ArrowRight" :
                this.boy.move(2); 
                break;

            case "ArrowDown" :
                this.boy.move(3); 
                break;

        }
    }

    keyUpHandler(e){
        
        console.log(e.key);             // 이렇게 해도 문제가 많다. 여러 방향키를 동시에 처리하지 못한다.(렉걸림)

        //this.boy.move(0);
        switch(e.key){
            case "ArrowUp":             // 이렇게만 만들면, 렉걸린 것처럼 보인다.(한번 클릭 후 기다렸다가 여러번 클릭된다.)
                                        // 이것은 더블클릭 때문에 눌렀다가 뗄 때, 기다리는 현상이 있다.
                this.boy.stop(1);       // 그래서, onkeyup으로 만들자!
                break;
            case "ArrowLeft":
                this.boy.stop(4); 
                break;

            case "ArrowRight":
                this.boy.stop(2); 
                break;

            case "ArrowDown":
                this.boy.stop(3); 
                break;
        }
    }         
}

// ***** export default class : 이렇게 말고 다른 방법으로 함수를 정의할 때 사용해도 된다.



  • enemy.js
export default class Enemy{
    constructor(x=0,y=0){
        this.x = x || 200;
        this.y = y || 300;
        
        this.vx = 0;
        this.vy = 0; 
        
        this.dx = 0;
        this.dy = 0;
        
        this.speed = 5;

        this.img = document.querySelector("#enemy");
        
        // this.onOutOfScreen = null;        // ***** 초기 설정 : 
        this.onRandomOutOfScreen = null;
        
        // *** 여기에 game-canvas에서 넘겨받은 화살표 함수를 담는다.

    }

    draw(ctx){
        ctx.drawImage(this.img, this.x, this.y);
    }
    
    update(){   

        // *** 삭제하는 방법 : 
        // 나를 참조하고 있느 것이 내가 갖고 있는 것이다. : 쌍방 참조
        // 즉, 트리 구조여야 한다. 체계적인 형태이다.

        // 콜 함수 : 
        // *** 콜백 함수 : 위임 함수, 나중에 호출할 때, 실행된다. 지금은 가지고 있다가(위임 함수의 의미) 나중에 함수가 호출된다. 언제 호출될 것인가가 이벤트(사건)이다.
        // *** 결론 : out of canvas라는 변수명에 가지고 있다가(on이라는 이름명) 너가 가지고 있다가 나중에 호출된다.

        // 이벤트를 설정할 수 있도록 제공해줘야 한다?
        // 결론 : enemy는 영역을 자기가 감지할 수 있지만, 영역 밖은 canvas의 값을 이용한다.


        this.y += this.speed;
        
        // ***** 초기 설정 :
        // if(this.y > 500)                        // this는 자신인데 자기가 자신의 함수(onOutofScreen)를 불러냄
        //     if(this.onOutOfScreen != null)
        //         this.onOutOfScreen(this);       // 넘겨받은 화살표 함수로부터 여기의 this가 en이 된다.

        if(this.y > 500)                        // this는 자신인데 자기가 자신의 함수(onOutofScreen)를 불러냄
            if(this.onRandomOutOfScreen != null)
                this.onRandomOutOfScreen(this);  
    }


}



3. 전역 객체, 컨텍스트 객체, 싱글톤 패턴 : [23.01.11]


1) 개념 정리

  • 객체의 트리 구조에 포함되지 않으면서!!
  • 모든 곳에서 쉽게 접근 할 수 있고!!
  • 모두 같은 객체를 이용할 수 있게 하는 방법
  • Singleton 패턴 : 개념이 중요하다. 코드는 중요하지 않다.
  • 객체지향에 대한 개념없이 제대로 만들어본 적이 없어서 대부분은 제대로 이용하지 못한다.



2) game에 적용


  • main.html
<!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>Document</title>

    <script type = "module" src="./app.js"></script>
</head>
<body>

      <canvas tabindex="0" class="game-canvas" width="640" height="800"></canvas>


</body>
</html>


  • app.js
import GameCanvas from './panel/game-canvas.js';        // *** default(GameCanvas) 이름을 마음대로 상용할 수 있다!!
import newlec from './newlec.js';        // *** default(GameCanvas) 이름을 마음대로 상용할 수 있다!!

window.addEventListener("load", function(){

    var gameCanvas = new GameCanvas();  // 다른 파일을 가져오는 방식이 자바스크립트에서는 없다.
                                        // 그래서 사용할 모듈을 html에게 요청한다.


                                        // 에러 : GameCanvas 객체를 생성시, 참조는 소문자에 넣어준다. 
    gameCanvas.run(); 
        
    // run()은 무한 loop해야 한다. UI 스레드는 별도의 흐름을 갖게 해야 한다.      
    // vscode에선 F12로 함수 선언과 이동 가능


    newlec.x++;
    console.log("x:", newlec.x);        // 2가 출력

 
});




  • newlec.js

// 1. 이런 방식이 아니라
// export default class Exam
// {
//     x: 1
// }

// 2. 이런 방식으로 전역 객체 사용(클래스가 아니며, 객체 그 자체이다.)
// export default 
// {
//     x: 1
// }

// 3. 객체를 이용하는 것이 편하므로 하나의 class를 정해서 시작하자!

class Context{
    #enemies;   // private도 가능!? 하지만, 정의도 여기서해주고 사용도 여기서 해야 한다!

    constructor(){
        this.#enemies = null;
    }

    set enemies(value){                 // getter, setter 해결
        this.#enemies = value;
    }

    get enemies(){
        return this.#enemies;
    }

}

export default new Context();


  • gamecanvas.js
// game-canvs.js는 사용자 입력과 출력을 담당한다.(중요!!) *****

import Boy from '../item/boy.js';
import Background from '../item/background.js';
import Enemy from '../item/enemy.js';
import Explosion from '../item/explosion.js';
import newlec from '../newlec.js';        // *** default(GameCanvas) 이름을 마음대로 상용할 수 있다!!


// **** import 된것을 새롭게 뱉어내는 방식!!
export default class GameCanvas {        // 클래스로 변경  

    constructor(){                                          // **** 중요한 개념 : 
        this.dom = document.querySelector(".game-canvas");  // 여기서 this는 GameCanvas의 new된 결과이다.
                                                            // dom이 canvas이다. canvas가 this의 멤버로 들어온 것이다.
                                                            // 원래 java에서는 canvas가 canvas를 갖는 것이 이상하다. 
                                                            // 그래서, JS는 틀로 쓸 수 있는 것이 아니다.(프레임워크가 아니였다.)

                                                            // 앞으로는 this의 dom 기능을 이용한다.

        this.dom.focus();     // focus를 잡아주면 canvas에서 키보드 입력을 할 수 있다. **** 즉, 우선순위를 마우스에서 키보드로 넘겨준다.
                              // 키보드로 무브하는 방법???? 
    
        /** @type {CanvasRenderingContext2D} */ 
        
        this.ctx = this.dom.getContext('2d');   // var ctx가 아니라 this.ctx이다.(Boy의 ctx이므로)         
        


        // ----------------- [ Boy ] ----------------------------------------
   
        this.boy = new Boy(100, 100);

        this.boy.speed++;                       // get을 사용하지 않고도 speed로 바로 getSpeed 처럼 사용할 수 있다.
        console.log("speed:" + this.boy.speed);


        // ------------- [ 전역 객체!!! :  Singleton 패턴 ] ------------

        newlec.x++;                     // 위에서 new를 하지 않아서 기존의 객체를 가져다가 사용한다.(전역 공간!) 
        console.log("x:", newlec.x);    // 3이 출력

        
        // ---------------- [ Enemy ] ----------------------------------------------------
        
        this.enemy = [];    // Aggregation 방식!!

        this.enemyAppearDelay = 60;     // 적의 딜레이 만들기!(60프레임 기준 : 1초)
                                        // 17ms가 60프레임, 34ms가 30프레임


        // 이런식으로 적기를 만들 때, 콜백함수를 줄 수 있거나 따로 줄 수 도 있다.

        // *** 1번 Aggregation 관계에 사용
        this.enemies = [];

        // *** 2번 Composition 관계에 사용
        // this.enemies = [new Enemy(10,0), new Enemy(30,0), new Enemy(50,0), new Enemy(100,0),
        //     new Enemy(40,20), new Enemy(50,20), new Enemy(130,45)];



        // **** 다른 애가 불러내서 위임되는 함수라고 부르며 "call back function"이라고 부른다. 
        // 사용자가 클릭해달라고 위임했기 때문이다. call back function은 지금 실행하는 것이 아니라 나중에 실행된다.(위임해야지 나중에 전화할 수 있기 때문이다.(나중에 실행))
        // 전화하는 애가 주체가 된다. 
        // dom이 다시 호출해준다? 
        // this.dom.onclick = this.clickHandler;                   // ()처럼 함수를 호출한 결과 값 넣지 말기
        

        // 1. [Composition 관계]******
        // *** this를 바운더리 밖의 this를 이용하고 싶어서 람다함수를 이용한다.(bind를 안쓰려고!!)

        // *** 결론 : 객체를 new로 생성하면서 생성자에 바로 넘겨준다.
        // *** 결론 : 캔바스(객체 배열을 갖고 있는 것은 "game-canvas"라서 여기서 지운다.)에서 지우는 역할,
        // ***** enemy.js 객체(조건을 계속 감지)에서는 조건만 정해준다.

        // for(let enemy of this.enemies)                // ***** 초기 설정(랜덤 아니고 정해진 인덱스에서 출력)!!****
        //     enemy.onOutOfScreen=(en)=>{
        //         let idx = this.enemies.indexOf(en);   // en은 객체, 그것에서 index 받아서 splice이용하여 null 값 만들기
        //         this.enemies.splice(idx,1);           // 스스로 지울 수 있는 기능이 있는지 찾아보고 없으면, slice이용하기
        // };      // *** 여기서 함수(화살표 함수)가 생성되고 enemy 객체에 위임이 되고 그 함수가 호출될 때, 실행된다.(bind - this와 느낌이 비슷하다.)
        
        


        // ---------------- [ Background ] ------------------------------------------------
        
        this.bg = new Background();

        // --------- 전역객체-----------
        newlec.enemies = this.enemies;  // newlec의 enemies 던 canvas의 enemies던 같이 사용 가능!

        // ---------------- [ Eplosion ] ----------------------------------------------------
        this.explosion = new Explosion();

        this.gameover = false;      // setTime을 위한 상태값!!****, 게임 오버는 아니고 애니메이션만 
                                    // 멈추게 해야 한다. 즉, 멈추면 상태값이 전부 날아가는 것이 
                                    // 아니라 정지인 상태이다.(배경과 적들은 움직임)

        this.pause = false;         // 그래서, 게임이 끝나는 것이 아니라 일시정지가 되어야 한다.
        this.dom.onclick = this.clickHandler.bind(this);         // **** .bind() 처리하면서 .bind(this)의 this는 이벤트 핸들러를 위한 객체를 불러온다. 원래 this는 gamecanvas이다.
        // this.dom.onclick = this.clickHandler;                       // **** .bind() 처리하면서 .bind(this)의 this는 이벤트 핸들러를 위한 객체를 불러온다. 원래 this는 gamecanvas이다.

        this.dom.onkeydown = this.keyDownHandler.bind(this);
        this.dom.onkeyup = this.keyUpHandler.bind(this);
        
        
    }                                                            
                                                                  // **** 내장 객체의 함수와 새로만들어 지는 객체의 함수를 비교할 것.
                                                                  // clickHandler는 사용자 입력에 의한 이벤트 핸들러이기 때문에
                                                                  //  window에서 가져와야 해서 .bind(this)를 뒤에 붙여준다.
    

    // 이렇게 사용하면 위의 코드와 비교해서 dom은 이전의 생성자에서 넘겨받아서 this가 dom이 아니다.
    //console.log(this)


    run(){
        //console.log(this);
        if(this.pause)
            return;

        // 60 프레임으로 화면을 다시 그리는 코드!!
        this.update();
        this.draw();
    

        window.setTimeout(()=>{                 // 알람 맞추는 것과 같다.(60 프레임에 맞추어서 = 1000/60 = 17)
            this.run();    
        },17);

        // 여기까지 하면, 무한히 반복한다. 멈추고 싶으면? 이 안에서 멈추는 도구가 필요하다. 그래서 상태변수가 필요하다.

    }

        // canvas는 UI이고 UI에 의해서 꼭두각시처럼 컨트롤된다.

    update(){   // update는 단위 계수마다 잘게 쪼개서 움직여야 한다.
                // 시간에 국한에서 잘게 짜르려면 frame을 나누어야 한다. 
                // 이동하는 속도가 일정하려면, 

        // **** [전역 객체]
        // *** [전역 객체] = [문맥 객체] = [Context 객체] : 객체 사이에서 공유할 있는 전역객체가 필요하다!
        // *** update에서 "적기"에 대해 참조하기! : 1) canvas에 대해 참조한다? : 서로 참조해서 이상하다! 
        // 2) update 인자 넘겨주는 것도 문제이다.
        // 3) 전역변수화해서 아무나 쉽게 접글 할 수 있게 하기!! ("SingleTon" 객체: 전역객체이다.)

        this.boy.update();                  // 캐릭터를 이동시키자!
        //this.bg.update();              

        for(let enemy of this.enemies)      // 적기도 이동시키자!
            enemy.update();

        
        this.enemyAppearDelay--;

        if(this.enemyAppearDelay == 0){
            
            let x = getRandomInt(-50, 50+this.dom.width); // -50 ~ this.dom.width+50
            let y = -50;
            
            let enemy = new Enemy(x, y);              // ver 2 : 
            //this.enemies.push(new Enemy(x, y));     // ver 1 : 하지만, 이렇게하면 너무 무더기로 나온다.
            
            // enemyOutOfScreenHandler를 멤버로서 넣어주기!!

            // 2. [Aggregation 관계]******
            //for(let enemy of this.enemies)           // ver 1 :
            // enemy.onRandomOutOfScreen=(en)=>{         // ver 2 :
            //     let idx = this.enemies.indexOf(enr);   // en은 객체, 그것에서 index 받아서 splice이용하여 null 값 만들기
            //     this.enemies.splice(idx,1);            // 스스로 지울 수 있는 기능이 있는지 찾아보고 없으면, slice이용하기
            // };

            
            // ** 멤버로서 인식하게 해준다!!
            enemy.onRandomOutOfScreen=this.enemyOutOfScreenHandler.bind(this);
            
            this.enemies.push(enemy);                      // ver 2 :
            this.enemyAppearDelay = getRandomInt(30, 60);  // 30프레임이라서 0.5초이다.(17ms가 60프레임, 34ms가 30프레임)
        }

        function getRandomInt(min, max) {   // mdn의 랜덤 메서드 이용
            min = Math.ceil(min);
            max = Math.floor(max);
            return Math.floor(Math.random() * (max - min)) + min; //최댓값은 제외, 최솟값은 포함
        }
    }

    draw(){

        this.bg.draw(this.ctx);             // 순서 중요!!
        this.boy.draw(this.ctx);   
        this.explosion.draw(this.ctx);       

        //this.bg.draw(this.ctx);

        for(let enemy of this.enemies)      // 좌표를 이동시킨 적기를 이동시킨다!
            enemy.draw(this.ctx);
    }

    pause(){
        
    }
    
    // ** 멤버로서 인식하게 해준다!!
    enemyOutOfScreenHandler(en){            
        let idx = this.enemies.indexOf(en);   // en은 객체, 그것에서 index 받아서 splice이용하여 null 값 만들기
        this.enemies.splice(idx,1);  
    }

    // ------------------------------- event handler***** --------------------------------
    // 이벤트가 발생했다!! 사용자 입력에 의해서 이벤트가 발생 // 사건의 구체적인 정보를 알아야내 한다.(어떤 위치에 마우스를 클릭했는지, 어떤 키보드를 눌렀는지)

    clickHandler(e){         // 클릭하면, 타이머가 멈춘다!!
        //this.pause = true; // 같은 함수 내부라서 함수 호출 시, 
                             // 함수를 사용하는 것이 아니라 값을 입력해준다.

        // this.boy.move(2);    // 마우스로 move하는 것은 옳지 않고 방향키로 나중에 구현할 것
                                // 마우스는 사용자 입력이다.
        //console.log(this);
        // ** 상태 변수를 이용
                                    // moveTo 메서드에서는 e.x와 e.y는 마우스 클릭할 때, 이용                        
        this.boy.moveTo(e.x,e.y);   // 이동할 때, 대각선 길이를 잘게 쪼개는데 가로 세로는 그 값이 얼마였든지간에 비율로 대각선 길이로 나눈다.
                                    // 나중에 배우자! 이벤트 함수 : screen x, event x, osell x


        // 화면 지우기 필요!
    
        this.boy.draw(this.ctx);    // 함수 호출자는 dom이다. 하지만, 우리는 this가 GameCanvas였으면 좋겠다. 서로 역할이 엇갈림.
                                    // 그래서 우리는 보따리를 쌓아 줘야한다.(위에서 bind로 묶어줌.) *******
    
    
        //console.log(this);      // 위의 dom이 앞에서 위임받아서 dom이 아니라 이전 생성자이다.
        }


    // ***** canvas나 div 태그는 키 입력을 안 받게 했다.
    keyDownHandler(e){
        
        console.log(e.key);
        switch(e.key){
            case "ArrowUp" :            // 이렇게만 만들면, 렉걸린것처럼 보인다.(한번 클릭 후 기다렸다가 여러번 클릭된다.)
                                        // 이것은 더블클릭 때문에 눌렀다가 뗄 때, 기다리는 현상이 있다.
                this.boy.move(1); 
                break;
            case "ArrowLeft" :
                this.boy.move(4); 
                break;

            case "ArrowRight" :
                this.boy.move(2); 
                break;

            case "ArrowDown" :
                this.boy.move(3); 
                break;

        }
    }

    keyUpHandler(e){
        
        console.log(e.key);             // 이렇게 해도 문제가 많다. 여러 방향키를 동시에 처리하지 못한다.(렉걸림)

        //this.boy.move(0);
        switch(e.key){
            case "ArrowUp":             // 이렇게만 만들면, 렉걸린 것처럼 보인다.(한번 클릭 후 기다렸다가 여러번 클릭된다.)
                                        // 이것은 더블클릭 때문에 눌렀다가 뗄 때, 기다리는 현상이 있다.
                this.boy.stop(1);       // 그래서, onkeyup으로 만들자!
                break;
            case "ArrowLeft":
                this.boy.stop(4); 
                break;

            case "ArrowRight":
                this.boy.stop(2); 
                break;

            case "ArrowDown":
                this.boy.stop(3); 
                break;

        }
    }

            
}


// ***** export default class : 이렇게 말고 다른 방법으로 함수를 정의할 때 사용해도 된다.


  • boys.js
// 원래 웹에서는 파일명을 가장 앞에는 소문자로 구분하고 대쉬(-)로 구분한다

// 중요!!!!**** 
// 캡슐화 vs 캡슐(데이터를 구조화한 것) 
// 캡슐화란 - 책임을 부여하는 것, 역할을 나누는 것, 기능을 부여하는 것
// 객체지향은 그림을 그릴 수 밖에 없고, 그 그림은 설계가 된다.

import newlec from '../newlec.js';

export default class Boy {                     // *******

    constructor(x,y){
        this.x = x || 200;      // 6. 오버로드 생성자로 사용자가 넘겨줄 값이 있을 때, 가능하게 하려고 이렇게도 가능하다.
        this.y = y || 100;      // 사용자 입력이 없을 때는, x와 y는 || 뒤의 기본값이 들어간다.
        
        this.vx = 0;            // 단위 위치(잘게 조개진 값)
        this.vy = 0;            
        
        this.dx = 0;            // 목적지 위치
        this.dy = 0;     
        
        this.dir = 0;


        var moveLeft = false;   // 처음에 FALSE로 설정
        var moveRight = false;
        var moveUp = false;
        var moveDown = false;

        this.speed = 3;         // JS : 속성, , 이벤트

        this.img = document.querySelector("#boy");          // html에서 이미지를 이렇게 가져온다.(이젠, 생성자에서 그리기 위해서!) *************

        this.ix = 1;                                        // ix, iy는 스프라이트 사진에서 부분을 꺼낼수 있게 배열의 인덱스 값이다!
        this.iy = 2;
    
        this.walkDelay = 20;            // 프레임 낮춰주기!!


        // *** 중요!!
        this.sw = this.img.width/3;     //106;
        this.sh = this.img.height/4;    //148.25;


        this.sx = this.sw * this.ix;
        this.sy = this.sh * this.iy;    // 메인 스레드는 절차를 담당하는 기본 흐름이다. setTimeout인 보조 흐름이며 추가적으로 작동을 만든 것이 게임 스레드이다.

       
    }                                   // 이것의 차이를 알기!!****
                                        // update와 draw는 메인 스레드(게임 스레드)이다.(사용자 입력이 없더라도, 내가 호출하는 것이 아니라 초당 60번씩 계속 돌고 있다.)
                                        // 나머지는 UI 스레드이다.(사용자 입력이 있을 때만, 한 번만 움직임.)

    // set speed(value){
    //     this.#speed = value;             // *** JS에서 getter, setter를 이렇게 사용할 수 있다.
    // }

    // get speed(){
    //     return this.#speed;
    // }


    draw(ctx){

        this.sx = this.sw * this.ix;
        this.sy = this.sh * this.iy;
        ctx.drawImage(this.img, this.sx, this.sy, this.sw, this.sh,     // **** 에러발생 : img는 생성자에서 가져오기 때문에 this.img 이다!!
            this.x-this.sw/2, this.y-this.sh+15, this.sw, this.sh);     // 발이 기준이 되게 값을 조정해주자!! 
        
        // var img = new Image();	
        // img.src = './image/boy.png';    // 이미지도 생성자에서 생성하자!!*****

        // img.onload = function(){    // 이렇게 만들면 에러가 나는 이유?

        //                             // sx는 source(원본의 x크기), dx(display의 x크기)
          
        // }.bind(this);               // 여기서는 Boy에서 img로 이벤트가 위임 되었다!!(중요)
        
        // 개념적인 부분 : 위임? 다시 이벤트 위임 공부, Function.prototype.call()은 모든 함수에서 call이 있어야 한다.
        
        // 중요!! 함수가 호출한 주체가 this가 된다. apply, call
        //console.log(this);
        ctx.beginPath();
        ctx.arc(this.x, this.y, this.sw/2, 0, 2 * Math.PI);
        ctx.stroke();


    }

    
    
    update(){       // ** update는 상태값만 변경해줘야 한다.

        for(let enemy of newlec.enemies) {          // 역할을 누구한테 줄지 생각하기!!

            //  *** 보정된 enemy 객체의 좌표 **** : 쌤 작성
            let ex = enemy.x;
            let ey = enemy.y;

            let x = this.x;         //  원래 boy는 중앙을 잡고 조정해서 그린 것이 여서 건드리지 말자!
            let y = this.y;

            // Math 함수 안에 Math 함수가 중첩이 불가능하다!
            let d = Math.sqrt((ex-x)*(ex-x)+(ey-y)*(ey-y));
            let r1r2 = this.sw/2 + enemy.width/2;               // boy와 enemy의 반지름의 합
           
            if(d <= r1r2)
                console.log("충돌");

            //  *** 보정된 enemy 객체의 좌표 **** : 내가 작성
            // let x = enemy.centerX;    // 원래 x는 public이라서 private 해줘야하지만 귀찮..
            // let y = enemy.centerY;    // 원래 y도 public이라서 private 해줘야하지만 귀찮..

            // // 하지만, boy 이미지의 좌표는 발밑에 있다.
            
            // let d = Math.sqrt(Math.pow(x-this.x,2)+Math.pow(y-this.y,2));
            // let r1r2 = 30; 
            // Math.sqrt(x*x+y*y)+ Math.sqrt(this.x*this.x + this.y*this.y);


            // *** 1. 도형을 그리는 방법 : begin은 항상 필요한다.(그냥 함수가 rect나 arc만 해주는 경우)
            // *** 2. 도형을 그리는 방법 : begin이 합쳐진 함수인 fillRect는 begin이 필요없다.
        }

        console.log(newlec.enemies.length);     // ** 왜 enemy인데 boy에서 만들었는가? enemy에서도 이용가능!
        
        // ======================================
        
        // --- 이동을 위한 코드 -------

        if(this.moveUp)                     // 이런식으로 구현하면 여덟 방향의 이동도 가능하다.
            this.y -= this.speed;          // 방향을 이거로만 업데이트!!
        
        if(this.moveDown)
            this.y += this.speed;          // 또한, #을 붙이면, private 변수로 바뀌고 이것은 java처럼 getter, setter처럼 이용하자!
        
        if(this.moveRight)                  // getter, setter를 이용할 수 있는 색다른 방법? : C#에서 이용한다!
            this.x += this.speed;
            
        if(this.moveLeft)
            this.x -= this.speed;





        // switch(this.dir){          // 코드의 순서 주의!!(아래 코드에서 return 때문에 문제가 발생했다.)
        //     case 1: // 북쪽        // 하지만 이렇게 하면, 캐릭터가 멈추지 않아서 키보드 뗄 때, 멈추도록하자!     
        //         this.y -= 1;      
        //     break;                   // 브라우저 기준으로 시작을 생각해보면 된다.
    
        //     case 2: // 동쪽
        //         this.x += 1;
        //     break;
            
        //     case 3: // 남쪽
        //         this.y += 1;
        //     break;
            
        //     case 4: // 서쪽
        //         this.x -= 1;
        //     break;
            
        // }

        // setInterval(draw, 10);
        
        //this.draw().drawImage(this.img, this.sx*this.ix, this.sy*this.iy);
        
        // 조건 설정!! dx가 중심점, dy가 중심점이라고 가정하에 조건문 설정!
        // 정확히는 dx가 기준점이 되고 x가 움직이는 변수이다.
        //  x축 따로, y축 따로 구성해서 합쳐서 구현!!
        
        if(this.dx - 1 < this.x  && this.x < this.dx + 1 ||  this.dy - 1 < this.y && this.y < this.dy + 1){           
            this.vx = 0;
            this.vy = 0;
            
            this.ix = 1;                         // 멈출 때만 상태값을 바꿔줘도 되는가?***
            
        }
        
        if(!(this.moveLeft || this.moveRight || this.moveUp || this.moveDown || false))     // 방향키가 눌린게 없다면? 다음 조건문을 진행!
            if(this.vx == 0 && this.vy == 0 ) {                                             // 멈출 때 조건 설정 : vx 설정 (이것도 되긴 된다.)
                this.ix = 1;                                                                // 멈췄는지 안멈췄는지는 vx의 유무가 중요하다!!
                return;
            }                                                                               // 여기가 정확히 캐릭터가 멈추게 해주는 역할이다!!!!*******
                                                                                            // 그래서 여기서, 발바꿈을 해준다.
                                                                                            // *** 숙제!! : 캐릭터 화면 전환하기
        // ======================================                                           // *** 추가로 할 것 : 발속도 올리기 : speed라는 변수를 둬야 한다.
        
        this.x += this.vx;                  // 여기서 목적직 위치까지 단위 위치(벡터)로 더하면서 업데이트 시킴
        this.y += this.vy;

        // 걸음을 걷는 효과!
        this.walkDelay--;
        //this.ix=this.ix==2?0:2;   // 3항 연산 중요!!!
        if(this.walkDelay == 0){
            if(this.ix == 0){
                this.ix = 2;
                
            }
            // else if(this.ix == 1){
            //     this.ix = 0;
                
            // }
            else {
                this.ix = 0;
                
            }
            this.walkDelay = 20;
        }


        // ------------------- 이동을 위한 로직 ------------
    }

    moveTo(dx,dy){
        
        let w = dx - this.x;                // 목적지 위치 - 현재 위치(업데이트되고나서) = 남은 위치 거리 계산 
        let h = dy - this.y;

        let d = Math.sqrt(w*w+h*h);         // 대각선 길이 공식

        this.vx = w / d;                    // 여기 this도 주의해서 적기, 그 남은 위치 거리를 다시 잘게 쪼개진 비율로 나누어서 이동한다.
        this.vy = h / d;
        
        this.dx = dx;
        this.dy = dy;
    }

    move(dir){

        switch(dir){               // 코드의 순서 주의!!(아래 코드에서 return 때문에 문제가 발생했다.)
            case 1: // 북쪽        // 하지만 이렇게 하면, 캐릭터가 멈추지 않아서 키보드 뗄 때, 멈추도록하자!    
                this.moveUp = true;
                this.iy= 0;
                break;
                
            case 3: // 남쪽        // ** 결론 : 자연스럽게 방향키 이동을 위해서, 동서남북의 4방향 모두 각각 상태 변수로 컨트롤 되어야 한다!!!!
                this.moveDown = true;
                this.iy= 2;
                break;
    
            case 2: // 동쪽
                this.moveRight = true;
                this.iy= 1;
                break;

            case 4: // 서쪽
                this.moveLeft = true;
                this.iy= 3;
                break;

            default:
                console.log(this.dir);
            
        }

    }

    stop(dir){

        switch(dir){               // 코드의 순서 주의!!(아래 코드에서 return 때문에 문제가 발생했다.)
            case 1: // 북쪽        // 하지만 이렇게 하면, 캐릭터가 멈추지 않아서 키보드 뗄 때, 멈추도록하자!    
                this.moveUp = false;
                break;

            case 3: // 남쪽        // ** 결론 : 자연스럽게 방향키 이동을 위해서, 동서남북의 4방향 모두 각각 상태 변수로 컨트롤 되어야 한다!!!!
                this.moveDown = false;
                break;
    
            case 2: // 동쪽
                this.moveRight = false;
                break;

            case 4: // 서쪽
                this.moveLeft = false;
                break;

            default:
                console.log(this.dir);
 
        }

    }
}



  • enemy.js
//import newlec from '../newlec.js';

import Explosion from "./explosion.js";

export default class Enemy{
    constructor(x=0,y=0){
        this.x = x || 200;
        this.y = y || 300;
        
        this.vx = 0;
        this.vy = 0; 
        
        this.dx = 0;
        this.dy = 0;
        
        this.speed = 5;

        this.img = document.querySelector("#enemy");


        // ------------------- [Explosion] ------------------------
        // *** 터지는 이미지에 대해서는 행동이 없어서! 딱히 책임을 줄 필요가 없다!!(굳이 그렇게 할 필요가 없다.)
        // 하지만, 터지는 것이 여러 직군에서 터지면 캡슐화로 만들 수도 있다.

        this.imgExpl = document.querySelector("#explosion");

        this.eix=0;
        this.eiy=0;

        this.esw= this.imgExpl.width/4;
        this.esh= this.imgExpl.width/5;
        
        // this.onOutOfScreen = null;        // ***** 초기 설정 : 
        this.onRandomOutOfScreen = null;
        
        // *** 여기에 game-canvas에서 넘겨받은 화살표 함수를 담는다.


        console.log(this.img.width);

    }

    // 하지만, 객체 지향에서는 책임에 대한 생각을 해야 한다!!!(누가 누구때문에 파괴되는지!)
    // x가 정해지면 자동으로 좌표가 정해져서 get만 설정!(set을 할 필요가 없다.)
    get centerX(){
        return this.x + this.img.width/2;           // 적기의 좌표 조정!(원래 이미지 하나당 왼쪽위를 기준이여서 그렇다!)
    }

    get centerY(){
        return this.y + this.img.height/2;
    }

    get width(){
        return this.img.width;
    }

    draw(ctx){

        // 적기****
        ctx.drawImage(this.img, this.x, this.y);

        this.esx = this.esw * this.eix;
        this.esy = this.esh * this.eiy;    

        // explosion 이미지****
        ctx.drawImage(this.imgExpl, this.esx, this.esy, this.esw, this.esh,     // **** 에러발생 : img는 생성자에서 가져오기 때문에 this.img 이다!!
        this.x-this.esw/2+25, this.y-this.esh/2+30, this.esw, this.esh);     

        // 적기의 범위****
        ctx.beginPath();                // 범위 그려주기
        ctx.arc(this.x+25, this.y+30, this.width/2, 0, 2 * Math.PI);
        ctx.stroke();

        //ctx.Explosion.draw();
    }
    
    update(){   
        //console.log(newlec.enemies.length);

        // *** 삭제하는 방법 : 
        // 나를 참조하고 있느 것이 내가 갖고 있는 것이다. : 쌍방 참조
        // 즉, 트리 구조여야 한다. 체계적인 형태이다.

        // 콜 함수 : 
        // *** 콜백 함수 : 위임 함수, 나중에 호출할 때, 실행된다. 지금은 가지고 있다가(위임 함수의 의미) 나중에 함수가 호출된다. 언제 호출될 것인가가 이벤트(사건)이다.
        // *** 결론 : out of canvas라는 변수명에 가지고 있다가(on이라는 이름명) 너가 가지고 있다가 나중에 호출된다.

        // 이벤트를 설정할 수 있도록 제공해줘야 한다?
        // 결론 : enemy는 영역을 자기가 감지할 수 있지만, 영역 밖은 canvas의 값을 이용한다.


        this.y += this.speed;
        
        // ***** 초기 설정 :
        // if(this.y > 500)                        // this는 자신인데 자기가 자신의 함수(onOutofScreen)를 불러냄
        //     if(this.onOutOfScreen != null)
        //         this.onOutOfScreen(this);       // 넘겨받은 화살표 함수로부터 여기의 this가 en이 된다.

        if(this.y > 500)                        // this는 자신인데 자기가 자신의 함수(onOutofScreen)를 불러냄
            if(this.onRandomOutOfScreen != null)
                this.onRandomOutOfScreen(this);  
    }
}


  • explosion.js
export default class Explosion{
    constructor(x,y){
        this.x = x || 300;
        this.y = y || 300;

        this.ix = 0;
        this.iy = 0;

        this.img = document.querySelector("#explosion"); 
        
        this.sw = this.img.width/4;     
        this.sh = this.img.height/5;    


        this.sx = this.sw * this.ix;
        this.sy = this.sh * this.iy;    

    }

    draw(ctx){
        ctx.drawImage(this.img, this.sx, this.sy, this.sw, this.sh,     // **** 에러발생 : img는 생성자에서 가져오기 때문에 this.img 이다!!
        this.x, this.y, this.sw, this.sh);     // 발이 기준이 되게 값을 조정해주자!! 
    }

    
}

4. [23.01.12]


1) main.html

<!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>Document</title>

    <!-- 순서가 중요하다.**** --> <!-- 새로운 페이지 등록 , url 연결 시켜주기! -->
 
    <!-- <script src="./item/background.js"></script>    
    <script src="./item/boy.js"></script>
    <script src="./item/enemy.js"></script>
    <script src="./panel/game-canvas.js"></script>   -->
    <script type = "module" src="./app.js"></script>
    
    <!-- <script src="ex1-es6-var.js"></script> -->
    <!-- <script type="module" src="./ex2-es6-module.js"></script> -->
 
    <!-- 문서가  읽혀지고 읽어지는 방법 : html, js에서 해결 가능(document.addEventListener 이용) -->
    
</head>
<body>
      <!-- 주의사항 : css를 키우면 배율이 커진다. 그래서 속성을 키워야 한다. , canvas를 키워야 한다.
           보통의 경우 속성을 키우고 css로 줄이는 경우는 이미지가 깨지는것을 방지해준다.  -->

      <input tabindex="2">
      <img>
      <div tabindex="1">            <!-- tabindex를 넣어야지 tab으로 이동할  있다. -->
            test                    <!-- 마우스가 기본이라서 마우스가 우선 순위라서 사용자가 마우스를 클릭하고나서 키의 입력을 받을  있다. -->
      </div>                        <!-- 그래서 키보드 입력하기 위해서 tabindex를 설정한다!! -->
      <input tabindex="0">
      <!-- style="display: none;" style 안보이지만 메모리에 올라가 있다 , dom에 올라가 있따. -->
      <img src="./image/boy.png" id ="boy" style="display: none;">
      <img src="./image/explosion.jpg" id ="explosion" style="display: none;">
      <img src="./image/enemy.png" id ="enemy" style="display: none;">
      <img src="./image/map.png" id ="bg" style="display: none;">
      <canvas tabindex="0" class="game-canvas" width="900" height="1000"></canvas>
      <canvas tabindex="1" class="rank-canvas" width="900" height="1000"></canvas>

</body>
</html>


2) app.js

import GameCanvas from './panel/game-canvas.js';
import newlec from './newlec.js';
import RankCanvas from './panel/rank-canvas.js';

window.addEventListener("load", function(){

    var gameCanvas = new GameCanvas();  // 다른 파일을 가져오는 방식이 자바스크립트에서는 없다.
                                        // 그래서 사용할 모듈을 html에게 요청한다.


                                        // 에러 : GameCanvas 객체를 생성시, 참조는 소문자에 넣어준다. 
    gameCanvas.run(); 


    // ------------------------------
    var rankCanvas = new RankCanvas();
    rankCanvas.run();
    
    // run()은 무한 loop해야 한다. UI 스레드는 별도의 흐름을 갖게 해야 한다.      
    // vscode에선 F12로 함수 선언과 이동 가능

    newlec.x++;
    console.log("x:", newlec.x);        // 2가 출력

 
});




3) game-canvas.js

// game-canvs.js는 사용자 입력과 출력을 담당한다.(중요!!) *****


import Boy from '../item/boy.js';
import Background from '../item/background.js';
import Enemy from '../item/enemy.js';
import Explosion from '../item/explosion.js';
import newlec from '../newlec.js';        // *** default(GameCanvas) 이름을 마음대로 상용할 수 있다!!
import ConfirmDlg from '../item/confirmdlg.js';


// **** import 된것을 새롭게 뱉어내는 방식!!
export default class GameCanvas {        // 클래스로 변경  

    constructor(){                                          // **** 중요한 개념 : 
        this.dom = document.querySelector(".game-canvas");  // 여기서 this는 GameCanvas의 new된 결과이다.
                                                            // dom이 canvas이다. canvas가 this의 멤버로 들어온 것이다.
                                                            // 원래 java에서는 canvas가 canvas를 갖는 것이 이상하다. 
                                                            // 그래서, JS는 틀로 쓸 수 있는 것이 아니다.(프레임워크가 아니였다.)

                                                            // 앞으로는 this의 dom 기능을 이용한다.

        this.dom.focus();     // focus를 잡아주면 canvas에서 키보드 입력을 할 수 있다. **** 즉, 우선순위를 마우스에서 키보드로 넘겨준다.
                              // 키보드로 무브하는 방법???? 
    
        /** @type {CanvasRenderingContext2D} */ 
        
        this.ctx = this.dom.getContext('2d');   // var ctx가 아니라 this.ctx이다.(Boy의 ctx이므로)         
        


        // ----------------- [ Boy ] ----------------------------------------
   
        this.boy = new Boy(100, 100);

        // ----------- boyNoLifeHandler -----------
        this.boy.onNoLife = this.boyNoLifeHandler.bind(this);

        this.boy.speed++;                       // get을 사용하지 않고도 speed로 바로 getSpeed 처럼 사용할 수 있다.
        console.log("speed:" + this.boy.speed);


        // ------------- [ 전역 객체!!! :  Singleton 패턴 ] ------------

        newlec.x++;                     // 위에서 new를 하지 않아서 기존의 객체를 가져다가 사용한다.(전역 공간!) 
        console.log("x:", newlec.x);    // 3이 출력

        
        // ---------------- [ Enemy ] ----------------------------------------------------
        
        this.enemy = [];    // Aggregation 방식!!

        this.enemyAppearDelay = 60;     // 적의 딜레이 만들기!(60프레임 기준 : 1초)
                                        // 17ms가 60프레임, 34ms가 30프레임


        // 이런식으로 적기를 만들 때, 콜백함수를 줄 수 있거나 따로 줄 수 도 있다.

        // *** 1번 Aggregation 관계에 사용
        this.enemies = [];

        // *** 2번 Composition 관계에 사용
        // this.enemies = [new Enemy(10,0), new Enemy(30,0), new Enemy(50,0), new Enemy(100,0),
        //     new Enemy(40,20), new Enemy(50,20), new Enemy(130,45)];



        // **** 다른 애가 불러내서 위임되는 함수라고 부르며 "call back function"이라고 부른다. 
        // 사용자가 클릭해달라고 위임했기 때문이다. call back function은 지금 실행하는 것이 아니라 나중에 실행된다.(위임해야지 나중에 전화할 수 있기 때문이다.(나중에 실행))
        // 전화하는 애가 주체가 된다. 
        // dom이 다시 호출해준다? 
        // this.dom.onclick = this.clickHandler;                   // ()처럼 함수를 호출한 결과 값 넣지 말기
        

        // 1. [Composition 관계]******
        // *** this를 바운더리 밖의 this를 이용하고 싶어서 람다함수를 이용한다.(bind를 안쓰려고!!)

        // *** 결론 : 객체를 new로 생성하면서 생성자에 바로 넘겨준다.
        // *** 결론 : 캔바스(객체 배열을 갖고 있는 것은 "game-canvas"라서 여기서 지운다.)에서 지우는 역할,
        // ***** enemy.js 객체(조건을 계속 감지)에서는 조건만 정해준다.

        // for(let enemy of this.enemies)                // ***** 초기 설정(랜덤 아니고 정해진 인덱스에서 출력)!!****
        //     enemy.onOutOfScreen=(en)=>{
        //         let idx = this.enemies.indexOf(en);   // en은 객체, 그것에서 index 받아서 splice이용하여 null 값 만들기
        //         this.enemies.splice(idx,1);           // 스스로 지울 수 있는 기능이 있는지 찾아보고 없으면, slice이용하기
        // };      // *** 여기서 함수(화살표 함수)가 생성되고 enemy 객체에 위임이 되고 그 함수가 호출될 때, 실행된다.(bind - this와 느낌이 비슷하다.)
        
        

        // ---------------- [ Background ] ------------------------------------------------
        
        this.bg = new Background();

        // --------- 전역객체-----------
        newlec.enemies = this.enemies;  // newlec의 enemies 던 canvas의 enemies던 같이 사용 가능!

        // ---------------- [ Eplosion ] ----------------------------------------------------
        this.explosion = new Explosion();

        this.gameover = false;      // setTime을 위한 상태값!!****, 게임 오버는 아니고 애니메이션만 
                                    // 멈추게 해야 한다. 즉, 멈추면 상태값이 전부 날아가는 것이 
                                    // 아니라 정지인 상태이다.(배경과 적들은 움직임)

        // ---------------- [ dlg ] ----------------------------------------------------

        this.dlg = new ConfirmDlg();

        this.dlg.onclick=()=>{              // OS 수업 듣기! ****
            console.log("clicked");         // 통지만 해주기!!
        }                                   // 결국, bind 해줘야 한다!(콜백함수?) 
                                            // 내가 : 클릭하는 이벤트는 canvas에서만 갖고 있어서 이 기능을 confirm으로 넘겨줘서 실행하게 해준다?
                                            // 자식 클래스는 통지를 받는 기능이 있어서 이벤트 함수를 갖고 실행해준다!!
        this.dlg.show();                    // 우리가 할 것 : window의 모든 것에 통지를 하고 dlg에서 이벤트 함수 실행해주기!!
        
        // **** 장치라는 것은 브라운관을 다룬다. / OS 입장에서는 가상의 공간인 window를 다룬다.(OS의 중요성!)

        // ----------------------------------------------------------------------------

        this.pause = false;                                      // 그래서, 게임이 끝나는 것이 아니라 일시정지가 되어야 한다.
        this.dom.onclick = this.clickHandler.bind(this);         // **** .bind() 처리하면서 .bind(this)의 this는 이벤트 핸들러를 위한 객체를 불러온다. 원래 this는 gamecanvas이다.
        // this.dom.onclick = this.clickHandler;                 // **** .bind() 처리하면서 .bind(this)의 this는 이벤트 핸들러를 위한 객체를 불러온다. 원래 this는 gamecanvas이다.

        this.dom.onkeydown = this.keyDownHandler.bind(this);
        this.dom.onkeyup = this.keyUpHandler.bind(this);
        
        
    }                                                            
                                                                  // **** 내장 객체의 함수와 새로만들어 지는 객체의 함수를 비교할 것.
                                                                  // clickHandler는 사용자 입력에 의한 이벤트 핸들러이기 때문에
                                                                  //  window에서 가져와야 해서 .bind(this)를 뒤에 붙여준다.
    

    // 이렇게 사용하면 위의 코드와 비교해서 dom은 이전의 생성자에서 넘겨받아서 this가 dom이 아니다.
    //console.log(this)


    run(){
        //console.log(this);
        if(this.pause)
            return;

        // 60 프레임으로 화면을 다시 그리는 코드!!
        this.update();
        this.draw();

        //this.dlg.show();
    

        window.setTimeout(()=>{                 // 알람 맞추는 것과 같다.(60 프레임에 맞추어서 = 1000/60 = 17)
            this.run();    
        },17);

        // 여기까지 하면, 무한히 반복한다. 멈추고 싶으면? 이 안에서 멈추는 도구가 필요하다. 그래서 상태변수가 필요하다.

    }

        // canvas는 UI이고 UI에 의해서 꼭두각시처럼 컨트롤된다.

    update(){   // update는 단위 계수마다 잘게 쪼개서 움직여야 한다.
                // 시간에 국한에서 잘게 짜르려면 frame을 나누어야 한다. 
                // 이동하는 속도가 일정하려면, 

        // **** [전역 객체]
        // *** [전역 객체] = [문맥 객체] = [Context 객체] : 객체 사이에서 공유할 있는 전역객체가 필요하다!
        // *** update에서 "적기"에 대해 참조하기! : 1) canvas에 대해 참조한다? : 서로 참조해서 이상하다! 
        // 2) update 인자 넘겨주는 것도 문제이다.
        // 3) 전역변수화해서 아무나 쉽게 접글 할 수 있게 하기!! ("SingleTon" 객체: 전역객체이다.)

        this.boy.update();                  // 캐릭터를 이동시키자!
        //this.bg.update();              

        for(let enemy of this.enemies)      // 적기도 이동시키자!
            enemy.update();

        
        this.enemyAppearDelay--;

        if(this.enemyAppearDelay == 0){
            
            let x = getRandomInt(-50, 50+this.dom.width); // -50 ~ this.dom.width+50
            let y = -50;
            
            let enemy = new Enemy(x, y);              // ver 2 : 
            //this.enemies.push(new Enemy(x, y));     // ver 1 : 하지만, 이렇게하면 너무 무더기로 나온다.
            
            // enemyOutOfScreenHandler를 멤버로서 넣어주기!!

            // 2. [Aggregation 관계]******
            //for(let enemy of this.enemies)           // ver 1 :
            // enemy.onRandomOutOfScreen=(en)=>{         // ver 2 :
            //     let idx = this.enemies.indexOf(enr);   // en은 객체, 그것에서 index 받아서 splice이용하여 null 값 만들기
            //     this.enemies.splice(idx,1);            // 스스로 지울 수 있는 기능이 있는지 찾아보고 없으면, slice이용하기
            // };

            
            // ** 멤버로서 인식하게 해준다!!
            enemy.onRandomOutOfScreen=this.enemyOutOfScreenHandler.bind(this);
            
            this.enemies.push(enemy);                      // ver 2 :
            this.enemyAppearDelay = getRandomInt(30, 60);  // 30프레임이라서 0.5초이다.(17ms가 60프레임, 34ms가 30프레임)
        }

        function getRandomInt(min, max) {   // mdn의 랜덤 메서드 이용
            min = Math.ceil(min);
            max = Math.floor(max);
            return Math.floor(Math.random() * (max - min)) + min; //최댓값은 제외, 최솟값은 포함
        }
    }

    draw(){

        this.bg.draw(this.ctx);             // 순서 중요!!
        this.boy.draw(this.ctx);   
        this.explosion.draw(this.ctx);       

        //this.bg.draw(this.ctx);

        for(let enemy of this.enemies)      // 좌표를 이동시킨 적기를 이동시킨다!
            enemy.draw(this.ctx);

        this.dlg.draw(this.ctx);
    }

    pause(){
        this.pause = true;
    }

    boyNoLifeHandler(){ 
        // boy가 게임을 끝내는 것이 아니라 버튼을 이용하기!**** 
        
        // 게임 종료를 의미하는 애니메이션을 실행하거나
        // 게임 종료 또는 계속을 위한 입력을 받거나
        // 바로 캔버스를 전환하거나..
        // 기타 등등... 
    }

    // ** 멤버로서 인식하게 해준다!!
    enemyOutOfScreenHandler(en){            
        let idx = this.enemies.indexOf(en);   // en은 객체, 그것에서 index 받아서 splice이용하여 null 값 만들기
        this.enemies.splice(idx,1);  
    }

    // ------------------------------- event handler***** --------------------------------
    // 이벤트가 발생했다!! 사용자 입력에 의해서 이벤트가 발생 // 사건의 구체적인 정보를 알아야내 한다.(어떤 위치에 마우스를 클릭했는지, 어떤 키보드를 눌렀는지)

    clickHandler(e){         // 클릭하면, 타이머가 멈춘다!!
        //this.pause = true; // 같은 함수 내부라서 함수 호출 시, 
                             // 함수를 사용하는 것이 아니라 값을 입력해준다.

        // this.boy.isnotifyClick(e.x,e.y); // *** 자식객체에서 통지를 받은 녀석이 있는지 확인해서 반환해주기



        // ***** 위치가 있으면 너 있는지 물어보기! 하지만, 그 안에 자식이 있으면, 얘가 이 안에서 자식에게 물어본다.(또 물어보기 X)
        this.dlg.notifyClick(e.x,e.y)    // ** 결국 : 통지만 해주기!! 물어보고 해야할 일이 없기 때문에!

        
                                    // moveTo 메서드에서는 e.x와 e.y는 마우스 클릭할 때, 이용                        
        this.boy.moveTo(e.x,e.y);   // 이동할 때, 대각선 길이를 잘게 쪼개는데 가로 세로는 그 값이 얼마였든지간에 비율로 대각선 길이로 나눈다.
                                    // 나중에 배우자! 이벤트 함수 : screen x, event x, osell x


        // 화면 지우기 필요!
    
        this.boy.draw(this.ctx);    // 함수 호출자는 dom이다. 하지만, 우리는 this가 GameCanvas였으면 좋겠다. 서로 역할이 엇갈림.
                                    // 그래서 우리는 보따리를 쌓아 줘야한다.(위에서 bind로 묶어줌.) *******
    
    
        //console.log(this);      // 위의 dom이 앞에서 위임받아서 dom이 아니라 이전 생성자이다.
        }


    // ***** canvas나 div 태그는 키 입력을 안 받게 했다.
    keyDownHandler(e){
        
        console.log(e.key);
        switch(e.key){
            case "ArrowUp" :            // 이렇게만 만들면, 렉걸린것처럼 보인다.(한번 클릭 후 기다렸다가 여러번 클릭된다.)
                                        // 이것은 더블클릭 때문에 눌렀다가 뗄 때, 기다리는 현상이 있다.
                this.boy.move(1); 
                break;
            case "ArrowLeft" :
                this.boy.move(4); 
                break;

            case "ArrowRight" :
                this.boy.move(2); 
                break;

            case "ArrowDown" :
                this.boy.move(3); 
                break;

        }
    }

    keyUpHandler(e){
        
        console.log(e.key);             // 이렇게 해도 문제가 많다. 여러 방향키를 동시에 처리하지 못한다.(렉걸림)

        //this.boy.move(0);
        switch(e.key){
            case "ArrowUp":             // 이렇게만 만들면, 렉걸린 것처럼 보인다.(한번 클릭 후 기다렸다가 여러번 클릭된다.)
                                        // 이것은 더블클릭 때문에 눌렀다가 뗄 때, 기다리는 현상이 있다.
                this.boy.stop(1);       // 그래서, onkeyup으로 만들자!
                break;
            case "ArrowLeft":
                this.boy.stop(4); 
                break;

            case "ArrowRight":
                this.boy.stop(2); 
                break;

            case "ArrowDown":
                this.boy.stop(3); 
                break;

        }
    }

            
}


// ***** export default class : 이렇게 말고 다른 방법으로 함수를 정의할 때 사용해도 된다.


4) rank-canvas.js


export default class RankCanvas{
    constructor(){
        this.dom = document.querySelector(".rank-canvas");
        this.dom.focus();
        this.ctx = this.dom.getContext('2d');
    }

    run(){                          // 위에 객체에 알려주는 것이 이벤트를 사용하는 것이다!(콜백함수)
        this.update();              // 아래 객체를 이용하는 것은 서비스 함수를 이용하는 것
        this.draw();

        setTimeout(()=>{            // 아키텍쳐 명명이 어렵다> 변수명, 함수명, 메소드명(구조 저으이)
            this.run();
        },17);
    }

    update(){

    }

    draw(){ 
        this.ctx.strokeRect(0,0,this.dom.width, this.dom.height);       // 기존 캔버스의 옆에 생김!!

    }
}


5) confirmdlg.js

export default class ConfirmDlg{

    constructor(){

        // 버튼의 상태변수 생각!
        this.x = 100;
        this.y = 100;

        this.width = 400;       
        this.height= 200;
        this.isVisible = false;

        this.onContinue = false;
        this.onEnd = false;

    }

    show(){
        this.isVisible = true;

    }

    update(){

    }

    draw(ctx){
        // ctx는 자기 크기를 자기가 정한다.
        if(!this.isVisible)
            return;

        let {x,y} = this;           // 이런식으로도 this값 가져오기

        ctx.fillStyle = '#FFF5';    // 투명도 조절!
        ctx.fillRect(x,y,this.width,this.height);     // this.ctx.width가 아니라 this.width이구나!!!

        ctx.fillStyle = '#000';
        ctx.strokeRect(x,y,this.width,this.height);   // this.ctx.width가 아니라 this.width이구나.
        
        //------------------

        ctx.fillStyle = 'black'
        ctx.font = 'bold 48px serif'
        ctx.fillText('Continue?', this.width/2, y+70);

        ctx.fillStyle = 'gray';
        ctx.fillRect(x+70, y+100, 100, 50);
        ctx.fillStyle = 'black';
        ctx.strokeRect(x+70, y+100, 100, 50);
        ctx.fillStyle = 'gray';
        ctx.fillRect(x+230, y+100, 100, 50);
        ctx.fillStyle = 'black';
        ctx.strokeRect(x+230, y+100, 100, 50);

        ctx.font = 'bold 30px serif'
        ctx.fillText('YES', this.width/2-10, y+135);
        ctx.font = 'bold 30px serif'
        ctx.fillText('NO', this.width/2+160, y+135);  
        
        // 버블링!! : 이 박스가 자식인 2개의 박스도 책임져야해서 물어볼 때, 주의!

    }
}



6) boy.js

// 원래 웹에서는 파일명을 가장 앞에는 소문자로 구분하고 대쉬(-)로 구분한다

// 중요!!!!**** 
// 캡슐화 vs 캡슐(데이터를 구조화한 것) 
// 캡슐화란 - 책임을 부여하는 것, 역할을 나누는 것, 기능을 부여하는 것
// 객체지향은 그림을 그릴 수 밖에 없고, 그 그림은 설계가 된다.

import newlec from '../newlec.js';

export default class Boy {                     // *******

    constructor(x,y){
        this.x = x || 200;      // 6. 오버로드 생성자로 사용자가 넘겨줄 값이 있을 때, 가능하게 하려고 이렇게도 가능하다.
        this.y = y || 100;      // 사용자 입력이 없을 때는, x와 y는 || 뒤의 기본값이 들어간다.
        
        this.vx = 0;            // 단위 위치(잘게 조개진 값)
        this.vy = 0;            
        
        this.dx = 0;            // 목적지 위치
        this.dy = 0;     
        
        this.dir = 0;


        var moveLeft = false;   // 처음에 FALSE로 설정
        var moveRight = false;
        var moveUp = false;
        var moveDown = false;

        this.speed = 3;         // JS : 속성, , 이벤트

        this.img = document.querySelector("#boy");          // html에서 이미지를 이렇게 가져온다.(이젠, 생성자에서 그리기 위해서!) *************

        this.ix = 1;                                        // ix, iy는 스프라이트 사진에서 부분을 꺼낼수 있게 배열의 인덱스 값이다!
        this.iy = 2;
    
        this.walkDelay = 20;            // 프레임 낮춰주기!!


        // *** 중요!!
        this.sw = this.img.width/3;     //106;
        this.sh = this.img.height/4;    //148.25;


        this.sx = this.sw * this.ix;
        this.sy = this.sh * this.iy;    // 메인 스레드는 절차를 담당하는 기본 흐름이다. setTimeout인 보조 흐름이며 추가적으로 작동을 만든 것이 게임 스레드이다.

       
    }                                   // 이것의 차이를 알기!!****
                                        // update와 draw는 메인 스레드(게임 스레드)이다.(사용자 입력이 없더라도, 내가 호출하는 것이 아니라 초당 60번씩 계속 돌고 있다.)
                                        // 나머지는 UI 스레드이다.(사용자 입력이 있을 때만, 한 번만 움직임.)

    // set speed(value){
    //     this.#speed = value;             // *** JS에서 getter, setter를 이렇게 사용할 수 있다.
    // }

    // get speed(){
    //     return this.#speed;
    // }


    draw(ctx){

        this.sx = this.sw * this.ix;
        this.sy = this.sh * this.iy;
        ctx.drawImage(this.img, this.sx, this.sy, this.sw, this.sh,     // **** 에러발생 : img는 생성자에서 가져오기 때문에 this.img 이다!!
            this.x-this.sw/2, this.y-this.sh+15, this.sw, this.sh);     // 발이 기준이 되게 값을 조정해주자!! 
        
        // var img = new Image();	
        // img.src = './image/boy.png';    // 이미지도 생성자에서 생성하자!!*****

        // img.onload = function(){    // 이렇게 만들면 에러가 나는 이유?

        //                             // sx는 source(원본의 x크기), dx(display의 x크기)
          
        // }.bind(this);               // 여기서는 Boy에서 img로 이벤트가 위임 되었다!!(중요)
        
        // 개념적인 부분 : 위임? 다시 이벤트 위임 공부, Function.prototype.call()은 모든 함수에서 call이 있어야 한다.
        
        // 중요!! 함수가 호출한 주체가 this가 된다. apply, call
        //console.log(this);
        ctx.beginPath();
        ctx.arc(this.x, this.y, this.sw/2, 0, 2 * Math.PI);
        ctx.stroke();


    }

    
    
    update(){       // ** update는 상태값만 변경해줘야 한다.

        for(let enemy of newlec.enemies) {          // 역할을 누구한테 줄지 생각하기!!

            //  *** 보정된 enemy 객체의 좌표 **** : 쌤 작성
            let ex = enemy.x;
            let ey = enemy.y;

            let x = this.x;         //  원래 boy는 중앙을 잡고 조정해서 그린 것이 여서 건드리지 말자!
            let y = this.y;

            // Math 함수 안에 Math 함수가 중첩이 불가능하다!
            let d = Math.sqrt((ex-x)*(ex-x)+(ey-y)*(ey-y));
            let r1r2 = this.sw/2 + enemy.width/2;               // boy와 enemy의 반지름의 합
           
            if(d <= r1r2){
                enemy.chungdol();       // 충돌시, 이미지
                console.log("충돌");

                // ***위임 받아 놓은 함수(callback 함수)를 호출한다.
                if(this.onNoLife)
                    this.onNoLife();
            }

            //  *** 보정된 enemy 객체의 좌표 **** : 내가 작성
            // let x = enemy.centerX;    // 원래 x는 public이라서 private 해줘야하지만 귀찮..
            // let y = enemy.centerY;    // 원래 y도 public이라서 private 해줘야하지만 귀찮..

            // // 하지만, boy 이미지의 좌표는 발밑에 있다.
            
            // let d = Math.sqrt(Math.pow(x-this.x,2)+Math.pow(y-this.y,2));
            // let r1r2 = 30; 
            // Math.sqrt(x*x+y*y)+ Math.sqrt(this.x*this.x + this.y*this.y);


            // *** 1. 도형을 그리는 방법 : begin은 항상 필요한다.(그냥 함수가 rect나 arc만 해주는 경우)
            // *** 2. 도형을 그리는 방법 : begin이 합쳐진 함수인 fillRect는 begin이 필요없다.
        }

        console.log(newlec.enemies.length);     // ** 왜 enemy인데 boy에서 만들었는가? enemy에서도 이용가능!
        
        // ======================================
        
        // --- 이동을 위한 코드 -------

        if(this.moveUp)                     // 이런식으로 구현하면 여덟 방향의 이동도 가능하다.
            this.y -= this.speed;          // 방향을 이거로만 업데이트!!
        
        if(this.moveDown)
            this.y += this.speed;          // 또한, #을 붙이면, private 변수로 바뀌고 이것은 java처럼 getter, setter처럼 이용하자!
        
        if(this.moveRight)                  // getter, setter를 이용할 수 있는 색다른 방법? : C#에서 이용한다!
            this.x += this.speed;
            
        if(this.moveLeft)
            this.x -= this.speed;





        // switch(this.dir){          // 코드의 순서 주의!!(아래 코드에서 return 때문에 문제가 발생했다.)
        //     case 1: // 북쪽        // 하지만 이렇게 하면, 캐릭터가 멈추지 않아서 키보드 뗄 때, 멈추도록하자!     
        //         this.y -= 1;      
        //     break;                   // 브라우저 기준으로 시작을 생각해보면 된다.
    
        //     case 2: // 동쪽
        //         this.x += 1;
        //     break;
            
        //     case 3: // 남쪽
        //         this.y += 1;
        //     break;
            
        //     case 4: // 서쪽
        //         this.x -= 1;
        //     break;
            
        // }

        // setInterval(draw, 10);
        
        //this.draw().drawImage(this.img, this.sx*this.ix, this.sy*this.iy);
        
        // 조건 설정!! dx가 중심점, dy가 중심점이라고 가정하에 조건문 설정!
        // 정확히는 dx가 기준점이 되고 x가 움직이는 변수이다.
        //  x축 따로, y축 따로 구성해서 합쳐서 구현!!
        
        if(this.dx - 1 < this.x  && this.x < this.dx + 1 ||  this.dy - 1 < this.y && this.y < this.dy + 1){           
            this.vx = 0;
            this.vy = 0;
            
            this.ix = 1;                         // 멈출 때만 상태값을 바꿔줘도 되는가?***
            
        }
        
        if(!(this.moveLeft || this.moveRight || this.moveUp || this.moveDown || false))     // 방향키가 눌린게 없다면? 다음 조건문을 진행!
            if(this.vx == 0 && this.vy == 0 ) {                                             // 멈출 때 조건 설정 : vx 설정 (이것도 되긴 된다.)
                this.ix = 1;                                                                // 멈췄는지 안멈췄는지는 vx의 유무가 중요하다!!
                return;
            }                                                                               // 여기가 정확히 캐릭터가 멈추게 해주는 역할이다!!!!*******
                                                                                            // 그래서 여기서, 발바꿈을 해준다.
                                                                                            // *** 숙제!! : 캐릭터 화면 전환하기
        // ======================================                                           // *** 추가로 할 것 : 발속도 올리기 : speed라는 변수를 둬야 한다.
        
        this.x += this.vx;                  // 여기서 목적직 위치까지 단위 위치(벡터)로 더하면서 업데이트 시킴
        this.y += this.vy;

        // 걸음을 걷는 효과!
        this.walkDelay--;
        //this.ix=this.ix==2?0:2;   // 3항 연산 중요!!!
        if(this.walkDelay == 0){
            if(this.ix == 0){
                this.ix = 2;
                
            }
            // else if(this.ix == 1){
            //     this.ix = 0;
                
            // }
            else {
                this.ix = 0;
                
            }
            this.walkDelay = 20;
        }


        // ------------------- 이동을 위한 로직 ------------
    }

    moveTo(dx,dy){
        
        let w = dx - this.x;                // 목적지 위치 - 현재 위치(업데이트되고나서) = 남은 위치 거리 계산 
        let h = dy - this.y;

        let d = Math.sqrt(w*w+h*h);         // 대각선 길이 공식

        this.vx = w / d;                    // 여기 this도 주의해서 적기, 그 남은 위치 거리를 다시 잘게 쪼개진 비율로 나누어서 이동한다.
        this.vy = h / d;
        
        this.dx = dx;
        this.dy = dy;
    }

    move(dir){

        switch(dir){               // 코드의 순서 주의!!(아래 코드에서 return 때문에 문제가 발생했다.)
            case 1: // 북쪽        // 하지만 이렇게 하면, 캐릭터가 멈추지 않아서 키보드 뗄 때, 멈추도록하자!    
                this.moveUp = true;
                this.iy= 0;
                break;
                
            case 3: // 남쪽        // ** 결론 : 자연스럽게 방향키 이동을 위해서, 동서남북의 4방향 모두 각각 상태 변수로 컨트롤 되어야 한다!!!!
                this.moveDown = true;
                this.iy= 2;
                break;
    
            case 2: // 동쪽
                this.moveRight = true;
                this.iy= 1;
                break;

            case 4: // 서쪽
                this.moveLeft = true;
                this.iy= 3;
                break;

            default:
                console.log(this.dir);
            
        }

    }

    stop(dir){

        switch(dir){               // 코드의 순서 주의!!(아래 코드에서 return 때문에 문제가 발생했다.)
            case 1: // 북쪽        // 하지만 이렇게 하면, 캐릭터가 멈추지 않아서 키보드 뗄 때, 멈추도록하자!    
                this.moveUp = false;
                break;

            case 3: // 남쪽        // ** 결론 : 자연스럽게 방향키 이동을 위해서, 동서남북의 4방향 모두 각각 상태 변수로 컨트롤 되어야 한다!!!!
                this.moveDown = false;
                break;
    
            case 2: // 동쪽
                this.moveRight = false;
                break;

            case 4: // 서쪽
                this.moveLeft = false;
                break;

            default:
                console.log(this.dir);
 
        }

    }
}



1. [23.01.13]


1) game Test


  • main.html


  • main.html
<!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>Document</title>

    <!-- 순서가 중요하다.**** --> <!-- 새로운 페이지 등록 , url 연결 시켜주기! -->
 
    <!-- <script src="./item/background.js"></script>    
    <script src="./item/boy.js"></script>
    <script src="./item/enemy.js"></script>
    <script src="./panel/game-canvas.js"></script>   -->
    <script type = "module" src="./app.js"></script>

    <!-- 마진을 0으로 만들어주기 --> <!-- display none도 style 태그로 설정(css) -->
    <style>
      body{
            margin: 0;
      }
      .d-none{
            display: none;

      }
    </style>

    
    <!-- <script src="ex1-es6-var.js"></script> -->
    <!-- <script type="module" src="./ex2-es6-module.js"></script> -->
 
    <!-- 문서가  읽혀지고 읽어지는 방법 : html, js에서 해결 가능(document.addEventListener 이용) -->
    
</head>
<body>
      <!-- 주의사항 : css를 키우면 배율이 커진다. 그래서 속성을 키워야 한다. , canvas를 키워야 한다.
           보통의 경우 속성을 키우고 css로 줄이는 경우는 이미지가 깨지는것을 방지해준다.  -->

      <input tabindex="2">
      <img>
      <div tabindex="1">            <!-- tabindex를 넣어야지 tab으로 이동할  있다. -->
            test                    <!-- 마우스가 기본이라서 마우스가 우선 순위라서 사용자가 마우스를 클릭하고나서 키의 입력을 받을  있다. -->
      </div>                        <!-- 그래서 키보드 입력하기 위해서 tabindex를 설정한다!! -->
      <input tabindex="0">
      <!-- style="display: none;" style 안보이지만 메모리에 올라가 있다 , dom에 올라가 있따. -->
      <img src="./image/boy.png" id ="boy" style="display: none;">
      <img src="./image/explosion.jpg" id ="explosion" style="display: none;">
      <img src="./image/enemy.png" id ="enemy" style="display: none;">
      <img src="./image/map.png" id ="bg" style="display: none;">
      <canvas tabindex="0" class="game-canvas" width="900" height="1000"></canvas>
      <canvas tabindex="1" class="rank-canvas d-none" width="900" height="1000"></canvas>

</body>
</html>


  • app.js
import GameCanvas from './panel/game-canvas.js';
import RankCanvas from './panel/rank-canvas.js';
import newlec from './newlec.js';

window.addEventListener("load", function(){

    const gameCanvas = new GameCanvas();  // 다른 파일을 가져오는 방식이 자바스크립트에서는 없다.
                                        // 그래서 사용할 모듈을 html에게 요청한다.
    gameCanvas.ongameOver=(e)=>{
        gameCanvas.dom.classList.add("d-none");
        rankCanvas.dom.classList.remove("d-none");
    };
    gameCanvas.run(); 

    const rankCanvas = new RankCanvas();
                                        // 에러 : GameCanvas 객체를 생성시, 참조는 소문자에 넣어준다. 


    // ------------------------------
    rankCanvas.run();
    
    // run()은 무한 loop해야 한다. UI 스레드는 별도의 흐름을 갖게 해야 한다.      
    // vscode에선 F12로 함수 선언과 이동 가능

    newlec.x++;
    console.log("x:", newlec.x);        // 2가 출력

 
});



  • game-canvas.js
// game-canvs.js는 사용자 입력과 출력을 담당한다.(중요!!) *****


import Boy from '../item/boy.js';
import Background from '../item/background.js';
import Enemy from '../item/enemy.js';
import Explosion from '../item/explosion.js';
import newlec from '../newlec.js';        // *** default(GameCanvas) 이름을 마음대로 상용할 수 있다!!
import ConfirmDlg from '../item/confirmdlg.js';


// **** import 된것을 새롭게 뱉어내는 방식!!
export default class GameCanvas {        // 클래스로 변경  

    constructor(){                                          // **** 중요한 개념 : 
        this.dom = document.querySelector(".game-canvas");  // 여기서 this는 GameCanvas의 new된 결과이다.
                                                            // dom이 canvas이다. canvas가 this의 멤버로 들어온 것이다.
                                                            // 원래 java에서는 canvas가 canvas를 갖는 것이 이상하다. 
                                                            // 그래서, JS는 틀로 쓸 수 있는 것이 아니다.(프레임워크가 아니였다.)

                                                            // 앞으로는 this의 dom 기능을 이용한다.

        this.dom.focus();     // focus를 잡아주면 canvas에서 키보드 입력을 할 수 있다. **** 즉, 우선순위를 마우스에서 키보드로 넘겨준다.
                              // 키보드로 무브하는 방법???? 
    
        /** @type {CanvasRenderingContext2D} */ 
        
        this.ctx = this.dom.getContext('2d');   // var ctx가 아니라 this.ctx이다.(Boy의 ctx이므로)         
        


        // ----------------- [ Boy ] ----------------------------------------
   
        this.boy = new Boy(100, 100);

        // ----------- boyNoLifeHandler -----------
        this.boy.onNoLife = this.boyNoLifeHandler.bind(this);

        this.boy.speed++;                       // get을 사용하지 않고도 speed로 바로 getSpeed 처럼 사용할 수 있다.
        console.log("speed:" + this.boy.speed);


        // ------------- [ 전역 객체!!! :  Singleton 패턴 ] ------------

        newlec.x++;                     // 위에서 new를 하지 않아서 기존의 객체를 가져다가 사용한다.(전역 공간!) 
        console.log("x:", newlec.x);    // 3이 출력

        
        // ---------------- [ Enemy ] ----------------------------------------------------
        
        this.enemy = [];    // Aggregation 방식!!

        this.enemyAppearDelay = 60;     // 적의 딜레이 만들기!(60프레임 기준 : 1초)
                                        // 17ms가 60프레임, 34ms가 30프레임


        // 이런식으로 적기를 만들 때, 콜백함수를 줄 수 있거나 따로 줄 수 도 있다.

        // *** 1번 Aggregation 관계에 사용
        this.enemies = [];

        // *** 2번 Composition 관계에 사용
        // this.enemies = [new Enemy(10,0), new Enemy(30,0), new Enemy(50,0), new Enemy(100,0),
        //     new Enemy(40,20), new Enemy(50,20), new Enemy(130,45)];



        // **** 다른 애가 불러내서 위임되는 함수라고 부르며 "call back function"이라고 부른다. 
        // 사용자가 클릭해달라고 위임했기 때문이다. call back function은 지금 실행하는 것이 아니라 나중에 실행된다.(위임해야지 나중에 전화할 수 있기 때문이다.(나중에 실행))
        // 전화하는 애가 주체가 된다. 
        // dom이 다시 호출해준다? 
        // this.dom.onclick = this.clickHandler;                   // ()처럼 함수를 호출한 결과 값 넣지 말기
        

        // 1. [Composition 관계]******
        // *** this를 바운더리 밖의 this를 이용하고 싶어서 람다함수를 이용한다.(bind를 안쓰려고!!)

        // *** 결론 : 객체를 new로 생성하면서 생성자에 바로 넘겨준다.
        // *** 결론 : 캔바스(객체 배열을 갖고 있는 것은 "game-canvas"라서 여기서 지운다.)에서 지우는 역할,
        // ***** enemy.js 객체(조건을 계속 감지)에서는 조건만 정해준다.

        // for(let enemy of this.enemies)                // ***** 초기 설정(랜덤 아니고 정해진 인덱스에서 출력)!!****
        //     enemy.onOutOfScreen=(en)=>{
        //         let idx = this.enemies.indexOf(en);   // en은 객체, 그것에서 index 받아서 splice이용하여 null 값 만들기
        //         this.enemies.splice(idx,1);           // 스스로 지울 수 있는 기능이 있는지 찾아보고 없으면, slice이용하기
        // };      // *** 여기서 함수(화살표 함수)가 생성되고 enemy 객체에 위임이 되고 그 함수가 호출될 때, 실행된다.(bind - this와 느낌이 비슷하다.)
        
        

        // ---------------- [ Background ] ------------------------------------------------
        
        this.bg = new Background();

        // --------- 전역객체-----------
        newlec.enemies = this.enemies;  // newlec의 enemies 던 canvas의 enemies던 같이 사용 가능!

        // ---------------- [ Eplosion ] ----------------------------------------------------
        this.explosion = new Explosion();

        this.gameover = false;      // setTime을 위한 상태값!!****, 게임 오버는 아니고 애니메이션만 
                                    // 멈추게 해야 한다. 즉, 멈추면 상태값이 전부 날아가는 것이 
                                    // 아니라 정지인 상태이다.(배경과 적들은 움직임)

        // ---------------- [ dlg ] ----------------------------------------------------

        this.dlg = new ConfirmDlg();

        this.dlg.onclick = this.dlgClickHandler.bind(this);

        // *****객체지향에서는 책임자가 누구인지가 중요!! 역할을 고르는 것이 어렵다!
        // 즉, 소유자가 관리하는 것이다@

        // ********** 이거 빼기 (뒤에서 멤버로 받기 때문에 이 코드는 없애주기!)
        // this.dlg.onclick=(id)=>{              
        //     console.log("clicked"+id);         
        // }   
                                            // OS 수업 듣기! ****                                
                                            // 통지만 해주기!!                        
                                            // 결국, bind 해줘야 한다!(콜백함수?) 
                                            // 내가 : 클릭하는 이벤트는 canvas에서만 갖고 있어서 이 기능을 confirm으로 넘겨줘서 실행하게 해준다?
                                            // 자식 클래스는 통지를 받는 기능이 있어서 이벤트 함수를 갖고 실행해준다!!
                                            // 우리가 할 것 : window의 모든 것에 통지를 하고 dlg에서 이벤트 함수 실행해주기!!
        
        // **** 장치라는 것은 브라운관을 다룬다. / OS 입장에서는 가상의 공간인 window를 다룬다.(OS의 중요성!)

        // ----------------------------------------------------------------------------

        this.pause = false;    // 그래서, 게임이 끝나는 것이 아니라 일시정지가 되어야 한다.     

        this.ongameOver =null;    // 초기에는 널값으로 받기!!                     
    
        this.dom.onclick = this.clickHandler.bind(this);         // **** .bind() 처리하면서 .bind(this)의 this는 이벤트 핸들러를 위한 객체를 불러온다. 원래 this는 gamecanvas이다.
        // this.dom.onclick = this.clickHandler;                 // **** .bind() 처리하면서 .bind(this)의 this는 이벤트 핸들러를 위한 객체를 불러온다. 원래 this는 gamecanvas이다.

        this.dom.onkeydown = this.keyDownHandler.bind(this);
        this.dom.onkeyup = this.keyUpHandler.bind(this);
    }                                                            
                                                                  // **** 내장 객체의 함수와 새로만들어 지는 객체의 함수를 비교할 것.
                                                                  // clickHandler는 사용자 입력에 의한 이벤트 핸들러이기 때문에
                                                                  //  window에서 가져와야 해서 .bind(this)를 뒤에 붙여준다.
    

    // 이렇게 사용하면 위의 코드와 비교해서 dom은 이전의 생성자에서 넘겨받아서 this가 dom이 아니다.
    //console.log(this)


    run(){
        //console.log(this);
        if(this.pause)
            return;

        // 60 프레임으로 화면을 다시 그리는 코드!!
        this.update();
        this.draw();

        // 책임을 질 수 있는 경우!를 생각해야 한다!!
        //this.dlg.show();
    
        window.setTimeout(()=>{                 // 알람 맞추는 것과 같다.(60 프레임에 맞추어서 = 1000/60 = 17)
            this.run();    
        },17);

        // 여기까지 하면, 무한히 반복한다. 멈추고 싶으면? 이 안에서 멈추는 도구가 필요하다. 그래서 상태변수가 필요하다.

    }

        // canvas는 UI이고 UI에 의해서 꼭두각시처럼 컨트롤된다.

    update(){   // update는 단위 계수마다 잘게 쪼개서 움직여야 한다.
                // 시간에 국한에서 잘게 짜르려면 frame을 나누어야 한다. 
                // 이동하는 속도가 일정하려면, 

        // **** [전역 객체]
        // *** [전역 객체] = [문맥 객체] = [Context 객체] : 객체 사이에서 공유할 있는 전역객체가 필요하다!
        // *** update에서 "적기"에 대해 참조하기! : 1) canvas에 대해 참조한다? : 서로 참조해서 이상하다! 
        // 2) update 인자 넘겨주는 것도 문제이다.
        // 3) 전역변수화해서 아무나 쉽게 접글 할 수 있게 하기!! ("SingleTon" 객체: 전역객체이다.)

        this.boy.update();                  // 캐릭터를 이동시키자!
        //this.bg.update();              

        for(let enemy of this.enemies)      // 적기도 이동시키자!
            enemy.update();

        
        this.enemyAppearDelay--;

        if(this.enemyAppearDelay == 0){
            
            let x = getRandomInt(-50, 50+this.dom.width); // -50 ~ this.dom.width+50
            let y = -50;
            
            let enemy = new Enemy(x, y);              // ver 2 : 
            //this.enemies.push(new Enemy(x, y));     // ver 1 : 하지만, 이렇게하면 너무 무더기로 나온다.
            
            // enemyOutOfScreenHandler를 멤버로서 넣어주기!!

            // 2. [Aggregation 관계]******
            //for(let enemy of this.enemies)           // ver 1 :
            // enemy.onRandomOutOfScreen=(en)=>{         // ver 2 :
            //     let idx = this.enemies.indexOf(enr);   // en은 객체, 그것에서 index 받아서 splice이용하여 null 값 만들기
            //     this.enemies.splice(idx,1);            // 스스로 지울 수 있는 기능이 있는지 찾아보고 없으면, slice이용하기
            // };

            
            // ** 멤버로서 인식하게 해준다!!
            enemy.onRandomOutOfScreen=this.enemyOutOfScreenHandler.bind(this);
            
            this.enemies.push(enemy);                      // ver 2 :
            this.enemyAppearDelay = getRandomInt(30, 60);  // 30프레임이라서 0.5초이다.(17ms가 60프레임, 34ms가 30프레임)
        }

        function getRandomInt(min, max) {   // mdn의 랜덤 메서드 이용
            min = Math.ceil(min);
            max = Math.floor(max);
            return Math.floor(Math.random() * (max - min)) + min; //최댓값은 제외, 최솟값은 포함
        }
    }

    draw(){

        this.bg.draw(this.ctx);             // 순서 중요!!
        this.boy.draw(this.ctx);   
        this.explosion.draw(this.ctx);       

        //this.bg.draw(this.ctx);

        for(let enemy of this.enemies)      // 좌표를 이동시킨 적기를 이동시킨다!
            enemy.draw(this.ctx);

        this.dlg.draw(this.ctx);
    }

    pause(){
        this.pause = true;
    }

    boyNoLifeHandler(){ 
        // boy가 게임을 끝내는 것이 아니라 버튼을 이용하기!**** 
        
        // 게임 종료를 의미하는 애니메이션을 실행하거나
        // 게임 종료 또는 계속을 위한 입력을 받거나
        // 바로 캔버스를 전환하거나..
        // 기타 등등... 

        // if(!this.dlg.isShow())
        
        this.dlg.show();
    }

    // ** 멤버로서 인식하게 해준다!!
    enemyOutOfScreenHandler(en){            
        let idx = this.enemies.indexOf(en);   // en은 객체, 그것에서 index 받아서 splice이용하여 null 값 만들기
        this.enemies.splice(idx,1);  
    }

    // ** 콜백함수 : 쟤를 바꿔 주세요가 아니라 나는 끝났다고 알려줄 것!!
    dlgClickHandler(id){

        if(this.ongameOver){     // 이런 식으로 할당을 하고 호출을 해준다!
            //this.pause();
            this.ongameOver();
            console.log(id);
        }

    }
    // -------------------- event handler***** --------------------------------
    // 이벤트가 발생했다!! 사용자 입력에 의해서 이벤트가 발생 // 사건의 구체적인 정보를 알아야내 한다.(어떤 위치에 마우스를 클릭했는지, 어떤 키보드를 눌렀는지)

    clickHandler(e){         // 클릭하면, 타이머가 멈춘다!!
        //this.pause = true; // 같은 함수 내부라서 함수 호출 시, 
                             // 함수를 사용하는 것이 아니라 값을 입력해준다.

        // this.boy.isnotifyClick(e.x,e.y); // *** 자식객체에서 통지를 받은 녀석이 있는지 확인해서 반환해주기

        // ***** 위치가 있으면 너 있는지 물어보기! 하지만, 그 안에 자식이 있으면, 얘가 이 안에서 자식에게 물어본다.(또 물어보기 X)
        this.dlg.notifyClick(e.x,e.y)    // ** 결국 : 통지만 해주기!! 물어보고 해야할 일이 없기 때문에!

        
                                    // moveTo 메서드에서는 e.x와 e.y는 마우스 클릭할 때, 이용                        
        this.boy.moveTo(e.x,e.y);   // 이동할 때, 대각선 길이를 잘게 쪼개는데 가로 세로는 그 값이 얼마였든지간에 비율로 대각선 길이로 나눈다.
                                    // 나중에 배우자! 이벤트 함수 : screen x, event x, osell x


        // 화면 지우기 필요!
    
        this.boy.draw(this.ctx);    // 함수 호출자는 dom이다. 하지만, 우리는 this가 GameCanvas였으면 좋겠다. 서로 역할이 엇갈림.
                                    // 그래서 우리는 보따리를 쌓아 줘야한다.(위에서 bind로 묶어줌.) *******
    
    
        //console.log(this);      // 위의 dom이 앞에서 위임받아서 dom이 아니라 이전 생성자이다.
        }


    // ***** canvas나 div 태그는 키 입력을 안 받게 했다.
    keyDownHandler(e){
        
        console.log(e.key);
        switch(e.key){
            case "ArrowUp" :            // 이렇게만 만들면, 렉걸린것처럼 보인다.(한번 클릭 후 기다렸다가 여러번 클릭된다.)
                                        // 이것은 더블클릭 때문에 눌렀다가 뗄 때, 기다리는 현상이 있다.
                this.boy.move(1); 
                break;
            case "ArrowLeft" :
                this.boy.move(4); 
                break;

            case "ArrowRight" :
                this.boy.move(2); 
                break;

            case "ArrowDown" :
                this.boy.move(3); 
                break;

        }
    }

    keyUpHandler(e){
        
        console.log(e.key);             // 이렇게 해도 문제가 많다. 여러 방향키를 동시에 처리하지 못한다.(렉걸림)

        //this.boy.move(0);
        switch(e.key){
            case "ArrowUp":             // 이렇게만 만들면, 렉걸린 것처럼 보인다.(한번 클릭 후 기다렸다가 여러번 클릭된다.)
                                        // 이것은 더블클릭 때문에 눌렀다가 뗄 때, 기다리는 현상이 있다.
                this.boy.stop(1);       // 그래서, onkeyup으로 만들자!
                break;
            case "ArrowLeft":
                this.boy.stop(4); 
                break;

            case "ArrowRight":
                this.boy.stop(2); 
                break;

            case "ArrowDown":
                this.boy.stop(3); 
                break;

        }
    }

            
}


// ***** export default class : 이렇게 말고 다른 방법으로 함수를 정의할 때 사용해도 된다.


  • rank-canvas.js

export default class RankCanvas{
    constructor(){
        this.dom = document.querySelector(".rank-canvas");
        this.dom.focus();
        this.ctx = this.dom.getContext('2d');
    }

    run(){                          // 위에 객체에 알려주는 것이 이벤트를 사용하는 것이다!(콜백함수)
        this.update();              // 아래 객체를 이용하는 것은 서비스 함수를 이용하는 것
        this.draw();

        setTimeout(()=>{            // 아키텍쳐 명명이 어렵다> 변수명, 함수명, 메소드명(구조 저으이)
            this.run();
        },17);
    }

    update(){

    }

    draw(){ 
        this.ctx.strokeRect(0,0,this.dom.width, this.dom.height);       // 기존 캔버스의 옆에 생김!!

    }
}


  • boy.js
// 원래 웹에서는 파일명을 가장 앞에는 소문자로 구분하고 대쉬(-)로 구분한다

// 중요!!!!**** 
// 캡슐화 vs 캡슐(데이터를 구조화한 것) 
// 캡슐화란 - 책임을 부여하는 것, 역할을 나누는 것, 기능을 부여하는 것
// 객체지향은 그림을 그릴 수 밖에 없고, 그 그림은 설계가 된다.

import newlec from '../newlec.js';

export default class Boy {                     // *******

    constructor(x,y){
        this.x = x || 200;      // 6. 오버로드 생성자로 사용자가 넘겨줄 값이 있을 때, 가능하게 하려고 이렇게도 가능하다.
        this.y = y || 100;      // 사용자 입력이 없을 때는, x와 y는 || 뒤의 기본값이 들어간다.
        
        this.vx = 0;            // 단위 위치(잘게 조개진 값)
        this.vy = 0;            
        
        this.dx = 0;            // 목적지 위치
        this.dy = 0;     
        
        this.dir = 0;


        var moveLeft = false;   // 처음에 FALSE로 설정
        var moveRight = false;
        var moveUp = false;
        var moveDown = false;

        this.speed = 3;         // JS : 속성, , 이벤트

        this.noLife = false;


        this.img = document.querySelector("#boy");          // html에서 이미지를 이렇게 가져온다.(이젠, 생성자에서 그리기 위해서!) *************

        this.ix = 1;                                        // ix, iy는 스프라이트 사진에서 부분을 꺼낼수 있게 배열의 인덱스 값이다!
        this.iy = 2;
    
        this.walkDelay = 20;            // 프레임 낮춰주기!!


        // *** 중요!!
        this.sw = this.img.width/3;     //106;
        this.sh = this.img.height/4;    //148.25;


        this.sx = this.sw * this.ix;
        this.sy = this.sh * this.iy;    // 메인 스레드는 절차를 담당하는 기본 흐름이다. setTimeout인 보조 흐름이며 추가적으로 작동을 만든 것이 게임 스레드이다.

       
    }                                   // 이것의 차이를 알기!!****
                                        // update와 draw는 메인 스레드(게임 스레드)이다.(사용자 입력이 없더라도, 내가 호출하는 것이 아니라 초당 60번씩 계속 돌고 있다.)
                                        // 나머지는 UI 스레드이다.(사용자 입력이 있을 때만, 한 번만 움직임.)

    // set speed(value){
    //     this.#speed = value;             // *** JS에서 getter, setter를 이렇게 사용할 수 있다.
    // }

    // get speed(){
    //     return this.#speed;
    // }


    draw(ctx){

        this.sx = this.sw * this.ix;
        this.sy = this.sh * this.iy;
        ctx.drawImage(this.img, this.sx, this.sy, this.sw, this.sh,     // **** 에러발생 : img는 생성자에서 가져오기 때문에 this.img 이다!!
            this.x-this.sw/2, this.y-this.sh+15, this.sw, this.sh);     // 발이 기준이 되게 값을 조정해주자!! 
        
        // var img = new Image();	
        // img.src = './image/boy.png';    // 이미지도 생성자에서 생성하자!!*****

        // img.onload = function(){    // 이렇게 만들면 에러가 나는 이유?

        //                             // sx는 source(원본의 x크기), dx(display의 x크기)
          
        // }.bind(this);               // 여기서는 Boy에서 img로 이벤트가 위임 되었다!!(중요)
        
        // 개념적인 부분 : 위임? 다시 이벤트 위임 공부, Function.prototype.call()은 모든 함수에서 call이 있어야 한다.
        
        // 중요!! 함수가 호출한 주체가 this가 된다. apply, call
        //console.log(this);
        ctx.beginPath();
        ctx.arc(this.x, this.y, this.sw/2, 0, 2 * Math.PI);
        ctx.stroke();


    }

    
    
    update(){       // ** update는 상태값만 변경해줘야 한다.

        for(let enemy of newlec.enemies) {          // 역할을 누구한테 줄지 생각하기!!

            //  *** 보정된 enemy 객체의 좌표 **** : 쌤 작성
            let ex = enemy.x;
            let ey = enemy.y;

            let x = this.x;         //  원래 boy는 중앙을 잡고 조정해서 그린 것이 여서 건드리지 말자!
            let y = this.y;

            // Math 함수 안에 Math 함수가 중첩이 불가능하다!
            let d = Math.sqrt((ex-x)*(ex-x)+(ey-y)*(ey-y));
            let r1r2 = this.sw/2 + enemy.width/2;               // boy와 enemy의 반지름의 합
           
            if(d <= r1r2){
                enemy.chungdol();       // 충돌시, 이미지
                console.log("충돌");

                // ***위임 받아 놓은 함수(callback 함수)를 호출한다.
                // 캔바스에서는 계속 호출해서 한번만 호출했다고 상태변수가 필요하다!
                // on은 이벤트핸들러가 만들고, nolife는 실행할 것이 있다면!
                if(this.onNoLife && !this.noLife){

                    this.onNoLife();
                    this.noLife = true;
                }
            }

            //  *** 보정된 enemy 객체의 좌표 **** : 내가 작성
            // let x = enemy.centerX;    // 원래 x는 public이라서 private 해줘야하지만 귀찮..
            // let y = enemy.centerY;    // 원래 y도 public이라서 private 해줘야하지만 귀찮..

            // // 하지만, boy 이미지의 좌표는 발밑에 있다.
            
            // let d = Math.sqrt(Math.pow(x-this.x,2)+Math.pow(y-this.y,2));
            // let r1r2 = 30; 
            // Math.sqrt(x*x+y*y)+ Math.sqrt(this.x*this.x + this.y*this.y);


            // *** 1. 도형을 그리는 방법 : begin은 항상 필요한다.(그냥 함수가 rect나 arc만 해주는 경우)
            // *** 2. 도형을 그리는 방법 : begin이 합쳐진 함수인 fillRect는 begin이 필요없다.
        }

        //console.log(newlec.enemies.length);     // ** 왜 enemy인데 boy에서 만들었는가? enemy에서도 이용가능!
        
        // ======================================
        
        // --- 이동을 위한 코드 -------

        if(this.moveUp)                     // 이런식으로 구현하면 여덟 방향의 이동도 가능하다.
            this.y -= this.speed;          // 방향을 이거로만 업데이트!!
        
        if(this.moveDown)
            this.y += this.speed;          // 또한, #을 붙이면, private 변수로 바뀌고 이것은 java처럼 getter, setter처럼 이용하자!
        
        if(this.moveRight)                  // getter, setter를 이용할 수 있는 색다른 방법? : C#에서 이용한다!
            this.x += this.speed;
            
        if(this.moveLeft)
            this.x -= this.speed;





        // switch(this.dir){          // 코드의 순서 주의!!(아래 코드에서 return 때문에 문제가 발생했다.)
        //     case 1: // 북쪽        // 하지만 이렇게 하면, 캐릭터가 멈추지 않아서 키보드 뗄 때, 멈추도록하자!     
        //         this.y -= 1;      
        //     break;                   // 브라우저 기준으로 시작을 생각해보면 된다.
    
        //     case 2: // 동쪽
        //         this.x += 1;
        //     break;
            
        //     case 3: // 남쪽
        //         this.y += 1;
        //     break;
            
        //     case 4: // 서쪽
        //         this.x -= 1;
        //     break;
            
        // }

        // setInterval(draw, 10);
        
        //this.draw().drawImage(this.img, this.sx*this.ix, this.sy*this.iy);
        
        // 조건 설정!! dx가 중심점, dy가 중심점이라고 가정하에 조건문 설정!
        // 정확히는 dx가 기준점이 되고 x가 움직이는 변수이다.
        //  x축 따로, y축 따로 구성해서 합쳐서 구현!!
        
        if(this.dx - 1 < this.x  && this.x < this.dx + 1 ||  this.dy - 1 < this.y && this.y < this.dy + 1){           
            this.vx = 0;
            this.vy = 0;
            
            this.ix = 1;                         // 멈출 때만 상태값을 바꿔줘도 되는가?***
            
        }
        
        if(!(this.moveLeft || this.moveRight || this.moveUp || this.moveDown || false))     // 방향키가 눌린게 없다면? 다음 조건문을 진행!
            if(this.vx == 0 && this.vy == 0 ) {                                             // 멈출 때 조건 설정 : vx 설정 (이것도 되긴 된다.)
                this.ix = 1;                                                                // 멈췄는지 안멈췄는지는 vx의 유무가 중요하다!!
                return;
            }                                                                               // 여기가 정확히 캐릭터가 멈추게 해주는 역할이다!!!!*******
                                                                                            // 그래서 여기서, 발바꿈을 해준다.
                                                                                            // *** 숙제!! : 캐릭터 화면 전환하기
        // ======================================                                           // *** 추가로 할 것 : 발속도 올리기 : speed라는 변수를 둬야 한다.
        
        this.x += this.vx;                  // 여기서 목적직 위치까지 단위 위치(벡터)로 더하면서 업데이트 시킴
        this.y += this.vy;

        // 걸음을 걷는 효과!
        this.walkDelay--;
        //this.ix=this.ix==2?0:2;   // 3항 연산 중요!!!
        if(this.walkDelay == 0){
            if(this.ix == 0){
                this.ix = 2;
                
            }
            // else if(this.ix == 1){
            //     this.ix = 0;
                
            // }
            else {
                this.ix = 0;
                
            }
            this.walkDelay = 20;
        }


        // ------------------- 이동을 위한 로직 ------------
    }

    moveTo(dx,dy){
        
        let w = dx - this.x;                // 목적지 위치 - 현재 위치(업데이트되고나서) = 남은 위치 거리 계산 
        let h = dy - this.y;

        let d = Math.sqrt(w*w+h*h);         // 대각선 길이 공식

        this.vx = w / d;                    // 여기 this도 주의해서 적기, 그 남은 위치 거리를 다시 잘게 쪼개진 비율로 나누어서 이동한다.
        this.vy = h / d;
        
        this.dx = dx;
        this.dy = dy;
    }

    move(dir){

        switch(dir){               // 코드의 순서 주의!!(아래 코드에서 return 때문에 문제가 발생했다.)
            case 1: // 북쪽        // 하지만 이렇게 하면, 캐릭터가 멈추지 않아서 키보드 뗄 때, 멈추도록하자!    
                this.moveUp = true;
                this.iy= 0;
                break;
                
            case 3: // 남쪽        // ** 결론 : 자연스럽게 방향키 이동을 위해서, 동서남북의 4방향 모두 각각 상태 변수로 컨트롤 되어야 한다!!!!
                this.moveDown = true;
                this.iy= 2;
                break;
    
            case 2: // 동쪽
                this.moveRight = true;
                this.iy= 1;
                break;

            case 4: // 서쪽
                this.moveLeft = true;
                this.iy= 3;
                break;

            default:
                console.log(this.dir);
            
        }

    }

    stop(dir){

        switch(dir){               // 코드의 순서 주의!!(아래 코드에서 return 때문에 문제가 발생했다.)
            case 1: // 북쪽        // 하지만 이렇게 하면, 캐릭터가 멈추지 않아서 키보드 뗄 때, 멈추도록하자!    
                this.moveUp = false;
                break;

            case 3: // 남쪽        // ** 결론 : 자연스럽게 방향키 이동을 위해서, 동서남북의 4방향 모두 각각 상태 변수로 컨트롤 되어야 한다!!!!
                this.moveDown = false;
                break;
    
            case 2: // 동쪽
                this.moveRight = false;
                break;

            case 4: // 서쪽
                this.moveLeft = false;
                break;

            default:
                console.log(this.dir);
 
        }

    }
}


  • enemy.js
//import newlec from '../newlec.js';

import Explosion from "./explosion.js";

export default class Enemy{
    constructor(x=0,y=0){
        this.x = x || 200;
        this.y = y || 300;
        
        this.vx = 0;
        this.vy = 0; 
        
        this.dx = 0;
        this.dy = 0;
        
        this.speed = 5;

        this.onOutOfScreen = null;
        this.isChungdol = false;

        this.img = document.querySelector("#enemy");


        // ------------------- [Explosion] ------------------------
        // *** 터지는 이미지에 대해서는 행동이 없어서! 딱히 책임을 줄 필요가 없다!!(굳이 그렇게 할 필요가 없다.)
        // 하지만, 터지는 것이 여러 직군에서 터지면 캡슐화로 만들 수도 있다.

        this.imgExpl = document.querySelector("#explosion");

        this.eix=0;
        this.eiy=0;

        this.esw= this.imgExpl.width/4;
        this.esh= this.imgExpl.width/5;
        
        // this.onOutOfScreen = null;        // ***** 초기 설정 : 
        this.onRandomOutOfScreen = null;
        
        // *** 여기에 game-canvas에서 넘겨받은 화살표 함수를 담는다.


        //console.log(this.img.width);

    }

    // 하지만, 객체 지향에서는 책임에 대한 생각을 해야 한다!!!(누가 누구때문에 파괴되는지!)
    // x가 정해지면 자동으로 좌표가 정해져서 get만 설정!(set을 할 필요가 없다.)
    get centerX(){
        return this.x + this.img.width/2;           // 적기의 좌표 조정!(원래 이미지 하나당 왼쪽위를 기준이여서 그렇다!)
    }

    get centerY(){
        return this.y + this.img.height/2;
    }

    get width(){
        return this.img.width;
    }

    // 소년에게 터지라고 요청한다.

    chungdol(){                     // 충돌 검사
        this.isChungdol = true;
    }

    draw(ctx){

        // 적기****
        ctx.drawImage(this.img, this.x, this.y);

        
        // explosion 이미지****
        if(this.isChungdol){                        // 이와 반대로 소유하고 있는 애한테 뭐했다고 얘기 해주는 것이 : 콜백함수이다. 
            this.esx = this.esw * this.eix;         // 책임성에 룰을 정해주자!
            this.esy = this.esh * this.eiy;    

            ctx.drawImage(this.imgExpl, this.esx, this.esy, this.esw, this.esh,     // **** 에러발생 : img는 생성자에서 가져오기 때문에 this.img 이다!!
            this.x-this.esw/2+25, this.y-this.esh/2+30, this.esw, this.esh);     
        }
        // --------------------------------------------
        // d = Math.sqrt((ex-x)*(ex-x)+(ey-y)*(ey-y));
        // r1r2 = this.sw/2 + enemy.width/2;

        // if(d <= r1r2)
        //     console.log("충돌");
        // --------------------------------------------


        // 적기의 범위****
        ctx.beginPath();                // 범위 그려주기
        ctx.arc(this.x+25, this.y+30, this.width/2, 0, 2 * Math.PI);
        ctx.stroke();

        //ctx.Explosion.draw();
    }
    
    update(){   
        //console.log(newlec.enemies.length);

        // *** 삭제하는 방법 : 
        // 나를 참조하고 있느 것이 내가 갖고 있는 것이다. : 쌍방 참조
        // 즉, 트리 구조여야 한다. 체계적인 형태이다.

        // 콜 함수 : 
        // *** 콜백 함수 : 위임 함수, 나중에 호출할 때, 실행된다. 지금은 가지고 있다가(위임 함수의 의미) 나중에 함수가 호출된다. 언제 호출될 것인가가 이벤트(사건)이다.
        // *** 결론 : out of canvas라는 변수명에 가지고 있다가(on이라는 이름명) 너가 가지고 있다가 나중에 호출된다.

        // 이벤트를 설정할 수 있도록 제공해줘야 한다?
        // 결론 : enemy는 영역을 자기가 감지할 수 있지만, 영역 밖은 canvas의 값을 이용한다.


        this.y += this.speed;
        
        // ***** 초기 설정 :
        // if(this.y > 500)                        // this는 자신인데 자기가 자신의 함수(onOutofScreen)를 불러냄
        //     if(this.onOutOfScreen != null)
        //         this.onOutOfScreen(this);       // 넘겨받은 화살표 함수로부터 여기의 this가 en이 된다.

        if(this.y > 500)                        // this는 자신인데 자기가 자신의 함수(onOutofScreen)를 불러냄
            if(this.onRandomOutOfScreen != null)
                this.onRandomOutOfScreen(this);  
    }


    


}


  • confirmdlg.js
export default class ConfirmDlg{

    constructor(){

        // 버튼의 상태변수 생각!
        this.x = 100;
        this.y = 100;

        this.width = 400;       
        this.height= 200;


        // ** 데이터를 구조화해서 사용하기!!
        this.btnYes = {
            x:70,
            y:100,
            width:100,
            height:50,
            label:'YES'
        }

        this.btnNo = {
            x:230,
            y:100,
            width:100,
            height:50,
            label:'NO'
        }

        this.isVisible = false;

        this.onContinue = false;
        this.onEnd = false;

        this.onclick = false;
    }

    // 윈도우로써 사용자 입력 이벤트를 수신하기 위한 함수, notify를 수신하기 위한 함수이다!!
    notifyClick(x, y){
        // 자식이 있다면 자식에게도 이 이벤트를 통지해야 한다.
        // 하지만 자식이 아직 분가를 하지않았다면 내가 체크해서 통지를 하면 된다.

        // 내가 클릭된건가?(범위 주의!!***)
        if((this.x < x && x < this.x+this.width) && (this.y < y && y < this.y+this.height)){

            // 앗 내가 클릭되었는지 확인되었으면, 내 자식들 중에 클릭 되었는지 체크한다.
            console.log("앗?");

            // 자식들에게 통지?
            // -- 자식들이 있다면 통지하고
            // 아 맞다.. 자식 객체가 없지? 결국, 내가 해야하는 구나?

            // 자식들의 영역을 찾아서 세미버튼?****
            // 여기 고치는 것 숙제!!(범위 지정과 if구문!!)
            if(this.onclick)
                this.onclick(3);   // 1을 받으면 DLG, 2는 Yes, 3은 No 버튼!
                                   // 캔바스 화면 넘어가게 하기!에서 사용!

            // *******
            // 우리는 코드를 재사용할 수 있게 캡슐화하여 생산력 늘리기!
            // 우리는 남이 만든 것을 잘 활용하는 능력이 필요하다! 즉, 오픈소스를 잘 활용하는 능력!
            // 추가로 우리가 설계하는 것은 수업을 빨리 듣고 깨우쳐라!

        }
    }

    show(){
        this.isVisible = true;

    }

    update(){

    }

    draw(ctx){
        // ctx는 자기 크기를 자기가 정한다.
        if(!this.isVisible)
            return;

        let {x,y} = this;           // 이런식으로도 this값 가져오기

        ctx.fillStyle = '#FFF5';    // 투명도 조절!
        ctx.fillRect(x,y,this.width,this.height);     // this.ctx.width가 아니라 this.width이구나!!!

        ctx.fillStyle = '#000';
        ctx.strokeRect(x,y,this.width,this.height);   // this.ctx.width가 아니라 this.width이구나.
        
        //------------------

        ctx.fillStyle = 'black'
        ctx.font = 'bold 48px serif'
        ctx.fillText('Continue?', this.width/2, y+70);


        let btns = [this.btnYes, this.btnNo];
        //for(let btn ?)
        // 버튼 그리기!(for of 흐름 파악!)
        for(let btn of btns){

            // 중요1! 디스트럭쳐링!!
            let {x, y, width:w, height:h, label} = btn;   // 중요!! 요약하기!! 

            //console.log("정답: ", btn.x);

            ctx.fillStyle = 'gray';
            ctx.fillRect(this.x+x, this.y+y, w, h);
            ctx.fillStyle = 'black';
            ctx.strokeRect(this.x+x, this.y+y, w, h);
        
            ctx.font = 'bold 30px serif'
            ctx.fillText(label, this.x+x+20, this.y+135);

            // *** this.x : 캔버스 기준, x는 text박스 기준, 20은 마진!
        }
        // 버블링!! : 이 박스가 자식인 2개의 박스도 책임져야해서 물어볼 때, 주의!

    }
}


  • explosion.js
export default class Explosion{
    constructor(x,y){
        this.x = x || 300;
        this.y = y || 300;

        this.ix = 0;
        this.iy = 0;

        this.img = document.querySelector("#explosion"); 
        
        this.sw = this.img.width/4;     
        this.sh = this.img.height/5;    


        this.sx = this.sw * this.ix;
        this.sy = this.sh * this.iy;    

    }

    draw(ctx){
        ctx.drawImage(this.img, this.sx, this.sy, this.sw, this.sh,     // **** 에러발생 : img는 생성자에서 가져오기 때문에 this.img 이다!!
        this.x, this.y, this.sw, this.sh);     // 발이 기준이 되게 값을 조정해주자!! 
    }

    
}


  • background.js
export default class Background {
    
    constructor(){
        this.x=0;
        this.y=0;
        this.img = document.querySelector("#bg");

    }

    draw(ctx){
        ctx.drawImage(this.img, this.x, this.y);
    }

    update(){

    }
}

    


  • newlec.js

// 1. 이런 방식이 아니라
// export default class Exam
// {
//     x: 1
// }

// 2. 이런 방식으로 전역 객체 사용(클래스가 아니며, 객체 그 자체이다.)
// export default 
// {
//     x: 1
// }

// 3. 객체를 이용하는 것이 편하므로 하나의 class를 정해서 시작하자!

class Context{
    #enemies;   // private도 가능!? 하지만, 정의도 여기서해주고 사용도 여기서 해야 한다!

    constructor(){
        this.#enemies = null;
    }

    set enemies(value){                 // getter, setter 해결
        this.#enemies = value;
    }

    get enemies(){
        return this.#enemies;
    }

}

export default new Context();



2) WEB 소켓 : GET과 POST

  • GET : 문서를 달라는 요청
  • POST : 데이터를 제출하는 요청