Notice
Recent Posts
Recent Comments
Link
«   2024/07   »
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30 31
Archives
Today
Total
관리 메뉴

시작은 0부터

13. Get 과 Set, 클래스 활용해서 슈팅게임 만들기 본문

C# 학습일지

13. Get 과 Set, 클래스 활용해서 슈팅게임 만들기

0base 2022. 7. 19. 00:21

함수를 호출할 때 Get 과 Set 이라는 용어를 쓴다. 각 뜻의 차이는 다음과 같다.

 

Get

: 함수 외부에서 함수 내부 값을 가져오는 것. 가령 A라는 함수 안에 int a = 1 라고 선언되어있을 때, 함수 안에 a 값을 가져온다면 1인 a값을 그대로 가져오는 것이다. 

 

 Class A
 {
 	int a;
    
    public int GetA()
    {
    	return a;
    }
 }

▲ 함수가 GetA 함수가 호출되면 A 클래스 안에 있는 a를 반환한다.

 

 

Set

: 함수 외부에서 함수 내부에 값을 입력하는 것.  가령 함수를 호출하는 부분에서 a값을 넣으면 함수에서 입력받은 a를 받아와서 처리한다.  void 일 경우 반환값이 없기 때문에 입력받기만 하고, return이 있을 경우 입력값을 받아 대입하여 얻은 결과값을 반환한다. 

Class A
{
    B b = new B; 
    int a = 1;
        
	public void Start()
    {
		
        b.SetA(a);
    }
   
}

Class B
{
	int A = 0;
    
	public void SetA(int a)
    {
    	A = a;
    }
}

▲ 예시에서 SetA의 형식이 void로 되어 있는 이유는 반환값이 없기 때문이다.

 

Void

: 반환(return)할 값이 없을 때 사용한다. 정수 형태로 값을 반환할 때는 int , 참/거짓 형태로 값을 반환할 때는 bool 등 반환하는 형태에 따라 형식이 다르다.

 

 

슈팅 게임 클래스 구조 : 1. Program 클래스(함수 진행구조)

internal class Program
    {
        static void Main(string[] args)
        {
            GameLoop gl = new GameLoop();

            gl.Awake();
            gl.Start();
            while(true)
            {
                gl.Update();
                //gl.Render();
            }
        }
    }

 

게임이 어떻게 돌아가는 지를 함수로 나눈 구조다. 처음에 Awake 함수 안에 설정값 등이 들어가고, Start에서 반복문의 시작에만 들어가는 값들, 초기값 등을 넣고 그 이후부터는 반복문으로 업데이트로 프레임마다 갱신할  Update값들과 화면에 출력할 Render 값을 호출한다. 교수님께서 클래스 설명을 위해 간단하고 직관적으로 표현한 코드 구조다. 랜더는 깜빡임 현상을 잡기 위해 Update 함수 안에 있는 딜레이 값이 충족되었을 때 실행되는 부문 안에 옮겨넣어 해당 부분은 주석처리했다. Render를 그냥 반복문 안에 넣으면 딜레이가 충족되지 않더라도 실행되기 때문에 매우 빠른 속도로 깜빡여 시각적으로 불편하다.

 

2. GameLoop 클래스

internal class GameLoop
    {
        //멤버변수
        Player p;
        Enemy e;
        Score score;
        Bullet bullet = null;
        int oldTime = 0;
        
        //생성단계
        public void Awake()
        {
            Console.BufferHeight = Console.WindowHeight = 40;
            Console.BufferWidth = Console.WindowWidth = 30;
            Console.CursorVisible = false;
            
            p = new Player();
            e = new Enemy();
           bullet = new Bullet();
            score = new Score();
        }

        public void Start()
        {
            p.Init();
            e.Init();
        }

        public void Update()
        {
            int curTime = System.Environment.TickCount & Int32.MaxValue; //음수출력방지

            if (curTime - oldTime > 10)
            {
                oldTime = curTime;
                
                //e.Update();
                if (bullet != null)
                {
                    bullet.Update();
                    //충돌체크
                    if (bullet.GetPosX() == e.GetPosX() &&
                        bullet.GetPosY() == e.GetPosY())
                    {
                        //1. 총알이 삭제되어야 한다.
                        bullet.SetIsAlive(false);
                        //2. 적도 삭제
                        e.SetIsAlive(false);
                        e.Init();
                        //3. 점수 증가
                        score.AddScore(1);
                    }
                }
                Render(); // 딜레이를 주어 화면 깜빡임 최소화
            }
           
            if (Console.KeyAvailable)
            {
                ConsoleKeyInfo cki = Console.ReadKey();
                switch(cki.Key)
                {
                    case ConsoleKey.LeftArrow:
                        p.Move_Left();
                        break;
                    case ConsoleKey.RightArrow:
                        p.Move_Right();
                        break;

                    case ConsoleKey.A:
                        {
                            //케이스 안에서 변수 선언시 중괄호 넣을것
                            if (bullet == null || bullet.GetIsAlive() == false)
                            {
                                
                                int x = p.GetPosX();
                                int y = p.GetPosY();

                                //bullet = new Bullet();
                                bullet.SetPosX(x);
                                bullet.SetPosY(y);
                                
                                if(y != 0 && bullet.isAlive == false)
                                {
                                    bullet.isAlive = true;
                                }
                            }
                        }
                        break;
                }
            }
        }

        //player.Update();        
        public void Render()
        {
            Console.Clear();
            score.Render();
            p.Render();
            e.Render();
            if (bullet != null && bullet.isAlive == true)
            {
                bullet.Render();
            }
        }   
    }

실질적인 코드 구조를 그리는 클래스. 반복문이 돌면서 게임의 프레임이 갱신되며 진행되므로 이 코드에서 갱신되는 동안 어떤 함수를 실행할지를 결정한다. 클래스 안 앞부분에 클래스 내부 함수들이 공용으로 사용할 수 있는 변수들을 선언한다. 이 때 선언하는 변수를 '멤버변수'라고 한다. 멤버변수는 함수 밖 클래스 안에 있기 때문에 함수가 사라지더라도, 즉  { } 괄호를 벗어나더라도 값이 사라지지 않는다. 함수 내부에서 선언한 변수는 함수 밖을 벗어나면 값이 사라진다.

멤버변수는 언제나 기억되는 리멤버변수로 기억하면 될것같다.

 

반복문 내부에 딜레이를 만들면 반복문이 딜레이만큼 지연되어 진행되기 때문에 프레임을 해당 부분에서 조절할 수 있다. GameLoop.Render 함수를 Program 클래스에서 GameLoop클래스 내부 Update함수로 옮긴 이유도 이와 관련이 있는데, 프로그램이 실질적으로 실행되는 메인인데, 루프 업데이트 실행 뒤에 루프 랜더를 실행하는 구조가 되면 업데이트에서 딜레이가 충족되지 않더라도 바로 뒤에서 랜더를 실행해버리기 때문에 랜더에 딜레이가 적용되지 않는 문제가 발생했다. 그래서 랜더 호출을 딜레이 충족 조건문 안에다 넣음으로써 딜레이가 되었을 때 랜더가 호출되는 방법을 적용했다.

 

3. Player 클래스

internal class Player
    {
        int pos_x;
        public int GetPosX() { return pos_x; }
        int pos_y;
        public int GetPosY() { return pos_y; } 
        public void Init()
        {
            //위치 초기화
            pos_x = 15;
            pos_y = 37;
        }

        //왼쪽으로 이동
        public void Move_Left()
        {
            --pos_x;
            if (pos_x < 0)
                pos_x = 0;
        }

        public void Move_Right()
        {
            ++pos_x;
            if(pos_x > 30)
                 pos_x = 30;
        }

        public void Render()
        {
            Console.SetCursorPosition(pos_x, pos_y);
            Console.Write("P");
        }
    }

Init 함수는 처음에 한번만 실행되는 초기 세팅값을 지정하는 함수다. 화면 하단 가운데 배치하기 위한 초기 위치설정을 해주고 루프문에서 키입력(좌,우)에 따라 그에 해당하는 함수를 호출받고 실행함으로써 x값을 움직이고, 최대 움직임을 설정한다.(맵 이탈 방지)

 

 

4. EnemyClass

internal class Enemy
    {
        bool isAlive;
        int pos_x;
        public int GetPosX()
        {
            return pos_x;
        }
        //세로 위치
        int pos_y;
        public int GetPosY()
        {
            return pos_y;
        }

        public bool GetIsAlive()
        {
            return isAlive;
        }

        public void SetIsAlive(bool alive) { isAlive = alive; }

        //충돌체크
        bool check(Bullet bullet)
        {
            return (bullet.GetPosX() == pos_x &&
                bullet.GetPosY() == pos_y);
        }

        //초기화 함수
        public void Init()
        {
            isAlive = true;
            Random rd = new Random();
            pos_x = rd.Next(0, 30);
            pos_y = 4;
        }

        public void Update()
        {
            Random rd = new Random();
            if(rd.Next() % 2 == 0)
            {
                //우측 이동
                ++pos_x;
                if (pos_x >= 29)
                    pos_x = 29;
            }
            else
            {
                //좌측 이동
                --pos_x;
                if (pos_x < 1) 
                    pos_x = 1;
            }
        }

        public void Render()
        {
            if (isAlive == false) return;
            Console.SetCursorPosition(pos_x, pos_y);
            Console.Write("E");
        }
    }

적은 랜덤한 위치에서 생성하기 위해 초기 위치 설정값을 정하는 부분에 랜덤함수를 사용한다. 적을 생성하면 적의 생성여부를 판단하는 참/거짓 변수가 참으로 설정되고 위치값을 설정한다. 랜더부분에서 참/거짓 변수가 참일 때만 해당 위치에 적을 그린다.

 

5. Bullet 클래스

internal class Bullet
    {
        public bool isAlive = true;
        public void SetIsAlive(bool alive) { isAlive = alive; }

        public bool GetIsAlive()
        {
            return isAlive;
        }


        int pos_x;

        public int GetPosX()
        {
            return pos_x;
        }
        public void SetPosX(int posX)
        {
            pos_x = posX;
        }

        int pos_y;

        public int GetPosY() 
        {
            return pos_y;
        }
        public void SetPosY(int posY)
        {
            pos_y = posY;
        }

        // 1초마다 호출
        public void Update()
        {
            //살아있지 않다면 계산 하지마
            if (isAlive == false)
                return;

            --pos_y;

            //총알이 더 이상 존재할 필요가 없다
            if (pos_y < 0)
            {
                pos_y = 0;
                isAlive = false;
            }

        }

        public void Render()
        {
            Console.SetCursorPosition(pos_x, pos_y);
            Console.Write("B");
        }
    }

플레이어가 a키를 누르면 발사되는 총알은 플레이어의 위치에서부터 발사되어야 되기 때문에 루프문 함수에서 플레이어 좌표값을 Get으로 가져와 불릿 클래스 내부 함수에 Set으로 집어넣어 플레이어 위치값을 초기값으로 설정한다. 그 이후 총알이 존재한다면 --y로 계속 위로 이동하고 y가 0보다 작아지면 false가 됨으로써 존재를 지운다.

 

레이싱 게임을 아직 완벽하게 구현하지 못한 상태로 다음 진도로 나갔다. 그런데 클래스를 사용해보니 어쩌면 레이싱게임에서 막혔던 부분에 대한 해답을 찾을 수 있을지도 모르겠다.