'XNA'에 해당되는 글 2건

  1. 2009.10.14 Xna Tetris (3)
  2. 2009.09.23 Xna Pong Game (1)

Naver Perl Community & Study Cafe


2009.10.14 01:19

Xna Tetris





XNA 번역서를 살펴보면 Tetris 예제가 있다.

워낙 유명한 게임이라 빼놓지 않고 구현해 놓았다.


사실 이 테트리스까지만 다루어도,

2D 게임의 전반적인 부분은 다 이해할 수 있을 정도인데,

간단한 스크립트를 짜서 전체 라인수를 출력해보니

5000줄 가까이 되는것을 알 수 있었다.


말이 5000줄이지만 주석이 3분의 1정도는 되는 듯...




공개되어 있는 소스를 그대로 설명할 필요는 없을 것이고,

기존의 소스를 바꾸어 테트리스 도형을 더 추가시켜보겠다.



TetrisGrid.cs 파일을 살펴보면,


블럭의 타입을 결정하는 상수들을 먼저 설정해 주어야 하는데,

public enum BlockTypes
{
            Empty,
            Block,
            Triangle,
            Line,
            RightT,
            LeftT,
            RightShape,
            LeftShape,
           UserShape1,
          UserShape2,
          UserShape3,

} // enum BlockTypes

기존의 상수에서 UserShape 3개를 더 추가를 시켰다.

이것은 내가 설정할 도형 3개를 추가하겠다는 뜻인데,

이렇게 설정한 값에 따라,

컬러도 지정해 줘야한다.


public static readonly Color[] BlockColor = new Color[]
{
    new Color( 60, 60, 60, 128 ), // Empty, color unused
    new Color( 50, 50, 255, 255 ), // Line, blue
    new Color( 160, 160, 160, 255 ), // Block, gray
    new Color( 255, 50, 50, 255 ), // RightT, red
    new Color( 255, 255, 50, 255 ), // LeftT, yellow
    new Color( 50, 255, 255, 255 ), // RightShape, teal
    new Color( 255, 50, 255, 255 ), // LeftShape, purple
    new Color( 50, 255, 50, 255 ), // Triangle, green
    new Color( 255, 255, 255, 255 ), // 첫번째 도형 , White
   new Color( 251, 70, 88, 255 ), // 두번째 도형 , 핑크
   new Color( 10, 220, 10, 255 ), // 세번째 도형 , 초록

}; // Color[] BlockColor


색도 지정을 했으니 이제 도형의 기본타입을 설정해야 된다.



public static readonly int[][,] BlockTypeShapesNormal = new int[][,]
{
    // Empty
    new int[,] { { 0 } },
    // Line
    new int[,] { { 0, 1, 0 }, { 0, 1, 0 }, { 0, 1, 0 }, { 0, 1, 0 } },
    // Block
    new int[,] { { 1, 1 }, { 1, 1 } },
    // RightT
    new int[,] { { 1, 1 }, { 1, 0 }, { 1, 0 } },
    // LeftT
    new int[,] { { 1, 1 }, { 0, 1 }, { 0, 1 } },
    // RightShape
    new int[,] { { 0, 1, 1 }, { 1, 1, 0 } },
    // LeftShape
    new int[,] { { 1, 1, 0 }, { 0, 1, 1 } },
    // LeftShape
    new int[,] { { 0, 1, 0 }, { 1, 1, 1 }, { 0, 0, 0 } },

    // 첫번째 도형   ㄷ 도형
    new int[,] { { 1, 0, 1 }, { 1, 0, 1 }, { 1, 1, 1 }},
    // 두번째 도형   두툼한 凸 도형 밑에 한칸이 더 있는..
    new int[,] { { 0, 1, 0 }, { 1, 1, 1 }, { 1, 1, 1 }},
    // 세번째 7칸짜리 네모 도형
    new int[,] { { 0, 1, 1 }, { 1, 1, 1 }, { 1, 1, 0 }}, 
 
              

}; // BlockTypeShapesNormal

위의 코드는 아래 그림과 같은 3개의 도형을 생성시킨다.


     
     



배열의 코드만 조금 수정하면, 다른 블럭들을 더 생성할 수 있다.

여기까지는 쉽지만 이 다음 코드부터는

소스에 대한 완벽한 이해가 필요하다.


저 블럭들을 움직이거나(move) 회전시키는(rotate) 코드는 각각의 함수로 이루어져 있다.

일단 MoveBlock 이라는 함수를 살펴보고 분석하여 주석을 달아보았다.

for (int x = 0; x < GridWidth; x++)
        for (int y = 0; y < GridHeight; y++)
                if (floatingGrid[x, y])
                      grid[x, y] = BlockTypes.Empty;

// 이전에 있었던 값들을 모두 지운다.
// GridWidth 와 GridHeight 는 상수이며,
// 가로와 세로 길이를 뜻한다. 이 값을 바꾸면
// 게임 전체의 가로와 세로 길이도 변하게 된다.


            
// Move stuff to new position
bool anythingBlocking = false;
Point[] newPos = new Point[9];
int newPosNum = 0;
if (moveType == MoveTypes.Left)
{

                for (int x = 0; x < GridWidth; x++)
                    for (int y = 0; y < GridHeight; y++)

                        // 이 소스는 블럭의 값을 검사해서
                        // x 를 -1 즉 왼쪽으로 이동했을 시,
                        // BlockTypes의 값이 비어있을 때 이동시킨다.

                        if (floatingGrid[x, y])
                        {
                            if (x - 1 < 0 ||
                                grid[x - 1, y] != BlockTypes.Empty)
                                anythingBlocking = true;
                            // x -1 < 0 보다 작다는 것은
                            // 더이상 왼쪽으로 갈 수 없는것을 의미한다.
                            // 그리고 grid[x - 1, y] != BlockTypes.Empty
                            // 이 코드는 grid 배열에 테트리스 블럭의 빈공간이
                            // 들어가지 않는것을 의미한다.

                            // 그래서 anythingBlocking 이 true가 된다.                             
                            else if (newPosNum < 9)
                            {
                                // newPosNum 은 블럭이 들어갈 수 있는것을 체크하며,
                                // 4보다 작을 때까지 체크하여,4일때까지만
                                // 새로운 포인터를 할당한다.

                                newPos[newPosNum] = new Point(x - 1, y);
                                newPosNum++;
                            } // else if                  
                        } // for for if
            } // if (left)


newPosNum 이라는 변수가 큰 역활을 하는데,

기존의 소스에는 이 값이 4가 넘지 않을때까지만 Check 되어 있다.

그리고

Point[] newPos = new Point[9];

이 부분의 소스도 Point[4] 로 고정되어 있어,

블럭이 4개까지만 바뀌도록 설정이 되어있다.

그러므로 저 값은 상수값을 줘서 처리하는게 좋지만, 그냥 9개 정도만 설정해 놓았다.


이 코드는 왼쪽으로 이동했을 때의 코드이며,

오른쪽으로 이동하는 소스와, 아래로 이동하는 소스 모두 동일하게 처리해주면 된다.



그리고 마지막으로 바뀐 값의 처리를 확실하게 해줘야 하는데,


            if (anythingBlocking ||
                // Or we didn't get all 4 new positions?
                newPosNum < 4 // 원래는 newPosNum != 4 로써 4개인지만 검사한다.
            {
                for (int x = 0; x < GridWidth; x++)
                    for (int y = 0; y < GridHeight; y++)
                        if (floatingGrid[x, y])
                            grid[x, y] = (BlockTypes)currentBlockType;
                return false;
            } // if
            else
            {
                if (moveType == MoveTypes.Left)
                    currentBlockPos = new Point(currentBlockPos.X - 1, currentBlockPos.Y);
                else if (moveType == MoveTypes.Right)
                    currentBlockPos = new Point(currentBlockPos.X + 1, currentBlockPos.Y);
                else if (moveType == MoveTypes.Down)
                    currentBlockPos = new Point(currentBlockPos.X, currentBlockPos.Y + 1);

                // Else we can move to the new position, lets do it!
                for (int x = 0; x < GridWidth; x++)
                    for (int y = 0; y < GridHeight; y++)
                        floatingGrid[x, y] = false;

 

                /*  여기가 핵심코드   */

                // newPosNum
                for (int i = 0; i < newPosNum ; i++)
                {
                        grid[newPos[i].X, newPos[i].Y] = (BlockTypes)currentBlockType;
                        floatingGrid[newPos[i].X, newPos[i].Y] = true;
                } // for

               
                Sound.Play(Sound.Sounds.BlockMove);

핵심코드라고 되어있는 부분은

새로운 포지션을 얻은 블럭에 크기만큼

새로운 블럭을 그려주고 있다.


이로써 4개로 이루어진 블럭 뿐만이 아니라,

9개 이하의 블럭은 모두 처리가 가능한 것이다.





이로써 블럭 추가는 끝.




Tip으로 Draw 함수에서 GameOver 문구를 출력하는 코드.





if (gameOver)
{
                TextureFont.WriteText(380, 400, "<< GAME OVER >>");
                return;
}

Draw 함수에서 설정해야 한다.

자칫 잘못해서 Update 함수에서 설정하면,

Game Over 문구가 한번 반짝 거리고 사라져 버리기 때문이다.





2시간동안 작업한 것 치고는 꾀 훌륭한 아웃풋이다.

슬슬 C# 코드도 눈에 익어가지만...

아직도 완벽하게 사용하기는 멀고도 멀은 듯하구나..

신고
Trackback 0 Comment 3
2009.09.23 14:00

Xna Pong Game


XNA 공부를 시작한지 어느정도 지났다...

이리저리 다른일한다고 핑계만 대다가,

기본서인 "실전 예제로 배우는 XNA Game Programming"  를 제대로 보기로 결심하고 천천히 살펴보던 중

이 책의 첫번째 게임 예제는 Pong 게임을 접하게 되었다.



서로 공을 주고 받으며, 대전을 하는 게임인데

공만 주고 받으면 심심하니 변수를 하나 만들어 보았다.


중앙에 운석을 하나 넣어서

그 운석에 맞으면 다른곳으로 튕겨나게....

헙.. 말은 쉽지만 구현하는데는 까다로울 듯 하다.



일단 Content에 Meteo 라는 운석 이미지를 추가를 한 뒤

LoadContent() 함수에 가서 meteorite 이미지를 불러온다.


texmeteo = Content.Load<Texture2D>("meteorite");


그리고 StartNewMeteo 이라는 함수를 만들어 처음 운석의 자리를 정해준다.

public void StartNewMeteo()
{
            MeteoPosition = new Vector2(0.5f, 0.5f);           // 0.5f , 0.5f 는 정 중앙을 뜻한다.
}

그리고 이미지를 출력하기 위해 RenderMeteo 함수도 만들어 준다.

public void RenderMeteo()
{
      RenderSprite(texmeteo, (int)((0.05f + 0.9f * MeteoPosition.X) * 1024) - MeteoRect.Width / 2,
                        (int)((0.02f + 0.96f * MeteoPosition.Y) * 768) - MeteoRect.Height / 2,  MeteoRect);
}





그리고 게임이 그려지는 Draw 함수 안에서

게임이 시작되었을 때 RenderMeteo() 함수만 불러주면 이미지 출력은 손쉽게 이루어진다.



허접하지만 운석그림이 화면에 출력되는것을 확인할 수 있다.


이제부터가 중요한데

Ball 이 저 운석에 충돌할때 되면 다른 방향으로 꺽어지는 부분은 어떻게 하면 좋을까?

일단 Ball 과 운석이 충돌했을때를 먼저 구현해보도록 하자.



이 부분은 Update 함수에서 구현을 하면 되는데,

공과 패들이 충돌할 때와 동일하게 구현하면 손쉽게 처리 할 수 있다.

일단 Meteo의 Size를 구하여, MeteoBox를 먼저 구해놓는다.

Vector2 MeteoSize = new Vector2(MeteoRect.Width / 1024.0f, MeteoRect.Height / 768.0f);

BoundingBox MeteoBox = new BoundingBox(new Vector3(MeteoPosition.X-MeteoSize.X/2, 
                           MeteoPosition.Y-MeteoSize.Y/2, 0),  new Vector3(MeteoPosition.X+MeteoSize.X/2,
                           MeteoPosition.Y+MeteoSize.Y/2, 0));


그 후에 Ball 과 MeteoBox가 충돌했는지의 여부를 파악한다.

if (ballBox.Intersects(MeteoBox)){

          // 출동했을시 실행

}


충돌했을때의 처리가 약간 까다로울텐데,

어디에서 공이 충돌하느냐에 따라 튕겨나가는 방향을 다르게 처리해야한다.

예를들어 운석은 4가지의 방향이 있는데 이 4가지를 모두 처리해보자.



if (ballSpeed.X < 0)
{
         ballSpeed.X = Math.Abs(ballSpeed.X);
}
else if (ballSpeed.X > 0)
{
         ballSpeed.X = -Math.Abs(ballSpeed.X);
}
if (ballSpeed.Y < 0)
{
         ballSpeed.Y = Math.Abs(ballSpeed.Y);
}
else if (ballSpeed.Y > 0)
{
         ballSpeed.Y = -Math.Abs(ballSpeed.Y);
}


나같은 경우에는 이런식으로 처리를 했는데

BallSpeed의 X 와 Y의 값에 따라 패들을 튕기게 만들었다.

실제로 게임 플레이는 잘 되지만 알고리즘상으로 좋은것은 아닐듯.. 그냥 if만 쉐리 박은거라;;



이제 마지막으로 운석을 움직여 보겠다.

 if (MeteoPosition.Y < 0.2f || MeteoPosition.Y > 0.8f)
{
         MeteoSpeed.Y = -MeteoSpeed.Y;
}

if (MeteoPosition.X < 0.2f || MeteoPosition.X > 0.8f)
{
         MeteoSpeed.X = -MeteoSpeed.X;
}

MeteoPosition += MeteoSpeed * moveFactorPerSecond ;


사각의 공간을 만들어 그 공간안에서 운석을 움직여 보았다.

그럼 이제 컴퓨터에게 패들을 맡기고 게임을 구경해보자.





이정도야 ㅋㅋ

신고
Trackback 0 Comment 1