본문 바로가기

Toy Project/react

Tetris 개발일지 (day 3)

 

1. canSettle 메소드 작성

canSettle (positions: [number, number][]) {
    for (let pos of positions) {
      if (
        pos[0] > this.state.stage.length - 1 ||
        pos[0] < 0 ||
        pos[1] > this.state.stage[0].length - 1 ||
        pos[1] < 0 ||
        this.state.stage[pos[0]][pos[1]].settle
      ) return false
    }

    return true
  }

 

  1. position의 x, y가 stage를 넘어설때
  2. 이미 stage에 놓아져 있을때

위와 같을때 false를 반환한다.

 

 

2. event handler 작성

stage component엔 내부적으로 사용자의 키보드에 따라 나눠지는 keydown 과 레벨에 따라 자동으로 빠르게 내려오는

interval 이벤트인 timer를 설정했으며, 게임 상태가 gameOver, pause일때 해당 이벤트들을 없애야 하므로

내부적인 이벤트 제어 메소드인 eventHandler를 작성한다.

 

  eventHandler (key: 'addKey' | 'removeKey' | 'addTimer' | 'removeTimer') {
    switch (key) {
      case 'addKey':
        if (!this.event.keyboard) {
          this.event.keyboard = true
          window.addEventListener('keydown', this.keyboardEvent)
        }
        break;
      case 'removeKey':
        if (this.event.keyboard) {
          this.event.keyboard = false
          window.removeEventListener('keydown', this.keyboardEvent)
        }
        break;
      case 'addTimer':
        if (this.event.timerId === false) {
          this.event.timerId = setInterval(this.timerEvent, this.timerSpeed)
        }
        break;
      case 'removeTimer':
        if (this.event.timerId !== false) {
          clearInterval(this.event.timerId)
          this.event.timerId = false
        }
        break
    }
  }

 

 

3. 블럭 움직임 메소드 작성전 준비

 

테트리스는 위로가는 움직임은 없고 좌, 우, 하 로 움직여야 한다.

단순히 좌, 우는 움직여야할 위치에 움직일수 있으면 놓으면 되고

 

아래로 움직이는 것은 아래로 움직여서 더이상 놓을수 없을때 다음과같은 추가 이벤트가 일어나야 한다.

  1. 아래로 더이상 갈수없으면 블럭 놓기
  2. 블럭을 놓았을때 블럭이 위치하는 row가 다 찼으면 제거 한후 점수 올리기 및 점수에 따라 timerSpeed 강화
  3. 새로운 블럭 생성 및 제일 상단에 위치하기
  4. 제일 상단에 위치시켰을때 블럭을 놓을수 없으면 gameOver 호출

 

주의해야될 사항

setState를 여러번 호출할경우 setState는 병합되며 경우에 따라서 비동기적으로 state가 바뀔수도 있다

따라서 다음과 같은 코드는 삼가해야한다.

state = {
  someThing: 1
}


// any method
this.setSate({
   someThing: 0
})
if (this.state.someThing === 0) return false

 

위를 피하기 위해서 block을 제거한 stage를 내보내는 메소드

block을 그린뒤 놓을지 안놓을지 설정한 stage를 가져오는 메소드를 작성하며 state를 직접적으로 바꾸는 다른 메소드들과의 혼동이 없도록 앞에 get 키워드를 붙인다.

 

  getStageConcatBlock (
    pos: [number, number][],
    background: string,
    isSettle: boolean,
    stage?: ModelStage
  ): ModelStage {
    const copyStage = stage ? [...stage] : [...this.state.stage]

    pos.forEach(posItem => {
      copyStage[posItem[0]][posItem[1]] = {
        background,
        settle: isSettle
      }
    })

    return copyStage
  }
  
 
  getStageRemoveBlock (pos: [number, number][], stage?: ModelStage): ModelStage {
    const copyStage = stage ? [...stage] : [...this.state.stage]

    pos.forEach(posItem => {
      copyStage[posItem[0]][posItem[1]] = {
        background: '',
        settle: false
      }
    })

    return copyStage
  }

 

 

4. 블럭 좌, 우 움직임

 

이벤트 추가 및 좌우로 움직였을때 알아서 판단한후 state.stage 를 바꿔주는 메소드 작성

아래로 가는 움직임을 위해서 "놓기"를 못할시 false를 반환하도록 한다.

  keyboardEvent (e: KeyboardEvent) {
    switch (e.keyCode) {
      case this.props.keyCode.left:
        this.judgeCanMoveAndGridStage([-1, 0])
        break;
      case this.props.keyCode.right:
        this.judgeCanMoveAndGridStage([1, 0])
        break;
      case this.props.keyCode.down:
        break;
      case this.props.keyCode.rotate:
        break;
      case this.props.keyCode.pastDown:
        break;
    }
  }
  
  
  judgeCanMoveAndGridStage (mover: [number, number]): boolean {
    const moverBlock = { ...this.state.userBlock }

    moverBlock.standardPos = [moverBlock.standardPos[0] + mover[0], moverBlock.standardPos[1] + mover[1]]
    moverBlock.pos = moverBlock.pos.map(pos => [pos[0] + mover[0], pos[1] + mover[1]])

    if (this.canSettle(moverBlock.pos)) {
      // remove PrevBlockStage
      const stageRemovePrevUserBlock = this.getStageRemoveBlock(this.state.userBlock.pos)

      // set stage by userBlock, settle is false
      this.setState({
        stage: this.getStageConcatBlock(moverBlock.pos, moverBlock.background, false, stageRemovePrevUserBlock),
        userBlock: moverBlock
      })

      return true
    } else {
      return false
    }
  }

 

 

4. 블럭 아래 움직임 - 1 ( 기본 움직임 )

  keyboardEvent (e: KeyboardEvent) {
    switch (e.keyCode) {
      case this.props.keyCode.left:
        this.judgeCanMoveAndGridStage([-1, 0])
        break;
      case this.props.keyCode.right:
        this.judgeCanMoveAndGridStage([1, 0])
        break;
      case this.props.keyCode.down:
        // 추가
        this.drop()
        break;
      case this.props.keyCode.rotate:
        break;
      case this.props.keyCode.pastDown:
        break;
    }
  }

  drop () {
    // timer가 돌아 drop하는 시간과 사용자의 drop event가 겹칠 경우를 생각해 event 지웠다가 다시 추가
    this.eventHandler('removeKey')
    this.eventHandler('removeTimer')
    
    // 만약 미래 블럭위치 "놓기"가 안될경우 현재 블럭위치 "놓기"
    if (!this.judgeCanMoveAndGridStage([0, 1])) this.blockSettle()
    this.eventHandler('addKey')
    this.eventHandler('addTimer')
  }
 

 

4. 블럭 아래 움직임 - 2 ( 놓기 이벤트 )

 

blockSettle (): void {
    // 현재 블럭을 settle한 stage 가져오기
    const userBlockSettleStage = this.getStageConcatBlock(this.state.userBlock.pos, this.state.userBlock.background, true)

    /*
      check stage by userBlock of y position [start]
    */
    const breakRowsStage = [...userBlockSettleStage]

    // step 1: get userBlock y pos list object
    // toCheckYPosList key is yPos and value is fill number as initialized zero
    const toCheckYPosList: { [yPos: number]: number } = {}
    for (let yIndex = 0; yIndex < this.state.userBlock.shape[0].length; yIndex++) {
      toCheckYPosList[this.state.userBlock.standardPos[1] + yIndex] = 0
    }

    // step 2: check stage y pos and toCheckYPosList value plus
    let breakCloumnNum = 0
    for (let xPos = 0; xPos < userBlockSettleStage.length; xPos++) {
      const row = userBlockSettleStage[xPos]

      for (const yPos in toCheckYPosList) {
        if (row[parseInt(yPos)].settle) toCheckYPosList[yPos]++
      }
    }

    // if toCheckYPosList value === stage row, splice and reset
    for (const yPos in toCheckYPosList) {
      if (toCheckYPosList[yPos] === userBlockSettleStage.length) {
        breakCloumnNum++

        for (let x = 0; x < breakRowsStage.length; x++) {
          breakRowsStage[x].splice(parseInt(yPos), 1)
          breakRowsStage[x] = [{ background: '', settle: false }, ...breakRowsStage[x]]
        }
      }
    }

    // call breakBlock event for parent component
    if (breakCloumnNum) this.props.breakRowsEvent(breakCloumnNum)
    /*
      check stage by userBlock of y position [end]
    */


    /*
      set new userBlock [start]
    */
    let newPredictionBlocks = [...this.state.predictionBlocks, getBlock()]
    const [newUserBlockShape] = newPredictionBlocks.splice(0, 1)

    const standardPos: [number, number] = [
      Math.floor((this.state.stage.length - newUserBlockShape.shape.length) / 2),
      0
    ]
    const newUserBlock: ModelUserBlockStage = {
      ...newUserBlockShape,
      standardPos: standardPos,
      pos: this.shapeBlockPosPareser({ ...newUserBlockShape, standardPos })
    }

    // if can't settle gameOver
    if (!this.canSettle(newUserBlock.pos)) {
      this.props.gameOverEvent()
    } else {
      this.setState({
        stage: this.getStageConcatBlock(newUserBlock.pos, newUserBlock.background, false, breakRowsStage),
        userBlock: newUserBlock,
        predictionBlocks: newPredictionBlocks
      })
    }
    /*
      set new userBlock [end]
    */
  }

 

5. 블럭 아래 움직임 - 3 ( break 및 gameover 호출을 위한 props 추가 )

 

/* 기본적인 동작

   score 계산
    == 100 * rows + 10 ( rows - 1 )
    ==> 10(11 * rows - 1)
    
   breakRows 계산 및 level과의 연동
    1. 현재 스테이지의 column / 2 만큼의 rows를 깨트렸다면 level + 1
   
   level에 따른 timer event speed 결정
     max 레벨인 20 레벨일 경우 200ms에 한번 drop event 하며
     1레벨일경우 4000ms
     =>
   stage.tsx 에 다음과 같은 get 추가

  private get timerSpeed () {
    // millisecond at one y line block drop
    // 20 level === 200, 1 level === 4000
    return 4000 - 200 * (this.props.level - 1)
  }
  
  이렇게 해서 eventHandler에 timer 설정할시 this.timerSpeed로 ms 설정했기때문에 자동으로 연동되며
  drop 이벤트일경우에만 breakRows 이벤트가 발생하고 drop 이벤트에는
  key, timer 각각 remove 했다가 add 하기 때문에 자동 갱신된다.
   
*/

// stage.tsx
interface StageProps {
  level: number;
  gameStatus: 'beforePlay' | 'play' | 'pause' | 'gameOver';
  // save data
  dataPredictionBlocks: ModelBlock[];
  dataStage: ModelStage;
  dataUserBlock: ModelUserBlock;
  keyCode: {
    left: number;
    right: number;
    down: number;
    rotate: number;
    pastDown: number;
  };
  // 추가
  breakRowsEvent (rows: number): void;
  // 추가
  gameOverEvent (): void;
}




// index.tsx

  breakRowsEvent (breakRows: number): void {
    // breakRows / (stage columns / 2) === +1 level
    const newBreakRows = this.state.scoreBoard.rows + breakRows
    const newScore = 10 * (11 * newBreakRows - 1)
    const level = (this.state.scoreBoard.level >= 20) ? this.state.scoreBoard.level : Math.floor(this.state.sendToStage.stage[0].length / 2) + 1

    this.setState({
      scoreBoard: {
        rows: newBreakRows,
        score: newScore,
        level
      }
    })
  }
  gameOverEvent () {
    this.setState({
      gameStatus: 'gameOver'
    })
  }
  
  
  render () {
    return (
      <div>
        <Stage
          dataStage={this.state.sendToStage.stage}
          dataPredictionBlocks={this.state.sendToStage.predictionBlocks}
          dataUserBlock={this.state.sendToStage.userBlock}
          level={this.state.scoreBoard.level}
          gameStatus={this.state.gameStatus}
          keyCode={this.state.sendToStage.keyCode}
          // 추가
          breakRowsEvent={this.breakRowsEvent}
          gameOverEvent={this.gameOverEvent}
        />
        <aside>
          <ScoreBoard {...this.state.scoreBoard} />
          <button onClick={this.gameBtHandler}>{
            this.state.gameStatus === 'play' ?
              'Pause' :
              'Play'
          }</button>
        </aside>
      </div>
    )
  }

 

 

6. 블럭 회전

 

회전은 전에 말했던 대로 간단히 x, y변경 및 x array reverse로 처리할 것이다.

여기서 회전할때 standardPos는 가장 낮은 x, y축 이다.

 

  keyboardEvent (e: KeyboardEvent) {
    switch (e.keyCode) {
      case this.props.keyCode.left:
        this.judgeCanMoveAndGridStage([-1, 0])
        break;
      case this.props.keyCode.right:
        this.judgeCanMoveAndGridStage([1, 0])
        break;
      case this.props.keyCode.down:
        this.drop()
        break;
      case this.props.keyCode.rotate:
        // 추가
        this.rotate()
        break;
      case this.props.keyCode.pastDown:
        break;
    }
  }
  
  rotate () {
    const shape = [...this.state.userBlock.shape]

    const xYReverse: boolean[][] = []
    shape.forEach((row, xIndex) => {
      row.forEach((yItem, yIndex) => {
        if (!xYReverse[yIndex]) xYReverse[yIndex] = []
        xYReverse[yIndex][xIndex] = yItem
      })
    })

    const rotateShape = xYReverse.reverse()
    const rotatePos = this.shapeBlockPosPareser({ standardPos: this.state.userBlock.standardPos, shape: rotateShape, background: this.state.userBlock.background })

    if (this.canSettle(rotatePos)) {
      this.setState({
        userBlock: {
          ...this.state.userBlock,
          shape: rotateShape,
          pos: rotatePos
        },
        stage: this.getStageRemoveBlock(this.state.userBlock.pos)
      }, () => {
        // setState가 일어난 이후에 실행되어야 할 함수는 이렇게 빼놓으며
        // 해당 함수는 현재 userBlock을 기준으로 그려주기 때문에 parameter로 [0, 0]을 넣는다
        this.judgeCanMoveAndGridStage([0, 0])
      })
    }
  }
  

 

다만 다음과 같은 상황을 고려해야 한다.

위와같은 경우엔 회전이 안되기 때문에 안될시 한번 좌로 움직여본다. (유저 친화적이게 만들기 위해)

 

 

  rotate () {
    const shape = [...this.state.userBlock.shape]

    const xYReverse: boolean[][] = []
    shape.forEach((row, xIndex) => {
      row.forEach((yItem, yIndex) => {
        if (!xYReverse[yIndex]) xYReverse[yIndex] = []
        xYReverse[yIndex][xIndex] = yItem
      })
    })

    const rotateShape = xYReverse.reverse()
    let rotatePos = this.shapeBlockPosPareser({ standardPos: this.state.userBlock.standardPos, shape: rotateShape, background: this.state.userBlock.background })


    // 여러곳에서 실행해야하는 코드 함수로 빼놓고
    const doSettle = () => {
      this.setState({
        userBlock: {
          ...this.state.userBlock,
          shape: rotateShape,
          pos: rotatePos
        },
        stage: this.getStageRemoveBlock(this.state.userBlock.pos)
      }, () => {
        this.judgeCanMoveAndGridStage([0, 0])
      })
    }

    if (this.canSettle(rotatePos)) {
      doSettle()
    } else {
      // try left x move
      for (let xMover = 1; xMover <= rotateShape.length; xMover++) {
        // 해당 rows만큼 -를 해준다.
        rotatePos = rotatePos.map(posItem => [posItem[0] - xMover, posItem[1]])

        if (this.canSettle(rotatePos)) {
          doSettle()
          break
        }
      }
    }
  }

 

 

 

 

이제 Tetris의 뼈대는 완성되었다 내일은 미리 떨어질 위치를 구해주는 shadow 바로 떨어지게하는 pastDown 이벤트 그리고 블럭 미리보여주기, gamover 됬을시 replay 하는 이벤트를 만들면 Tetris는 정말 끝난다 2일뒤엔 서버랑 연동이다.

 

여기까지의 commit

반응형

'Toy Project > react' 카테고리의 다른 글

동시성모드, selectable hydrate 관련 좋은 영상  (0) 2021.07.03
Tetris 개발일지 (day 4)  (0) 2020.05.27
Tetris 개발일지 (day 2)  (0) 2020.05.25
TETRIS 개발일지 (day1)  (0) 2020.05.22
TETRIS 개발일지 (prologue)  (0) 2020.05.22