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 : 데이터를 제출하는 요청