Front-End 개발
맵과 카메라 고정
- 뱀파이어 서바이벌처럼 특정 맵의 크기를 정해주고, 사용자는 가운데에 플레이어가 고정된 화면을 보아야 한다!
화면 고정
-
player를 중심으로 화면을 고정시키기 위해 플레이어가 이동할 때 화면도 같이 이동하는 것처럼 그리려고 한다!
-
이를 위해 ctx.translate(x,y)를 사용해본다!
(translate를 선언한 후 그려지는 이미지들은 x,y 값만큼 이동한다음 그려지게 된다)
class Map {
constructor(ctx, width, height, scaleRatio) {
this.ctx = ctx;
this.canvas = ctx.canvas;
this.width = width;
this.height = height;
this.scaleRatio = scaleRatio;
}
update(player) {
// 화면이동
this.ctx.translate(player.x, player.y)
}
draw() {
}
}
export default Map;
/* loop되는 update 함수 내부 */
ctx.clearRect(0, 0, canvas.width, canvas.height);
// 업데이트
player.update(deltaTime);
monsters.update(player, score, deltaTime)
items.update(deltaTime)
score.update(deltaTime)
map.update(player)
// 그려주기
player.draw();
monsters.draw();
bullets.draw();
items.draw();
=> 아무것도 안보이게 되었다…
-
생각해보니 translate가 지속적으로 발동되며 이동한 값이 아닌.. x,y 고정좌표만큼 이동을 계속하는 것이였다!
=> player가 이동 시 증가/감소량 만큼만 적용되게 수정해야겠다
-
Player의 moving() 메서드에서 증가/감소량만 출력할 수 있도록 수정
//index.js에서 업데이트 때 사용되는 메서드 update(deltaTime) { return this.moving(deltaTime) } moving(deltaTime) { // 입력되는 값들을 배열로 저장 const up = ["ArrowUp", "W", "w", "ㅈ"]; const down = ["ArrowDown", "S", "s", "ㄴ"]; const left = ["ArrowLeft", "A", "a", "ㅁ"]; const right = ["ArrowRight", "D", "d", "ㅇ"]; //변화량 측정 변수 let translateX = 0; let translateY = 0; // keys 에 있는 입력 키들을 확인하기 위해 배열생성 const keys = Object.keys(this.keys) // array.some과 array.includes를 통해 key가 입력되었는지 확인 if (keys.some((e) => up.includes(e) && this.keys[e]) && this.y > 0) translateY = -this.speed * deltaTime; this.y += translateY if (keys.some((e) => down.includes(e) && this.keys[e]) && this.y < this.canvas.height - this.height) translateY = this.speed * deltaTime; this.y += translateY if (keys.some((e) => left.includes(e) && this.keys[e]) && this.x > 0) translateX = -this.speed * deltaTime; this.x += translateX if (keys.some((e) => right.includes(e) && this.keys[e]) && this.x < this.canvas.width - this.width) translateX = this.speed * deltaTime; this.x += translateX // 변화량 반환 return [translateX, translateY] }
-
Map class에서 이를 이용해서 이동하도록 변경
update([translateX, translateY]) { // 화면이동 this.ctx.translate(-translateX, -translateY) }
-
index.js 에서 두 메서드를 이어줌
// 업데이트에서 player.update를 입력으로 받음 map.update(player.update(deltaTime)); monsters.update(player, score, deltaTime) items.update(deltaTime) score.update(deltaTime) // 그려주기 player.draw(); monsters.draw(); bullets.draw(); items.draw();
뭔가 왼쪽과 위로 갔을 때 이상하게 가속도가 붙는다..?
moving(deltaTime) {
// 입력되는 값들을 배열로 저장
const up = ["ArrowUp", "W", "w", "ㅈ"];
const down = ["ArrowDown", "S", "s", "ㄴ"];
const left = ["ArrowLeft", "A", "a", "ㅁ"];
const right = ["ArrowRight", "D", "d", "ㅇ"];
let translateX = 0;
let translateY = 0;
// keys 에 있는 입력 키들을 확인하기 위해 배열생성
const keys = Object.keys(this.keys)
// array.some과 array.includes를 통해 key가 입력되었는지 확인
if (keys.some((e) => up.includes(e) && this.keys[e]) && this.y > 0)
translateY = -this.speed * deltaTime;
if (keys.some((e) => down.includes(e) && this.keys[e]) && this.y < this.canvas.height - this.height)
translateY = this.speed * deltaTime;
if (keys.some((e) => left.includes(e) && this.keys[e]) && this.x > 0)
translateX = -this.speed * deltaTime;
if (keys.some((e) => right.includes(e) && this.keys[e]) && this.x < this.canvas.width - this.width)
translateX = this.speed * deltaTime;
this.x += translateX
this.y += translateY
return [translateX, translateY]
}
=> 리팩토링 과정을 거치니 괜찮아졌다
잘 작동한다!!
화면 scaleRatio 설정
- 현재 화면이 창의 크기에 맞춰 작동하는데, 새로고침을 하기 전까지 창을 움직여도 창이 고정되어 있다..
=> 창의 크기를 변경할 때마다 게임이 리셋되게 해야겠다.
-
비율에 맞춰 플레이어와 화면을 설정해주는 함수들을 생성 해준다
- 크기 조절할 때 비율을 찾아주는 함수
function getScaleRatio() { const screenHeight = Math.min(window.innerHeight, document.documentElement.clientHeight); const screenWidth = Math.min(window.innerHeight, document.documentElement.clientWidth); // window is wider than the game width if (screenWidth / screenHeight < MAP_WIDTH / MAP_HEIGHT) { return screenWidth / MAP_WIDTH; } else { return screenHeight / MAP_HEIGHT; } }
- 인게임 크기를 조절해주는 함수
function createSprites() { // 비율에 맞게 조정 // 유저 크기 const playerWidthInGame = PLAYER_WIDTH *scaleRatio; const playerHeightInGame = PLAYER_HEIGHT* scaleRatio; // 맵 크기 const mapWidthInGame = MAP_WIDTH *scaleRatio; const mapHeightInGame = MAP_HEIGHT* scaleRatio; // 총알 크기 const bulletSizeInGame = BULLET_SIZE *scaleRatio // 몬스터 크기 const monsterSizeInGame = MONSTER_SIZE* scaleRatio // 아이템 크기 const itemSizeInGame = ITEM_SIZE * scaleRatio player = new Player( ctx, playerWidthInGame, playerHeightInGame, PLAYER_MAX_HEALTH, PLAYER_DAMAGE, PLAYER_SPEED, scaleRatio ); map = new Map( ctx, mapWidthInGame, mapHeightInGame ) bullets = new BulletsController( ctx, BULLET_ATTACK_SPEED, BULLET_SPEED, bulletSizeInGame, scaleRatio ) monsters = new MonstersController( ctx, MONSTER_SPAWN_SPEED, monsterSizeInGame, scaleRatio ) items = new ItemsController( ctx, ITEM_SPAWN_SPEED, itemSizeInGame ) score = new Score( ctx, scaleRatio ); }
- 화면 초기화 함수
function setScreen() { scaleRatio = getScaleRatio(); canvas.width = MAP_WIDTH * scaleRatio; canvas.height = MAP_HEIGHT * scaleRatio; createSprites(); }
-
관련 메서드(이동) 들을 수정해준다
- Monster로 예를 들자면 moving() 메서드에서 x, y 값을 조절할 때 곱하여 사용
// 받아온 비율을 곱하여 이동되는 칸을 조절한다 this.x += Math.cos(direction) * (this.speed + accSpeed) * deltaTime * this.scaleRatio this.y += Math.sin(direction) * (this.speed + accSpeed) * deltaTime * this.scaleRatio
화면 조정 인식
- index.js 에서 실행 함수와 화면이 조정되면 다시 시작하도록 addEventListener를 사용했다.
setScreen()
// 화면이 조절될 시
window.addEventListener('resize', setScreen);
// 화면 방향 변경시
if (screen.orientation) screen.orientation.addEventListener('change', setScreen);
map 표현
- map의 크기를 시각적으로 알기 위해 사각형으로 그려주기로 했다!
//map class의 메서드
constructor(ctx, width, height) {
this.ctx = ctx;
this.canvas = ctx.canvas;
this.width = width;
this.height = height;
this.startX = Math.trunc((this.canvas.width - this.width) / 2)
this.startY = Math.trunc((this.canvas.height - this.height) / 2)
}
draw() {
this.ctx.fillStyle ="rgb(130, 218, 162)"
// startX와 startY는 canvas의 범위에서 map이 어디서 시작되는지 알기 위해 설정했다
this.ctx.fillRect(this.startX, this.startY, this.width, this.height);
}
-
이제 맵의 크기에서 벗어나지 못하도록 player의 class 를 조정해야하는데..
=> 이미 this.canvas의 범위를 못벗어나도록 되있으니 이걸 map으로만 바꾸면..?!
//player class
constructor(ctx, map,width, height, maxHealth, damage, speed, scaleRatio) {
// 메서드에 이용하기위해 부여받음
this.ctx = ctx
// map class를 받아 직접 이용하는 것으로 수정했다
this.canvas = map
this.x = this.canvas.width / 2 + this.canvas.startX
this.y = this.canvas.height / 2 + this.canvas.startY
// size를 넓이와 높이로 분리하여 변수로 지정
this.width = width
this.height = height
this.maxHealth = maxHealth;
this.health = maxHealth;
this.damage = damage
this.speed = speed
this.scaleRatio = scaleRatio
this.isDamaged = false;
this.keys = {};
}
- 생각해보니 시작지점을 기준으로 조정해주어야 한다!
//player의 메서드
moving(deltaTime) {
const up = ["ArrowUp", "W", "w", "ㅈ"];
const down = ["ArrowDown", "S", "s", "ㄴ"];
const left = ["ArrowLeft", "A", "a", "ㅁ"];
const right = ["ArrowRight", "D", "d", "ㅇ"];
let translateX = 0;
let translateY = 0;
const keys = Object.keys(this.keys)
// 뒤에 canvas.startY를 못넘어가도록 조정
if (keys.some((e) => up.includes(e) && this.keys[e]) && this.y > this.canvas.startY)
translateY = -this.speed * deltaTime * this.scaleRatio;
// 뒤에 canvas.startY + canvas.height을 못넘어가도록 조정
if (keys.some((e) => down.includes(e) && this.keys[e]) && this.y < this.canvas.height + this.canvas.startY - this.height)
translateY = this.speed * deltaTime * this.scaleRatio;
// 뒤에 canvas.startX를 못넘어가도록 조정
if (keys.some((e) => left.includes(e) && this.keys[e]) && this.x > this.canvas.startX)
translateX = -this.speed * deltaTime * this.scaleRatio;
// 뒤에 canvas.startX + canvas.width를 못넘어가도록 조정
if (keys.some((e) => right.includes(e) && this.keys[e]) && this.x < this.canvas.width + this.canvas.startX - this.width)
translateX = this.speed * deltaTime * this.scaleRatio;
this.x += translateX
this.y += translateY
return [translateX, translateY]
}
? 주인공 어디갔나요
- 생각해보니 시작지점이 0,0 으로 고정되어 있어 처음부터 화면을 다르게 시작하도록 조정해주어야 겠다!
// 화면 구성 함수
function setScreen() {
scaleRatio = getScaleRatio();
// map 크기에 비해 키워 화면이 잘리지 않도록
canvas.width = MAP_WIDTH * scaleRatio * 2;
canvas.height = MAP_HEIGHT * scaleRatio * 2;
createSprites();
// 시작 지점을 map 이 시작되는 지점 만큼 빼서 적용
ctx.translate(-map.startX, -map.startY)
}
잘 작동한다!
마지막 수정
-
현재는 map이 전부 보이도록 설정하여 캐릭터와 적들이 너무 작게 보인다..
-
그렇기에 카메라 width,height 변수를 만들어 보이는 화면을 조정해주어야 겠다.
-
카메라 변수 / map class 선언
/* 카메라(화면) 설정 */ const CAMERA_WIDTH = 500 const CAMERA_HEIGH = 500 const = camera = new Map( ctx, CAMERA_WIDTH, CAMERA_HEIGHT )
-
비율 함수에 적용
//비율 구하기 function getScaleRatio() { const screenHeight = Math.min(window.innerHeight, document.documentElement.clientHeight); const screenWidth = Math.min(window.innerHeight, document.documentElement.clientWidth); // 창이 게임보다 길면 if (screenWidth / screenHeight < MAP_WIDTH / MAP_HEIGHT) { return screenWidth / MAP_WIDTH; } else { return screenHeight / MAP_HEIGHT; } } // 화면구성 function setScreen() { scaleRatio = getScaleRatio(); // map 크기에 비해 키워 화면이 잘리지 않도록 canvas.width = MAP_WIDTH *scaleRatio* 1.1; canvas.height = MAP_HEIGHT *scaleRatio* 1.1; createSprites(); ctx.translate(-camera.startX, -camera.startY) }
아이템과 몬스터
- 현재 아이템과 몬스터가 랜덤으로 스폰이 되는 경향이 있는데 이를 조정해줄 것이다!
아이템 스폰
-
현재의 아이템은 시간이 지날 때마다 스폰을 하지만, 대신 몬스터가 죽었을 경우 일정확률로 드랍하도록 만드는게 좋을 것 같다!
-
추가로 item 정보들을 읽어 stage와 비교해 스폰되는 아이템의 스탯도 다르게 설정하도록 조정!
-
itemController class 수정
// 기존에 사용하던 spawnSpeed 와 spawnCool을 삭제하였다 constructor(ctx, map) { this.ctx = ctx this.itemStat this.canvas = map } // 생성하는 로직에 itemStat을 이용하기로 조정했다 (stage 레벨은 index.js에서 판정) createItem(x, y, itemStat) { const size = 30 //itemStat을 객체 형태로 받아 사용 const {id, score, health, damage, attackSpeed, speed}= itemStat const item = new Item(this.ctx, x, y, size, size, id, damage, health, attackSpeed, speed, score) this.items.push(item) }
-
item class 수정
class Item { // 사용할 스탯들을 정리 (id , attackSpeed 추가) constructor(ctx, positionX, positionY, width, height, id, damage, heal, attackSpeed, speed, score) { this.ctx = ctx this.canvas = ctx.canvas; this.x = positionX; this.y = positionY; this.width = width; this.height = height; this.id = id; this.damaged = damage; this.heal = heal; this.attackSpeed = attackSpeed this.speed = speed; this.score = score; this.pickup = false; } }
-
monster가 죽을 때 확률 계산으로 드롭되도록 설정
dead(itemsController, itemStat, prob = 0, idx) { const monster = this.monsters[idx] //확률로 아이템 생성 const check = Math.trunc(Math.random * 100) if (check <= prob) itemsController.createItem(monster.x, monster.y, itemStat) this.monsters.splice(idx, 1); }
-
monster가 죽을 때(총에 맞고) 트리거 연결
// 총알 충돌 const monsterIdx = monsters.monsters.findIndex((monster) => bullets.colliedWith(monster)) // 충돌이 일어났을 경우 if ( monsterIdx !== -1) { //충돌한 총알 찾기 const bulletIdx = bullets.bullets.findIndex((bullet) => bullet.hitOrOut === true) const damage = bullets.bullets[bulletIdx].damage - monsters.monsters[monsterIdx].defense // 데미지 적용 if (bullets.bullets[bulletIdx].hitOrOut) monsters.monsters[monsterIdx].damaged(damage) // 죽었는지 확인 + 아이템 정보 삽입 if (monsters.monsters[monsterIdx].health < 1) monsters.dead(items, itemStat, prob, monsterIdx) }
데이터 가공
- 데이터 테이블(json 파일)에 있는 데이터를 가공하여 게임에 집어넣어야 된다!
스테이지 이동
- item와 monster 의 경우 unlock 파일의 stage_level에 따라 스폰되는 시기가 다르기 때문에 stage 개념을 만들 필요가 있다!
-
stageMove 함수 생성
function stageMove(stage, time) { const stageInfo = getGameAssets.stages const data = stageInfo.data if (stage?.level) { return data[0] } else { const nextStage = data.find((e) => e.level === stage.level++) if (nextStage?.time <= time) return nextStage else return stage } }
-
함수 트리거
let stage = null /*update 함수 내부*/ const [ scorePoint, highScore, time]= score.getScore() stage = stageMove(stage, time)
해금 정보
- 스테이지에 따라 해금된 요소들을 저장해주는 함수를 만든다!
let unlockItem = null
let unlockMonster = null
// 해금요소 저장
function unlocked(stage) {
const assets = getGameAssets()
const unlockInfo = assets.unlock
const itemInfo = assets.item
const monsterInfo = assets.monster
const data = [unlockInfo.data, itemInfo.data, monsterInfo.data ];
const unlocked = data[0].filter((e) => stage.level >= e.stage_level).map((e) => e.target_id)
// 해금 요소 저장
unlockItem = data[1].findLast((e) => unlocked.includes(e.id))
unlockMonster = data[2].findLast((e) => unlocked.includes(e.id))
}
/*update 함수 내부*/
//스테이지 이동
stage = stageMove(stage, time)
//해금
unlocked(stage)
해금된 요소 매핑
- 각 controller class에 메서드를 만들어 해금요소들을 적용 시켜준다!
// 몬스터 Controller 메서드 수정
updateMonster(monsterStat) {
this.monsterStat = monsterStat
}
createMonster(monsterStat) {
const {id , health, defense, speed } = monsterStat
const positionX = Math.random() * this.canvas.width;
const positionY = Math.random() * this.canvas.height;
const size = Math.trunc(Math.random() * this.size) + this.size;
const monster = new Monster(this.ctx, positionX, positionY, size, size, id, health, defense, speed, this.scaleRatio)
this.monsters.push(monster)
}
// 아이템 Controller 메서드 수정
updateItem(itemStat) {
this.itemStat = itemStat
}
createItem(x, y) {
const size = 30
const {id, score, health, damage, attackSpeed, speed} = this.itemStat
const item = new Item(this.ctx, x, y, size, size, id, damage, health, attackSpeed, speed, score)
this.items.push(item)
}
- 해금 정보 획득 이후 적용해준다!
/*update 함수 이후*/
unlocked(stage)
items.updateItem(unlockItem)
monsters.updateMonster(unlockMonster)
한줄 평 + 개선점
- 현재 스테이지 업그레이드에 따라 색을 바꿔주는 기능을 추가하고 이것저것 디테일 부분을 많이 건들였지만..
진도가 너무 안나갔다( 핵심 부분을 먼저 하도록 계속해서 리마인드하자)
하지만 멋지죠?