ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 지뢰찾기를 만들어보자 #9 - 사용자 인터페이스 변경 2/4
    기술 관련/etc 2022. 10. 10. 11:08

    지난번 글에서 지뢰찾기 게임의 사용자 인터페이스를 변경 했었다. https://mc500.tistory.com/675 이번 글에서는 이어서 점수와 깃발 갯수를 표시하는 부분을 변경해 보도록 하겠다.

    3. 점수 및 깃발 수 표시 변경

    Windows 지뢰 찾기에는 이 정보를 표시할 때 빨간색 숫자로 표시되었었다. 상태에 따라 표시가 되는 곳을 7개의 구역으로 나누어져 있기에 7 segment 라고 부르며 전광판이나 전자 회로에서 간단한 숫자를 표시할 때 사용된다.

    7 segment tile

    앞서 해왔던 방법으로 위의 이미지를 타일로 만들어 표시하는게 가장 쉬운 방법이다. 하지만 이번에는 HTML5 Canvas의 기능을 이용하여 표현해 보고자 한다.

    7 segement의 각 부분에 번호를 지정하고 이를 활용하고자 한다. 번호는 사실 프로그래밍 하는 사람 마음이므로 편하게 지정 할 수 있다. 이 지뢰찾기에서는 각 부분에 다음과 같은 숫자를 지정했고 아래 지정된 숫자 또는 기호에 대한 code table을 구현 했다.

    const CONST_7SEGMENT_CODES = {
      '0':[ true,  true,  true,  true,  true,  true, false],
      '1':[false, false,  true,  true, false, false, false],
      '2':[false,  true,  true, false,  true,  true,  true],
      '3':[false,  true,  true,  true,  true, false,  true],
      '4':[ true, false,  true,  true, false, false,  true],
      '5':[ true,  true, false,  true,  true, false,  true],
      '6':[ true,  true, false,  true,  true,  true,  true],
      '7':[ true,  true,  true,  true, false, false, false],
      '8':[ true,  true,  true,  true,  true,  true,  true],
      '9':[ true,  true,  true,  true,  true, false,  true],
      '-':[false, false, false, false, false, false,  true],
    }


    코드 테이블이 준비되었다면, 헤더 영역의 특정 위치에 segment 모듈 하나를 그리는 method를 만들어 보자

    draw7Segment(x, y, number) {
      let fillStyleOn = "rgba(256, 0, 0, 1)"
      let fillStyleOff = "rgba(103, 50, 36, 1)"
      let ctx = this.header.ctx
    
      x = x || 0
      y = y || 0
      let segment = CONST_7SEGMENT_CODES[number] || CONST_7SEGMENT_CODES['-']
    
      // Background
      ctx.beginPath()
      ctx.fillStyle = "rgba(36, 50, 36, 1)"
      ctx.rect(x, y, 18, 29)
      ctx.fill()
      ctx.closePath()
      
      // TODO: draw each segments
    }

    여기에 각각 segment들을 그리는 코드를 추가한다.

    더보기
      // 0. left-top
      ctx.beginPath()
      if (segment[0]) {
        ctx.fillStyle = fillStyleOn
      } else {
        ctx.fillStyle = fillStyleOff
      }
      ctx.strokeStyle = ctx.fillStyle
    
      ctx.moveTo(x+2, y+2)
      ctx.lineTo(x+2, y+13)
      ctx.lineTo(x+4, y+11)
      ctx.lineTo(x+4, y+1)
      ctx.lineTo(x+2, y+2)
    
      ctx.fill()
      ctx.stroke()
      ctx.closePath()
    
      // 1. top
      ctx.beginPath()
      if (segment[1]) {
        ctx.fillStyle = fillStyleOn
      } else {
        ctx.fillStyle = fillStyleOff
      }
    
      ctx.rect(x+5, y+1, 8, 3)
    
      ctx.fill()
      ctx.closePath()
    
      // 2. right-top
      ctx.beginPath()
      if (segment[2]) {
        ctx.fillStyle = fillStyleOn
      } else {
        ctx.fillStyle = fillStyleOff
      }
      ctx.strokeStyle = ctx.fillStyle
    
      ctx.moveTo(x+14, y+1)
      ctx.lineTo(x+14, y+11)
      ctx.lineTo(x+16, y+13)
      ctx.lineTo(x+16, y+2)
      ctx.lineTo(x+14, y+1)
    
      ctx.fill()
      ctx.stroke()
      ctx.closePath()
    
      // 3. right-bottom
      ctx.beginPath()
      if (segment[3]) {
        ctx.fillStyle = fillStyleOn
      } else {
        ctx.fillStyle = fillStyleOff
      }
      ctx.strokeStyle = ctx.fillStyle
    
      ctx.moveTo(x+14, y+17)
      ctx.lineTo(x+14, y+27)
      ctx.lineTo(x+15, y+27)
      ctx.lineTo(x+16, y+26)
      ctx.lineTo(x+16, y+15)
      ctx.lineTo(x+14, y+17)
    
      ctx.fill()
      ctx.stroke()
      ctx.closePath()
    
      // 4. bottom
      ctx.beginPath()
      if (segment[4]) {
        ctx.fillStyle = fillStyleOn
      } else {
        ctx.fillStyle = fillStyleOff
      }
    
      ctx.rect(x+5, y+25, 8, 3)
    
      ctx.fill()
      ctx.closePath()
    
      // 5. right-bottom
      ctx.beginPath()
      if (segment[5]) {
        ctx.fillStyle = fillStyleOn
      } else {
        ctx.fillStyle = fillStyleOff
      }
      ctx.strokeStyle = ctx.fillStyle
    
      ctx.moveTo(x+2, y+15)
      ctx.lineTo(x+2, y+26)
      ctx.lineTo(x+3, y+27)
      ctx.lineTo(x+4, y+27)
      ctx.lineTo(x+4, y+17)
      ctx.lineTo(x+2, y+15)
    
      ctx.fill()
      ctx.stroke()
      ctx.closePath()
    
      // 6. center
      ctx.beginPath()
      if (segment[6]) {
        ctx.fillStyle = fillStyleOn
      } else {
        ctx.fillStyle = fillStyleOff
      }
      ctx.strokeStyle = ctx.fillStyle
    
      ctx.moveTo(x+5, y+13)
      ctx.lineTo(x+4, y+14)
      ctx.lineTo(x+5, y+15)
      ctx.lineTo(x+13, y+15)
      ctx.lineTo(x+14, y+14)
      ctx.lineTo(x+13, y+13)
      ctx.lineTo(x+5, y+13)
    
      ctx.fill()
      ctx.stroke()
      ctx.closePath()

    이렇게 만들어진 draw7Segement()를 이용하여 게임 점수를 표시하는 method를 만든다. 7segment는 각각의 자릿수를 의미하므로 100의 자리, 10의 자리, 1의 자리에 대한 번호를 얻고 이를 array로 담아 연속된 형태로 호출한다.

    drawElapsedTime(seconds) {
      let digits = [
      if (seconds > 99) {
        let n = seconds - seconds%100
        seconds = seconds-n
        digits.push(n/100)
      } else {
        digits.push(0)
      }
      if (number > 9) {
        let n = seconds - seconds%10
        seconds = seconds-n
        digits.push(n/10)
      } else {
        digits.push(0)
      }
    
      digits.push(seconds)
      for (let i in digits) {
        this.draw7Segment(i*18, 0, digits[i])
      }
    },

    이렇게 만들어진 method는 게임 각각 초기화 코드인 initGames()와 elapsted_time을 변경하는 method곳에서 호출한다.

    this.initBoxes()
    this.refreshAll()
    this.drawFace()
    this.drawElapsedTime(this.elapsed_time)
    let diff = (new Date().getTime()) - self.start_time
    self.elapsed_time = Math.round(diff/1000)
    self.drawElapsedTime(self.elapsed_time)


    스마일 아이콘 왼쪽에 아래 표기된 시간과 동일하게 나타나는 것을 볼 수 있다.


    남은 깃발 개수도 표시할 drawLeftFlags() method도 drawElapsedTime()과 크게 다르지 않으므로, 자릿 수 구분을 위한 로직을 분리하여 buildDigits()로 만들고 이를 이용하도록 한다.

    buildDigits(number) {
      let digits = []
    
      if (number > 99) {
        let n = number - number%100
        number = number-n
        digits.push(n/100)
      } else {
        digits.push(0)
      }
      if (number > 9) {
        let n = number - number%10
        number = number-n
        digits.push(n/10)
      } else {
        digits.push(0)
      }
    
      digits.push(number)
      return digits
    },
    drawElapsedTime(seconds) {
      let digits = this.buildDigits(seconds)
      for (let i in digits) {
        this.draw7Segment(i*18, 0, digits[i])
      }
    },
    drawLeftFlags(flags) {
      let digits = this.buildDigits(flags)
      for (let i in digits) {
        this.draw7Segment(206+i*18, 0, digits[i])
      }
    },


    drawLeftFlags()는 initGames()와 깃발 수를 변경하는 flagup()에서 호출한다.

      this.initBoxes()
      this.refreshAll()
      this.drawFace()
      this.drawElapsedTime(this.elapsed_time)
      this.drawLeftFlags(this.mines)
    this.drawLeftFlags(this.mines - this.flags)

    그리고, 마지막으로 기존에 Text로 표시되던 코드를 화면에 표시되지 않도록 변경하면

    <div style="display:none;">
      <span>{{elapsed_time}} sec(s), {{mines - flags}} left</span>
    </div>


    4. 999초 시간 제한

    게임 진행 시간 표시는 999초 (대략 16분) 까지 표시된다. 만약 그 이상인 경우 게임은 자동으로 종료되도록 코드를 추가한다.

    self.drawElapsedTime(self.elapsed_time)
    if (self.elapsed_time > 999) {
      self.finishGame()
    }



    5. 깃발 수를 지뢰 갯수로 제한

    현재는 깃발수가 무제한으로 되어 있는데 남은 깃발의 개수를 표시하는 부분이 음수가 되므로 이에 대한 제한을 둔다.
    flagup() 에서 깃발을 다음과 같이 추가할 때 지뢰의 갯수와 비교를 하여 음수가 되지 않도록 한다.

    if (this.flags == this.mines) {
      return
    }



    4. 게임 실패 시 모든 상자 공개

    finishGame()에 revealBoxes()를 추가하여 게임에서 실패 했을 때 그대로 결과만 보여주는 것이 아닌 가려져 있던 모든 상자를 공개하여 상태를 확인 할 수 있도록 한다.

      this.start_time = 0
      this.gameover = true
    
      if (win) {
        this.highscores[this.level] = record
        this.drawFace(CONST_FACE.win)
      } else {
        this.revealBoxes()
        this.drawFace(CONST_FACE.lose)
      }
Designed by Tistory.