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();

=> 아무것도 안보이게 되었다…

image

  • 생각해보니 translate가 지속적으로 발동되며 이동한 값이 아닌.. x,y 고정좌표만큼 이동을 계속하는 것이였다!

    => player가 이동 시 증가/감소량 만큼만 적용되게 수정해야겠다

  1. 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]
     }
    
  2. Map class에서 이를 이용해서 이동하도록 변경

     update([translateX, translateY]) {
         // 화면이동
         this.ctx.translate(-translateX, -translateY)
     }
    
  3. 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]
}

=> 리팩토링 과정을 거치니 괜찮아졌다

화면이동2

잘 작동한다!!

화면 scaleRatio 설정

  • 현재 화면이 창의 크기에 맞춰 작동하는데, 새로고침을 하기 전까지 창을 움직여도 창이 고정되어 있다..

=> 창의 크기를 변경할 때마다 게임이 리셋되게 해야겠다.

  1. 비율에 맞춰 플레이어와 화면을 설정해주는 함수들을 생성 해준다

    • 크기 조절할 때 비율을 찾아주는 함수
     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();
     }
    
  2. 관련 메서드(이동) 들을 수정해준다

    • 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]
}

화면이동3

? 주인공 어디갔나요

  • 생각해보니 시작지점이 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)
}

화면이동4

잘 작동한다!

마지막 수정

  • 현재는 map이 전부 보이도록 설정하여 캐릭터와 적들이 너무 작게 보인다..

  • 그렇기에 카메라 width,height 변수를 만들어 보이는 화면을 조정해주어야 겠다.

  1. 카메라 변수 / map class 선언

     /* 카메라(화면) 설정 */
     const CAMERA_WIDTH = 500
     const CAMERA_HEIGH = 500
    
     const = camera = new Map(
         ctx,
         CAMERA_WIDTH,
         CAMERA_HEIGHT
     )
    
  2. 비율 함수에 적용

     //비율 구하기
     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와 비교해 스폰되는 아이템의 스탯도 다르게 설정하도록 조정!

  1. 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)
     }
    
  2. 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;
         }
     }
    
  3. 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);
     }
    
  4. 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 개념을 만들 필요가 있다!
  1. 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
         }
     }
    
  2. 함수 트리거

     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)

한줄 평 + 개선점

  • 현재 스테이지 업그레이드에 따라 색을 바꿔주는 기능을 추가하고 이것저것 디테일 부분을 많이 건들였지만..
    진도가 너무 안나갔다( 핵심 부분을 먼저 하도록 계속해서 리마인드하자)

체험

하지만 멋지죠?