Skip to content

C언어로 구현한 불완전 상태의 초기 버전 텍스트 게임 소스를 분석하고 문제를 고치거나 개선해 완성도를 높이는 과제 풀이

Notifications You must be signed in to change notification settings

elian118/invader

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

20 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

인베이더 프로그램 개선하기

☞ 유튜브에서 보려면 여길 클릭하세요!!

개요

C언어로 구현한 불완전 상태의 초기 버전 텍스트 게임 소스를 분석하고 문제를 고치거나 개선해 완성도를 높여야 한다.

목표

  1. 프로그램 개선
    • 최고점수 반영 및 표시
    • 점수별 게임 오버 메시지 변경
    • 난이도(레벨) 기능 추가
    • 오브젝트에 색상 넣기
    • 폭탄 기능 넣기
    • 효과음 넣기
    • 랭킹 추가
    • 기타
      • 코드 스타일 통일
      • 리팩터링
  2. 아래 발견된 버그 잡기
    • 위 아래 이동 불가
    • 적 비행체가 오른쪽 끝으로 가면 계속 그곳에만 체류하는 현상
    • 특정 조건에서 잔상이 지워지지 않고 남아 있는 현상
    • 게임 재시작을 묻는 절차에서 Y, N과 상관 없이 아무 키나 눌러도 종료되는 현상

구성

  1. 개발 및 실행환경

    • IDE: ZetBrains의 CLion 사용
    • 빌드: CMake CMakeLists.txt
          cmake_minimum_required(VERSION 3.31.6)
          project(invader)
      
          # utf-8 컴파일 옵션 적용
          add_compile_options($<$<CXX_COMPILER_ID:MSVC>:/source-charset:utf-8>)
          
          # 헤더 파일이 있는 디렉토리를 추가합니다.
          include_directories(include)
          
          # 소스 파일이 있는 디렉토리와 파일 지정
          add_executable(invader
              Invader.c
              src/Console.c
              src/MyChar.c
              src/Enemy.c
              src/Util.c
          )
          
          # 생성된 실행 파일에 라이브러리 연결
          target_link_libraries(invader winmm)
          
          # 프로젝트 루트 경로를 나타내는 매크로 정의
          target_compile_definitions(invader
              PRIVATE # 소스 파일에만 적용
              PROJECT_ROOT_DIR="${CMAKE_SOURCE_DIR}"
          )
    • 단, 'Visual Studio(VS)'에서도 일관성 있게 작동하도록 조치
      • VS는 CMakeLists.txt 파일의 존재를 확인하면 자동으로 CMake 빌드 및 실행환경으로 전환하며,
        아래와 같은 설정파일 CMakeSettings.json을 로컬 환경에 맞춰 자동 생성함
          {
              "configurations": [
                  {
                      "name": "x64-Debug",
                      "generator": "Ninja",
                      "configurationType": "Debug",
                      "inheritEnvironments": [ "msvc_x64_x64" ],
                      "buildRoot": "${projectDir}\\out\\build\\${name}",
                      "installRoot": "${projectDir}\\out\\install\\${name}",
                      "cmakeCommandArgs": "",
                      "buildCommandArgs": "",
                      "ctestCommandArgs": ""
                  }
              ]
          }
  2. 구조

    • 유형에 따른 파일 분리
      • include: 헤더파일(.h) 위치
      • src: 컴파일 대상인 소스파일(.c) 위치
      • assets: 효과음 등에 사용할 파일 위치
    • tree
        C:.
         │   CMakeLists.txt
         │   Invader.c
         │   README.MD
         │   ...                        
         ├───assets
         │       attack-match-4.wav
         │       attack-match.wav
         │       big-bomb-explosion.wav
         │       game-fail.wav
         │       level-complete.wav
         │       
         ├───include
         │       Console.h
         │       Main.h
         │       Util.h
         │                       
         ├───src
         │       Console.c
         │       Enemy.c
         │       MyChar.c
         │       Util.c
         │   ...    
         └───x64
         └───Debug
         Project1.exe
         Project1.pdb
  3. 코드 컨벤션

    • 들여쓰기: 4칸
    • 케이스
      • 상수: 스네이크 케이스(모두 대문자)
      • 전역 함수 또는 구조체: 파스칼 케이스
      • 그 외 변수 또는 함수: 카멜 케이스
    • 함수 길이가 과도하게 길어지면 관심사에 따라 분리 처리
    • 조건식을 3항식으로 처리 가능한 경우 코드 대체

과제 풀이


인베이더는 텍스트 기반 게임으로, 1978년 일본의 게임 개발사 타이토에서 개발한
"스페이스 인베이더(Space Invaders)"를 모티브로 했다.

텍스트 기반 게임은 커서를 감추고 지정된 매 틱마다 printf문을 출력하고
바로 다음 틱에서 이전 출력문을 지우고 다시 그리는 것의 반복으로 구현된다.

이를 처리하는 유틸들은 Console.c에 위치한다.

void InitConsole() {
	CONSOLE_CURSOR_INFO conInfo;

	conInfo.bVisible = FALSE;
	conInfo.dwSize = 1;

	hout = GetStdHandle(STD_OUTPUT_HANDLE);

	SetConsoleCursorInfo(hout, &conInfo);
}

위 코드는 프로그램 실행시 거의 극 초반에 실행되는 함수다. 셸 등의 터미널에서 커서가 보이지 않게 처리하고 있다.

void goToXY(UPOINT pt) {
	COORD pos;

	pos.X = pt.x;
	pos.Y = pt.y;

	SetConsoleCursorPosition(hout , pos);
}

위 코드는 InitConsole함수로 인해 보이지 않지만 분명히 어딘가에 존재하는 커서를 원하는 위치로 이동시키는 함수다.

이 함수는 다른 출력 코드와 함께 조합돼 특정 문자열을 커서가 위치한 곳에 출력하거나 " "의 문자열을 덮어씌워 출력문을 지우는 데 주로 활용된다.

다음은 특정 위치에 있는 모든 출력문을 지우는 함수다.

void ClearScreen() {
	UPOINT pos;

	for (int i = 1 ; i < 25 ; i++) {
		for (int j = 1; j < 80 ; j++) {
			pos.x = j;
			pos.y = i;
			goToXY(pos);
			printf(" ");
		}
	}
}

아래는 goToXY 함수를 사용해 플레이어의 비행기를 그리는 함수다.

void DrawMyShip(UPOINT *pt, UPOINT *oldPt) {
	// 이동했을때만 비행기 잔상 제거
	if (pt -> x != oldPt -> x || pt -> y != oldPt -> y) {
		goToXY(*oldPt);
		printf("     "); // 직전 비행기의 잔상을 빈 문자열로 덮어 지운다.
	}

	// 현재 위치에 비행기 새로 그리기
	goToXY(*pt);
	DrawColorMyShip(); // 비행기 그리기

	*oldPt = *pt; // 함수 종료 전에 반드시!! 이전 비행기 위치를 현재 위치로 업데이트 → 안 그러면, if절 앞에 잔상 제거 위치를 다시 잡는 코드를 추가해야 되고 조건식 넣고 지우개 칸도 넉넉히 해야되고 암튼 매우 복잡해진다.
}

다른 오브젝트. 그러니까 적 비행기, 미사일, 폭탄, 잔해 들도 로직의 차이와 실행 순서에 차이가 있을 뿐
위와 같이 화면 내 특정 위치로 이동해 오브젝트를 그리고 지우고를 반복하는 식으로 구현돼 있다.

이렇듯 텍스트 기반 게임은 출력하고 지우고를 반복해 착시를 일으켜 마치 움직이는 것처럼 보여지는 게 기본 동작 원리다.

그러면, 매 틱마다 그리고 지우고를 반복 실행하는 로직은 어디있는지 궁금할텐데...

그건 Invader.c 파일 play 함수에 구현돼 있다.

void play() {
	static UPOINT ptMyOldPos;
	DWORD         gThisTickCount = GetTickCount();
	DWORD         gCount = gThisTickCount;
	DWORD         Count = gThisTickCount;
	DWORD         bulletCount = gThisTickCount;
	UPOINT        ptScore, ptHi;
	int           enemySpeed = 500;

	score = 0; // 점수 초기화

	InitConsole();    
	InitMyShip();
	InitEnemyShip();
	
	ptThisMyPos.x = ptMyOldPos.x = MY_SHIP_BASE_POSX;
	ptThisMyPos.y = ptMyOldPos.y = MY_SHIP_BASE_POSY;
	...

	while(TRUE) {
	    gThisTickCount = GetTickCount();
	    ...
	    // 이전 틱과 150ms가 차이나면 실행 → 150ms마다 실행, 대상: 플레이어 관련 오브젝트
	    if (gThisTickCount - Count > 150) {
	        if (IsHitByEnemyBullet(ptThisMyPos) == 0) {
	            if (score > 2000) hiscore = score;
	            break;
            }
            // 주요 오브젝트 그리기
            CheckEnemy(enemyShip); // 이 안에 그리기 로직이 또 존재
            DrawMyBullet();
            DrawMyBomb();
            DrawMyShip(&ptThisMyPos , &ptMyOldPos);
            ...
            Count = gThisTickCount;
        }
        
        // 적 비행기 속도(500ms)마다 실행 - 대상: 적 비행기, 적 총알
        if (gThisTickCount - gCount > enemySpeed) {
            BulletShot();
            DrawBullet();
            CalEnemyShipPos(); // 적 비행기 위치 계산
            // DrawEnemyShip();	// 적 비행기 그리기
            if (CheckEnemyPos() == 1) break;
            gCount = gThisTickCount;
        }
    }
}

play함수는 main함수에서 직접적으로 실행되며, 이 main 함수만 봐도 프로그램이 대략 어떻게 굴러가는지 알 수 있다.

실행하자마자 콘솔 반복 출력을 담당하는 play 함수가 실행되고,

게임 오버 조건이 되면 play가 끝나며 gameOver 함수가 실행됨으로써 게임이 종료되는 식으로 작동한다.

void main(void) {
    UPOINT        ptEnd;
    int	loop = 1;
    
    ptEnd.x = 36;
    ptEnd.y = 12;
    while(loop) {
        play();
        gameOver(&ptEnd, &loop);
    }
}

  1. 프로그램 개선
    • 최고점수 반영 및 표시 + 점수별 게임 오버 메시지 변경 Invader.c
      void play() {
          ...
          goToXY(ptHi); // 좌상단 끝 위치로 커서 이동
          printf("최고 점수: %d | 남은 폭탄: %d", hiscore, myShipRestBomb);
          ...
      
      }
      void gameOver() {
          ...
          hiscore = score > hiscore ? score : hiscore; // 최고득점 정보 갱신
      
          char *printStr = killNum == 40 ? "축하합니다!! 모든 침입자를 격추했습니다!"
              : score == hiscore ? "최고 기록을 경신했습니다!"
              : "당신의 비행기는 파괴되었습니다.";
          ...
          // 이후 Y를 눌러 게임을 계속 진행하면 최고 점수가 변경돼 있다.
          ...
          goToXY(*ptEnd);
          printf("%s", printStr);
      
          ptEnd -> y += 1;
          printf("게임을 계속하시겠습니까? (y/n)\n");
      
          // Y, N 이외의 키 입력 무시
          do {
              input = _getch();
          } while (input != 'y' && input != 'n');
      }
    • 난이도(레벨) 기능 추가
      • Invader.c
            int	   level = 0; // 레벨(난이도)
            
            ...
            void play() {
                ...
                int enemySpeed = 500 - (level * 50) > 100 ? 500 - (level * 50) : 100;
                while(TRUE) {
                    gThisTickCount = GetTickCount();
                    ...
                    if (gThisTickCount - Count > 150) {
                        ...
                        printf("최고 점수: %d | 레벨: %d | 남은 폭탄: %d", hiscore, level + 1, myShipRestBomb);
                        ...
                        if (killNum > 20) enemySpeed = 150 - (level * 10) > 60 ? 150 - (level * 10) : 60; // 적 비행기 움직임 틱 500ms → 150ms 난이도 상승
            ...   
            void gameOver (UPOINT *ptEnd, int *loop) {
                ...
                printf(killNum > 20 ? "다음 단계로 넘어가시겠습니까? (y/n)" : "게임을 계속하시겠습니까? (y/n)\n");
                ...
                if (input == 'y') {
                    ...
                    killNum > 20 && level++;
                    ...
                }
                ...
    • 오브젝트에 색상 넣기
      • 색상 설정 유틸 추가 Util.c
           void ColorSet(int textColor, int backColor) {
               HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);
               // (backColor << 4) + textColor = 배경색 4비트 + 글자색 4비트 = 최종 색상 값 ex) 1010 0011 → 초록색 배경에 노란 글씨
               SetConsoleTextAttribute(handle, (backColor << 4) + textColor);
           }
      • 컬러풀한 플레이어 비행기 그리기 함수 추가 MyChar.c
            ...
            char  myShipShape[10] = "-i^i-";
            ...
            void DrawColorMyShip() {
                // "-i^i-"
                for (int i = 0; i < 5; i++) {
                    int colors[] = {11, 6, 9, 6, 11};
                    ColorSet(colors[i], 0);
                    printf("%c", myShipShape[i]);
                }
        
                ColorSet(7, 0); // White → 원래 색 조합으로 복귀
            }
      • 컬러풀한 적 비행기 그리기 함수 추가 MyChar.c
            ...
            char  enemyShipShape[5] = "^V^";
            ...
            void DrawColorEnemyShip() {
                for (int i = 0; i < 3; i++) {
                    int colors[] = {12, 5, 12};
                    ColorSet(colors[i], 0);
                    printf("%c", enemyShipShape[i]);
                }
        
                ColorSet(7, 0); // White → 원래 색 조합으로 복귀
            }
    • 폭탄 기능 넣기
      • 폭탄 발사 키 추가 Invader.c
            case 'd':
                if (gThisTickCount - bulletCount > 500) {
                    MyBombShot(ptThisMyPos);
                    bulletCount = gThisTickCount;
                }
                break;
      • 폭탄 발사 함수 추가 MyChar.c
            int    score, hiscore = 2000, killNum, myShipRestBomb;
            ...
        
            void InitMyShip() {
                ...
                myShipRestBomb = 3; // 남은 폭탄 수 초기화
            }
            ...
        
            void MyBombShot(UPOINT ptThisMyPos) {
                if (myShipRestBomb > 0) {
                    for (int i = 0; i < MAX_MY_BOMB ; i++) {
                        if (myShipBomb[i].flag == FALSE) {
                            myShipBomb[i].flag = TRUE;
                            myShipBomb[i].pos.x = ptThisMyPos.x + 2;
                            myShipBomb[i].pos.y = ptThisMyPos.y - 1;
                            myShipRestBomb--; // 폭탄 키 여럿 눌러도 남은 폭탄 숫자 소모 안 되도록 여기에 위치
                            playSound("assets/attack-match.wav");
                            break;
                        }
                    }
                }
            }
      • 폭탄 그리기 함수(Invader.c play 함수에서 매 틱마다 실행) 추가 MyChar.c
            void DrawMyBomb() {
                UPOINT ptPos, oldPos;
        
                for (int i = 0; i < MAX_MY_BOMB ; i++) {
                    if (myShipBomb[i].flag == TRUE) {
                        // 폭탄이 아무데도 맞지 않고 맨 위까지 도달한 경우
                        if (myShipBomb[i].pos.y < 1) {
                            myShipBomb[i].flag = FALSE; // 폭탄 제거
                            oldPos.x = myShipBomb[i].pos.x;
                            oldPos.y = myShipBomb[i].pos.y + 1; // y가 0에서 더 줄어들이 않으므로 1을 더함
                            goToXY(oldPos);
                            printf(" "); // 맨 위까지 도달한 폭탄 잔상 제거
                            continue;
                        }
        
                        oldPos.x = myShipBomb[i].pos.x;
                        oldPos.y = myShipBomb[i].pos.y;
                        --myShipBomb[i].pos.y;
                        ptPos.x = myShipBomb[i].pos.x;
                        ptPos.y = myShipBomb[i].pos.y;
                        goToXY(oldPos);
                        printf(" ");
                        goToXY(ptPos);
                        ColorPrint("☢️", 9, 0);
                    }
                }
            }
      • 폭탄 명중 후 폭발 및 정산 처리 Enemy.c
            // 적 비행기 (격추)상태 확인 - 매 틱마다 실행
            void CheckEnemy(ENEMYSHIP *enemyShip) {
                int i; // 변수 i를 두 곳 이상의 for문에서 사용중이므로 초기 지역 변수 j만 제거
                static BULLET boomBulletPos[MAX_MY_BULLET]; // 폭발한 총알 위치
                static BULLET bombBoomPos[MAX_ENEMY]; // 폭발한 폭탄 위치
                
                // 직전 틱의 격추 잔상("***") 지우기
                for (i = 0; i < MAX_MY_BULLET ; i++) {
                    if (boomBulletPos[i].flag == TRUE) {
                        goToXY(boomBulletPos[i].pos); // 격추된 위치로 커서 이동
                        printf("   "); // 지우기
                        boomBulletPos[i].flag = FALSE;
                    }
                }
        
                // 직전 틱의 폭발 잔상(십자대형 "***" 5개) 지우기
                for (i = 0; i < MAX_ENEMY && myShipRestBomb >= 0; i++) { // 폭발 범위에 있었는지 확인해야 하므로 모든 적의 격추상태를 확인
                    if (bombBoomPos[i].flag == TRUE) {
                        goToXY(bombBoomPos[i].pos);
                        printf("   ");
                        bombBoomPos[i].flag = FALSE;
                    }
                }
        
                // 총알을 순회하며 격추여부 확인
                CheckBulletHit(enemyShip, myShipBullet, boomBulletPos);
        
                // 폭탄을 순회하며 격추여부 확인
                if (myShipRestBomb >= 0) CheckBombHit(enemyShip, myShipBomb, bombBoomPos);
            }
      • 폭탄을 순회하며 격추여부 확인 Enemy.c
            void CheckBombHit(ENEMYSHIP *enemyShip, BULLET *myShipBomb, BULLET *bombBoomPos) {
                for (int i = 0; i < MAX_MY_BOMB; i++) {
                    if (myShipBomb[i].flag == TRUE) {
                        for (int j = 0; j < MAX_ENEMY; j++) {
                            if (enemyShip[j].flag == TRUE) {
                                int isShotDown = enemyShip[j].pos.x <= myShipBomb[i].pos.x &&
                                myShipBomb[i].pos.x <= (enemyShip[j].pos.x + 2) &&
                                (enemyShip[j].pos.y == myShipBomb[i].pos.y);
        
                                if (isShotDown) {
                                    int killedCount = 0;
                                    myShipBomb[i].flag = FALSE;
                                    Detonate(j, enemyShip, bombBoomPos);
                    	            UPOINT bombHitEnemy = {j / MAX_ENEMY_BASE_COL, j % MAX_ENEMY_BASE_COL};
                                    killedCount++;
        
                                    for (int k = 0; k < 4; k++) {
                                        // 반복 실행을 위한 상하좌우 2차원 좌표 배열
                                        int dXY[4][2] = {{0, -1}, {0, 1}, {-1, 0}, {1, 0}};
                                        UPOINT t = {bombHitEnemy.x + dXY[k][0], bombHitEnemy.y + dXY[k][1]};
        
                                        if (t.x >= 0 && t.x < MAX_ENEMY_BASE_ROW && t.y >= 0 && t.y < MAX_ENEMY_BASE_COL) {
                                            int idx = t.x * MAX_ENEMY_BASE_COL + t.y;
                                            if (enemyShip[idx].flag == TRUE) {
                                                Detonate(idx, enemyShip, bombBoomPos);
                                                killedCount++;
                                            }
                                        }
                                    }
                                    score += killedCount * 100;
                                    killNum += killedCount;
                                }
                            }
                        }
                    }
                }
            }
      • 폭탄에 의한 터짐 처리 함수 추가 Enemy.c → 반복되는 중복코드라 별도 함수로 분리
            void Detonate(int enemyIdx, ENEMYSHIP *enemyShip, BULLET *bombBoomPos) {
                enemyShip[enemyIdx].flag = FALSE;
                goToXY(enemyShip[enemyIdx].pos);
                ColorPrint("***", 12, 0);
                playSound("assets/big-bomb-explosion.wav");
                // 폭발 위치 저장
                bombBoomPos[enemyIdx].pos = enemyShip[enemyIdx].pos;
                bombBoomPos[enemyIdx].flag = TRUE; // 폭탄 티짐 및 소멸
            }
    • 효과음 넣기
      • 환경변수(매크로) 추가 CMakeLists.txt
        • 아래와 같이 프로젝트 경로를 지정하면 VS에서 실행해도 효과음이 정상적으로 재생됨
          → VS에서 CMake로 빌드하면, out 폴더를 절대경로 시작점으로 인식하므로 효과음 파일을 찾을 수 없음
        ...
        # 생성된 실행 파일에 라이브러리 연결
        target_link_libraries(Project1 winmm)
        
        # 프로젝트 루트 경로(/Project1)를 나타내는 매크로 정의
        target_compile_definitions(Project1
            PRIVATE # 소스 파일에만 적용
            PROJECT_ROOT_DIR="${CMAKE_SOURCE_DIR}"
        )
      • 효과음 재생 유틸 추가 Util.c
            void playSound(char* soundFile) {
                char fullPath[256]; // 충분한 크기의 스택 버퍼 할당
                snprintf(fullPath, sizeof(fullPath), "%s/%s", PROJECT_ROOT_DIR, soundFile);
                PlaySound(fullPath, NULL, SND_FILENAME | SND_ASYNC | SND_NODEFAULT);
            }
      • 효과음 재생 예시
            void gameOver (UPOINT *ptEnd, int *loop) {
                ...    
                char *soundFile = killNum == 40 || score == hiscore ? "assets/level-complete.wav"
                    : "assets/game-fail.wav";
        
                playSound(soundFile);
                ...
            }
    • 랭킹 추가
      • 랭킹을 기록하고 표시할 파일 입출력 관련 유틸 추가 Util.c
            void RenewRanking(int score, int ranking[], int size) {
                int insertIdx = -1; // 삽입 위치
        
                for (int i = 0; i < size; i++) {
                    if (score == ranking[i]) return; // 이미 같은 값이 있으면 종료
                    if (score > ranking[i]) {
                        insertIdx = i; // 삽입 위치 기록
                        break;
                    }
                }
        
                if (insertIdx == -1) return; // 삽입할 위치가 없다면 종료
                // 삽입 위치부터 배열 끝까지 한 칸씩 뒤로 밀기
                for (int i = size - 1; i > insertIdx; i--) ranking[i] = ranking[i - 1];
                ranking[insertIdx] = score; // 삽입 위치에 점수 입력
        
                updateRanking(ranking); // 랭킹 갱신
            }
        
            void UpdateRanking(int ranking[]) {
                mkdir("static");
        
                FILE *file = fopen("static/ranking.txt", "w");
                if (file != NULL) {
                    for (int i = 0; i < 5; i++) {
                        fprintf(file, "%d\n", ranking[i]);
                    }
                    fclose(file);
                }
            }
        
            void PrintRanking() {
                FILE *file = NULL;
                int ranking;
                UPOINT titlePos = {42, 15};
                UPOINT rankPos = {45, 17};
                int i = 0;
        
                file = fopen("static/ranking.txt", "r");
                if (file != NULL) {
                    for (i = 0; i < 5; i++) {
                        if (fscanf(file, "%d", &ranking) != EOF) {
                            if (i == 0) {
                                goToXY(titlePos);
                                printf("☆★☆ 최고기록 ☆★☆");
                            }
                            goToXY(rankPos);
                            ranking != 0 && printf("%d위. %d\n", i + 1, ranking);
                            rankPos.y++;
                        } else break;
                    }
                    fclose(file);
                }
            }
        
            void ReadRanking() {
                FILE *file = NULL;
                int rank;
        
                file = fopen("static/ranking.txt", "r");
                if (file != NULL) {
                    for (int i = 0; i < 5; i++) {
                        if (fscanf(file, "%d", &rank) != EOF) ranking[i] = rank;
                        if (i == 0) hiscore = rank > hiscore ? rank : hiscore;
                        else break;
                    }
                }
            }
      • 게임 종료 시 호출
            void gameOver (UPOINT *ptEnd, int *loop) {
                ...
        
                renewRanking(score, ranking, 5); // 랭킹 갱신
                hiscore = score > hiscore ? score : hiscore; // 최고득점 정보 갱신
                ...
        
                ptEnd -> y += 1;
                goToXY(*ptEnd);
                printf(killNum > 20 ? "다음 단계로 넘어가시겠습니까? (y/n)" : "게임을 계속하시겠습니까? (y/n)\n");
                printRanking(); // 랭킹 출력
            }
  2. 아래 발견된 버그 잡기
    • 위 아래 이동 불가
      • 키 입력 케이스 추가 invader.c
            #include <ctype.h>
            ...
            UPOINT ptThisMyPos; // 내 비행기 위치 포인터
            ...
            void play() {
                static UPOINT ptMyOldPos; // 내 비행기 직전 위치 포인터 → 다른 곳에서도 추적 가능해야 하므로 static 선언
                ...
                if (_kbhit()) {
                    char inputKey = tolower(_getch());
                    handleInput(inputKey, &ptThisMyPos, &ptMyOldPos, gThisTickCount, &bulletCount);
                }
                ...
            }
            ...
            void handleInput(char inputKey, UPOINT *ptThisMyPos, UPOINT *ptMyOldPos, DWORD gThisTickCount, DWORD *bulletCount) {
                switch (inputKey) {
                    ...
                    case 'i': //
                        ptMyOldPos.y = ptThisMyPos.y;
                        if (--ptThisMyPos.y < 1) ptThisMyPos.y = 1;
                        DrawMyShip(&ptThisMyPos , &ptMyOldPos);
                        break;
                    case 'k': // 아래
                        ptMyOldPos.y = ptThisMyPos.y;
                        if (++ptThisMyPos.y > MY_SHIP_BASE_POSY) ptThisMyPos.y = MY_SHIP_BASE_POSY; // 기본 위치 줄(23) 밑으로 내려가지 못하게 제한
                        DrawMyShip(&ptThisMyPos , &ptMyOldPos);
                        break;
                    default: break; // 안전장치로 추가
                }
            }
      • 위 아래 이동 직후 플레이어 비행기 잔상 완벽 제거 MyChar.c
            void DrawMyShip(UPOINT *pt, UPOINT *oldPt) {
                // 이동했을때만 비행기 잔상 제거
                if (pt -> x != oldPt -> x || pt -> y != oldPt -> y) {
                    goToXY(*oldPt);
                    printf("     ");
                }
        
                // 현재 위치에 비행기 새로 그리기
                goToXY(*pt);
                DrawColorMyShip();
        
                *oldPt = *pt; // 함수 종료 전에 반드시!! 이전 비행기 위치를 현재 위치로 업데이트 → 안 그러면, if절 앞에 잔상 제거 위치를 다시 잡는 코드를 추가해야 되고 조건식 넣고 지우개 칸도 넉넉히 해야되고 암튼 매우 복잡해진다.
           }
    • 적 비행체가 오른쪽 끝으로 가면 계속 그곳에만 체류하는 현상
      • 이유는 모르겠지만, play 함수의 키 입력 스위치케이스문에 기본 case 코드 추가 후 저절로 fix 됨 Invader.c
            void play() {
                ...
                if (_kbhit()) {
        	                switch (_getch()) {
                                ...
        	                    default: break; // 안전장치로 추가
        	                }
                      }
                ...
            }
    • 특정 조건에서 잔상이 지워지지 않고 남아 있는 현상
      • 텍스트 기반 게임은 로직이 깔끔하지 못하면 잔상이 남아 있거나 애니메이션이 의도와 다르게 동작하는 증상들이 흔히 나타난다.
      • Enemy.c
            void DrawEnemyShip() {
                UPOINT pos, posOld;
        
                for (int i = 0 ; i < MAX_ENEMY ; i++) {
                    if (enemyShip[i].flag == TRUE) {
                        posOld.x = ptOld[i].x;
                        posOld.y = ptOld[i].y;
                        // 조건식 추가: x좌표 위치가 -1가 되면 잔상 문자 하나만 남기라는 명령으로 인식하기 때문
                        pos.x = enemyShip[i].pos.x > 0 ? enemyShip[i].pos.x : 0;
                        pos.y = enemyShip[i].pos.y;
        
                        goToXY(posOld);
                        printf("    "); // 적 비행기 잔상 제거
        
                        goToXY(pos);
                        DrawColorEnemyShip();
                    }
                }
            }
      • MyChar.c
            void DrawMyShip(UPOINT *pt, UPOINT *oldPt) {
                // 이동했을때만 비행기 잔상 제거
                if (pt -> x != oldPt -> x || pt -> y != oldPt -> y) {
                    goToXY(*oldPt);
                    printf("     ");
                }
        
                // 현재 위치에 비행기 새로 그리기
                goToXY(*pt);
                DrawColorMyShip();
        
                *oldPt = *pt; // 함수 종료 전에 반드시!! 이전 비행기 위치를 현재 위치로 업데이트 → 안 그러면, if절 앞에 잔상 제거 위치를 다시 잡는 코드를 추가해야 되고 조건식 넣고 지우개 칸도 넉넉히 해야되고 암튼 매우 복잡해진다.
           }
    • 게임 재시작을 묻는 절차에서 Y, N과 상관 없이 아무 키나 눌러도 종료되는 현상
         void gameOver() {
             ...
             goToXY(*ptEnd);
             printf("게임을 계속하시겠습니까? (y/n)\n");
         
             // Y, N 이외의 키 입력 무시
             do {
                 input = tolower(_getch());
             } while (input != 'y' && input != 'n');
         }

About

C언어로 구현한 불완전 상태의 초기 버전 텍스트 게임 소스를 분석하고 문제를 고치거나 개선해 완성도를 높이는 과제 풀이

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published