Chapter

 22

네트워크 게임 만들기

이번 장에서는 네트워크 게임 만들기에 도전해 보자. 이해하기 어려운 부분도 있을 것이다. 하지만 여기까지 열심히 공부했다면 충분히 해낼 수 있을 것으로 믿고, 나아가 자신만의 네트워크 게임을 제작할 수 있을 것이다.

 

 

 

네트워크 오목 게임

오목은 흑과 백, 2명의 선수가 오목판(?)에 교대로 자신의 돌을 한 수씩 두어, 다섯 개의 돌을 가로, 세로 또는 대각선 방향으로 일렬로 놓으면 이기는 경기이다. 모르는 사람이 없을 정도로 잘 알려진 것이므로 굳이 자세하게 설명할 필요는 없을 것 같다.

 

여기서 만들 네트워크 오목 게임은 네트워크상의 두 사람이 만나서 오목을 둘 수 있을 뿐만 아니라 많은 사람이 접속하여 대화도 나눌 수 있다. 다음 그림은 클라이언트의 실행화면이다.

사용자 삽입 이미지

[그림 22-1] 네트워크 오목 게임 실행화면

 

여기서는 많은 사람이 입장할 수 있는 대기실과 두 사람이 입장하여 오목을 둘 수 있는 방도 만들 것이다. 대기실과 방은 번호를 매겨서 구분하도록 한다. 대기실은 0번을 사용하고 다른 방들은 0이상의 자연수를 사용한다. 실제로 방을 만드는 것이 아니라 단지 클라이언트에게 방 번호를 부여하는 것뿐이다. 그렇게 함으로써 같은 번호를 가진 클라이언트들은 서로 메시지를 주고받을 수 있게 하면 된다.

 

아래 그림을 보자. 클라이언트1이 메시지를 서버 측으로 보내면(ⓐ), 클라이언트1과 연결된 스레드1은 메시지 방송자에게 메시지를 전달한다(ⓑ). 메시지 방송자는 클라이언트1과 같은 방 번호를 가진 스레드들에게 메시지를 전파한다(ⓒ).

 

사용자 삽입 이미지

[그림 22-2] 방 번호가 부여된 클라이언트들의 통신

 

 

 

통신 프로토콜 만들기

서버와 클라이언트가 올바르게 통신하기 위해서는 주고받는 메시지가 어떤 유형인가를 정해야한다.

 

예를 들면, 클라이언트가 어떤 방에 입장하려고 서버에게 요청하면 서버는 입장할 수 있는 상태이면 입장할 수 있다는 메시지를 전송하고, 반대로 입장할 수 없는 상태이면 입장할 수 없다는 메시지를 전송할 것이다. 메시지는 "[메시지유형]+내용"과 같은 형태를 가지도록 정하자. 클라이언트가 20번 방에 입장하려고 "[ROOM]20"이라는 메시지를 서버로 전송했다고 치자. 서버의 응답은 클라이언트가 20번 방에 입장할 수 있으면 적절한 조치를 취한 후, 클라이언트에게 "[ROOM]20"을 그대로 전송하고, 방이 차서 입장할 수 없는 상태라면 "[FULL]"이라는 메시지를 클라이언트에게 전송하는 약속을 만들 수 있을 것이다.

 

클라이언트의 요청: [ROOM]20

서버의 응답: [ROOM]20, 또는 [FULL]

 

또 다른 예로, 어떤 방에 두 명의 사용자가 입장하여 오목을 두려한다고 생각하자. 먼저 한 명의 사용자가 "[START]"라는 메시지를 보낸다. 다른 사용자도 마찬가지로 "[START]"를 서버로 보내면 서버는 두 명이 모두 오목 두기를 원하는 것으로 생각하고 임으로 흑과 백을 정하여 "[COLOR]BLACK"와 "[COLOR]WHITE"를 각각의 사용자에게 보낸다.

 

클라이언트1의 요청: [START]

클라이언트2의 요청: [START]

서버의 응답: [COLOR]BLACK과 [COLOR]WHITE

 

이와 같이 게임에 필요한 메시지의 유형들을 정하고 클라이언트가 서버로 보내는 메시지와 반대로 서버가 클라이언트로 보내는 메시지로 어떤 종류가 있는지 생각해보자.

 

[표 22-1]과 [표 22-2]는 이 게임에서 쓰이는 메시지 유형들을 설명한 것이다. 먼저 [표22-1]은 클라이언트 측에서 보내는 메시지 유형과 그 설명이다.

 

클라이언트 측 메시지 유형

설명

[NAME]철수

사용자의 이름(철수)을 보낸다.

[ROOM]20

20번 방에 입장하려고 요청한다.

[MSG]문자열

채팅 메시지를 보낸다.

[START]

게임을 시작할 준비가 되었음을 알린다.

[DROPGAME]

기권했음을 알린다.

[STONE]5 7

(5, 7)에 돌을 두었음을 알린다.

[WIN]

이겼음을 알린다.

[STOPGAME]

어떤 원인에 의해 게임의 중지를 통보한다.

[표 22-1] 클라이언트의 메시지

 

 

서버 측 메시지 유형

설명

[EXIT]철수

철수가 방에서 퇴장하였음을 알린다.

[ENTER]철수

철수가 방으로 입장하였음을 알린다.

[STONE]5 7

(5, 7)위치에 상대편이 돌을 두었음을 알린다.

[COLOR]BLACK

게임 시작 시, 흑이 선택되었음을 알린다.

[COLOR]WHITE

게임 시작 시, 백이 선택되었음을 알린다.

[ROOM]20

사용자가 20번 방에 입장하였음을 알린다.

[FULL]

방이 다 찼음을 알린다.

[DROPGAME]

상대편이 기권하였음을 알린다.

[WIN]

게임에서 이겼음을 알린다.

[LOSE]

게임에서 졌음을 알린다.

[DISCONNECT]

상대편이 접속을 끊었음을 알린다.

[PLAYERS]

현재의 방에 있는 사용자들의 명단을 보낸다.

이름은 tab으로 구분한다.

예) [PLAYERS]철수\t영희\t명수

[표 22-2] 서버의 메시지

 

 

 

네트워크 오목 게임 서버

네트워크 오목 게임 서버는 앞장에서 만든 채팅 서버와 같이 크게 3개의 클래스를 가진다. 메인 클래스로 정의되는 OmokServer와 스레드 클래스인 Omok_Thread, 그리고 메시지 방송자 클래스로 BManager가 있다.

 

OmokrServer 클래스의 역할은 채팅 서버에서와 같이 단순하다. 접속해오는 클라이언트와 소켓을 연결한 후, 스레드를 실행시키고 스레드를 BManager 객체에게 추가시키는 것이 OmokServer의 역할이다.

 

private BManagerbMan=new BManager();           // 메시지 방송자 객체

...

while(true){

  Socketsocket=server.accept();                   // 소켓을 얻는다.

  Omok_Thread ot=new Omok_Thread(socket);  // 스레드를 만든다.

  ot.start();                                       // 스레드를 실행시킨다.

  bMan.add(ot);                                  // 스레드를 추가한다.

  ...

}

 

Omok_Thread 객체는 소켓과 입·출력 스트림 외에도 클라이언트에 대한 정보로써 클라이언트(사용자) 이름, 방 번호, 게임의 시작 여부(boolean) 등을 가진다. 클라이언트 측에서 보낸 메시지가 누구의 메시지인지 또는 어느 방에서 전송된 메시지인지를 알아야하기 때문이다. 방송자가 어느 한 방에 메시지를 전송할 때에도 스레드 객체의 방 번호를 참조한다.

 

BManager는 Vector를 상속하는데 Omok_Thread 객체의 방 번호와 사용자 이름 등을 자유롭게 참조하기 위해 Omok_Thread 객체를 벡터의 요소(elements)로 가진다. 주어진 방에 메시지를 전달하기 위해 벡터의 각 요소(스레드)의 방 번호와 주어진 방 번호를 비교하여 같으면 메시지를 전달하도록 할 것이다.

 

실제로 sendToRoom는 주어진 방(roomNum)에 메시지(msg)를 전달하는 메소드로 아래와 같다.

 

voidsendToRoom(int roomNum, String msg){

  for(int i=0;i<size();i++){

    if(roomNum==i번째 스레드의 방번호){

      // i번째 스레드와 연결된 클라이언트로 메시지를 전달한다.

    }

  }

}

 

sendToOthers 메소드는 해당 스레드(ot)와 같은 방에 있는 다른 사용자에게 메시지(msg)를 전달하는 메소드이다. 이 메소드도 sendToRoom과 같이 for문을 돌면서 요소의 방 번호를 검사한다. 다만 ot와 연결된 클라이언트에게는 메시지를 전달하지 않는다.

 

voidsendToOthers(Omok_Thread ot, String msg){

  for(int i=0;i<size();i++){

    if(i번째 스레드의 방 번호==ot의 방 번호&&i번째 스레드 !=ot){

      // i번째 스레드와 연결된 클라이언트로 메시지를 전달한다.

    }

  }

}

 

sendToRoom과 sendToOthers의 알고리즘은 방이 많으면 많을수록 서버에 심각한 부하를 만들 것이고 메시지의 전송이 더디게 될 것이다. 따라서 독자는 더 나은 알고리즘을 생각해 볼 수 있다.

 

아래는 서버 프로그램의 전체 소스이다. 코드가 길고 복잡하므로 초보자가 이해하기 만만치 않다. 남이 작성한 코드를 이해하는 것은 직접 작성하는 것보다 많은 시간이 걸린다. 따라서 전체적인 흐름 위주로 살펴보는 것이 좋고 독자가 직접 비슷한 프로그램을 작성해 보는 것이 좋을 것이다.

 

OmokServer.java

 

import java.net.*;

import java.io.*;

import java.util.*;

public classOmokServer{

  private ServerSocketserver;

  private BManagerbMan=new BManager();   // 메시지 방송자

  private Randomrnd= new Random();       // 흑과 백을 임의로 정하기 위한 변수

  public OmokServer(){}

  voidstartServer(){                         // 서버를 실행한다.

    try{

      server=new ServerSocket(7777);

      System.out.println("서버소켓이 생성되었습니다.");

      while(true){

 

        // 클라이언트와 연결된 스레드를 얻는다.

        Socket socket=server.accept();

        

        // 스레드를 만들고 실행시킨다.

        Omok_Thread ot=new Omok_Thread(socket);

        ot.start();

 

        // bMan에 스레드를 추가한다.

        bMan.add(ot);

 

        System.out.println("접속자 수: "+bMan.size());

      }

    }catch(Exception e){

      System.out.println(e);

    }

  }

  public static voidmain(String[] args){

    OmokServer server=new OmokServer();

    server.startServer();

  }

 

 // 클라이언트와 통신하는 스레드 클래스

  classOmok_Threadextends Thread{

    private introomNumber=-1;        // 방 번호

    private StringuserName=null;       // 사용자 이름

    private Socketsocket;              // 소켓

 

    // 게임 준비 여부, true이면 게임을 시작할 준비가 되었음을 의미한다.

    private booleanready=false;

 

    private BufferedReaderreader;     // 입력 스트림

    private PrintWriterwriter;           // 출력 스트림

    Omok_Thread(Socket socket){     // 생성자

      this.socket=socket;

    }

    SocketgetSocket(){               // 소켓을 반환한다.

      return socket;

    }

    intgetRoomNumber(){             // 방 번호를 반환한다.

      return roomNumber;

    }

    StringgetUserName(){             // 사용자 이름을 반환한다.

      return userName;

    }

    booleanisReady(){                 // 준비 상태를 반환한다.

      return ready;

    }

    public voidrun(){

      try{

        reader=new BufferedReader(

                            new InputStreamReader(socket.getInputStream()));

        writer=new PrintWriter(socket.getOutputStream(), true);

 

        Stringmsg;                     // 클라이언트의 메시지

 

        while((msg=reader.readLine())!=null){

 

          // msg가 "[NAME]"으로 시작되는 메시지이면

          if(msg.startsWith("[NAME]")){

            userName=msg.substring(6);          // userName을 정한다.

          }

 

          // msg가 "[ROOM]"으로 시작되면 방 번호를 정한다.

          else if(msg.startsWith("[ROOM]")){

            int roomNum=Integer.parseInt(msg.substring(6));

            if( !bMan.isFull(roomNum)){             // 방이 찬 상태가 아니면

 

              // 현재 방의 다른 사용에게 사용자의 퇴장을 알린다.

              if(roomNumber!=-1)

                bMan.sendToOthers(this, "[EXIT]"+userName);

 

              // 사용자의 새 방 번호를 지정한다.

              roomNumber=roomNum;

 

              // 사용자에게 메시지를 그대로 전송하여 입장할 수 있음을 알린다.

              writer.println(msg);

 

              // 사용자에게 새 방에 있는 사용자 이름 리스트를 전송한다.

              writer.println(bMan.getNamesInRoom(roomNumber));

 

              // 새 방에 있는 다른 사용자에게 사용자의 입장을 알린다.

              bMan.sendToOthers(this, "[ENTER]"+userName);

            }

            else writer.println("[FULL]");        // 사용자에 방이 찼음을 알린다.

          }

 

          // "[STONE]" 메시지는 상대편에게 전송한다.

          else if(roomNumber>=1 && msg.startsWith("[STONE]"))

            bMan.sendToOthers(this, msg);

 

          // 대화 메시지를 방에 전송한다.

          else if(msg.startsWith("[MSG]"))

            bMan.sendToRoom(roomNumber,

                              "["+userName+"]: "+msg.substring(5));

 

          // "[START]" 메시지이면

          else if(msg.startsWith("[START]")){

            ready=true;   // 게임을 시작할 준비가 되었다.

 

            // 다른 사용자도 게임을 시작한 준비가 되었으면

            if(bMan.isReady(roomNumber)){

 

              // 흑과 백을 정하고 사용자와 상대편에게 전송한다.

              int a=rnd.nextInt(2);

              if(a==0){

                writer.println("[COLOR]BLACK");

                bMan.sendToOthers(this,"[COLOR]WHITE");

              }

              else{

                writer.println("[COLOR]WHITE");

                bMan.sendToOthers(this,"[COLOR]BLACK");

              }

            }

          }

 

          // 사용자가 게임을 중지하는 메시지를 보내면

          else if(msg.startsWith("[STOPGAME]"))

            ready=false;

 

          // 사용자가 게임을 기권하는 메시지를 보내면

          else if(msg.startsWith("[DROPGAME]")){

            ready=false;

            // 상대편에게 사용자의 기권을 알린다.

            bMan.sendToOthers(this, "[DROPGAME]");

          }

 

          // 사용자가 이겼다는 메시지를 보내면

          else if(msg.startsWith("[WIN]")){

            ready=false;

            // 사용자에게 메시지를 보낸다.

            writer.println("[WIN]");

 

            // 상대편에는 졌음을 알린다.

            bMan.sendToOthers(this, "[LOSE]");

          }  

        }

      }catch(Exception e){

      }finally{

        try{

          bMan.remove(this);

          if(reader!=null) reader.close();

          if(writer!=null) writer.close();

          if(socket!=null) socket.close();

          reader=null; writer=null; socket=null;

          System.out.println(userName+"님이 접속을 끊었습니다.");

          System.out.println("접속자 수: "+bMan.size());

          // 사용자가 접속을 끊었음을 같은 방에 알린다.

          bMan.sendToRoom(roomNumber,"[DISCONNECT]"+userName);

        }catch(Exception e){}

      }

    }

  }

  classBManagerextends Vector{       // 메시지를 전달하는 클래스

    BManager(){}

    voidadd(Omok_Thread ot){           // 스레드를 추가한다.

      super.add(ot);

    }

    voidremove(Omok_Thread ot){        // 스레드를 제거한다.

       super.remove(ot);

    }

    Omok_ThreadgetOT(int i){            // i번째 스레드를 반환한다.

      return (Omok_Thread)elementAt(i);

    }

    SocketgetSocket(int i){              // i번째 스레드의 소켓을 반환한다.

      return getOT(i).getSocket();

    }

 

    // i번째 스레드와 연결된 클라이언트에게 메시지를 전송한다.

    voidsendTo(int i, String msg){

      try{

        PrintWriter pw= new PrintWriter(getSocket(i).getOutputStream(), true);

        pw.println(msg);

      }catch(Exception e){}  

    }

    intgetRoomNumber(int i){            // i번째 스레드의 방 번호를 반환한다.

      return getOT(i).getRoomNumber();

    }

    synchronized booleanisFull(int roomNum){    // 방이 찼는지 알아본다.

      if(roomNum==0)return false;                 // 대기실은 차지 않는다.

 

      // 다른 방은 2명 이상 입장할 수 없다.

      int count=0;

      for(int i=0;i<size();i++)

        if(roomNum==getRoomNumber(i))count++;

      if(count>=2)return true;

      return false;

    }

 

    // roomNum 방에 msg를 전송한다.

    voidsendToRoom(int roomNum, String msg){

      for(int i=0;i<size();i++)

        if(roomNum==getRoomNumber(i))

          sendTo(i, msg);

    }

 

    // ot와 같은 방에 있는 다른 사용자에게 msg를 전달한다.

    voidsendToOthers(Omok_Thread ot, String msg){

      for(int i=0;i<size();i++)

        if(getRoomNumber(i)==ot.getRoomNumber() && getOT(i)!=ot)

          sendTo(i, msg);

    }

 

    // 게임을 시작할 준비가 되었는가를 반환한다.

    // 두 명의 사용자 모두 준비된 상태이면 true를 반환한다.

    synchronized booleanisReady(int roomNum){

      int count=0;

      for(int i=0;i<size();i++)

        if(roomNum==getRoomNumber(i) && getOT(i).isReady())

          count++;

      if(count==2)return true;

      return false;

    }

 

    // roomNum방에 있는 사용자들의 이름을 반환한다.

    StringgetNamesInRoom(int roomNum){

      StringBuffer sb=new StringBuffer("[PLAYERS]");

      for(int i=0;i<size();i++)

        if(roomNum==getRoomNumber(i))

          sb.append(getOT(i).getUserName()+"\t");

      return sb.toString();

    }

  }

}


 

 

 

네트워크 오목 게임 클라이언트

네트워크 오목 게임 클라이언트는 프로그램의 특성상 서버의 소스보다 두 배 이상 길다. 하지만 필요한 AWT 컴포넌트를 만들고 배치하는 코드가 상당하기 때문에 그것을 제외하면 그렇게 긴 것도 아니다.

 

먼저 오목판(OmokBoard 클래스)을 구현해보자. 오목판은 15×15의 크기, 즉, 225개의 격자를 가지며 프레임에 포함될 수 있도록 캔버스를 상속한다. 오목판은 사용자가 돌을 둘 수 있도록 마우스 이벤트에 반응한다. 그리고 상대편에게 사용자가 돌을 두었다는 메시지를 전달하기 위해 출력 스트림을 가진다. 그리기 작업을 하는 여러 메소드들도 있다. 무엇보다도 중요한 것은 바로 승부를 결정하는 것이다. 가로, 세로, 또는 대각선 방향으로 같은 색깔의 다섯 개의 돌이 놓였을 때 승부가 결정된다.

 

아래 그림을 보자. 먼저 1번, 2번, 3번, 4번에 흑이 놓여져 있다. 마지막에 흑이 5번에 놓으면 가로로 흑이 5개이므로 흑의 승이다. 프로그램에서는 어떻게 구현할 수 있을까? 5번 돌이 놓였을 때, 먼저 왼쪽으로 연속한 흑의 개수를 카운트한다. 2번과 1번이 연속으로 놓여있으므로 2개이다. 그리고 오른쪽으로 연속한 흑의 개수를 카운트한다. 3번과 4번이 연속이므로 2개이다. 따라서 5번과 합쳐서 5개의 돌이 가로 방향으로 놓여져 있음을 알 수 있다. 물론 세로와 양 대각선 방향으로도 가로와 같은 방법으로 검사해야 할 것이다.

 

사용자 삽입 이미지

[그림 22-3] 5개의 돌 찾기

 

map은 돌들의 위치를 기억하기 위한 2차원 배열이다. 만약 (2, 3)위치에 흑이 놓인다면 map[2][3]에는 BLACK(=1)이라는 상수가 대입되고, 백이 놓이면 WHITE(=-1) 값이 대입된다. 그리고 해당 위치에 어떤 돌도 없다면 0이 대입된다.

 

map[2][3]=BLACK;      // (2, 3)에 흑이 놓여있다.

map[5][5]=WHITE;      // (5, 5)에 백이 놓여있다.

map[10][10]=0;         // (10, 10)에는 돌이 없다.

 

map의 크기는 계산상의 편리로 15×15가 아닌 17×17로 정한다. 따라서 오목판의 왼쪽 상단의 좌표는 (0, 0)이 아닌 (1, 1)이 된다. 남는 부분은 오목판의 외각을 위한 것으로 생각해도 된다.

 

count 메소드는 연속한 돌의 개수를 찾는다. p는 기준이 되는 좌표이고 dx는 가로 방향, dy는 세로 방향을 결정하는 변수이다. col은 돌의 색을 의미한다. dx가 -1이고, dy가 0이면 왼쪽 방향으로 연속한 돌의 개수를 카운트하고, dx가 1이고 dy가 1이면 오른쪽 위를 향하는 대각선 방향으로 연속한 돌의 개수를 카운트한다. dx와 dy는 -1, 0, 또는 1의 값을 가질 수 있다. p와 dx, dy에 값을 대입하고 실행 순서를 추적해 보면 이해될 것이다.

 

private intcount(Point p, int dx, int dy, int col){

  int i=0;

  for(; map[p.x+(i+1)*dx][p.y+(i+1)*dy]==col ;i++)

  {}

 

 return i;

}

 

check 메소드는 count 메소드를 이용하여 가로, 세로, 그리고 대각선 방향으로 좌표 p에 위치한 돌의 색(col)과 같은 돌들이 5개 놓였으면 true를 아니면 false를 반환한다.

 

private booleancheck(Point p, int col){

  if(count(p, 1, 0, col)+count(p, -1, 0, col)==4)     // 가로 방향 검사

    return true;

  if(count(p, 0, 1, col)+count(p, 0, -1, col)==4)     // 세로 방향 검사

    return true;

  if(count(p, -1, -1, col)+count(p, 1, 1, col)==4)    // 대각선 방향 검사

    return true;

  if(count(p, 1, -1, col)+count(p, -1, 1, col)==4)    // 대각선 방향 검사

    return true;

  return false;

}

 

사용자가 color 색의 돌을 (x, y)에 두면 다음과 같이 검사하여 이겼을 때의 처리를 할 수 있다. 이겼으면 "[WIN]" 메시지를 서버로 전송하다. 서버는 상대편에게 "[LOSE]" 메시지를 전송할 것이다.

 

map[x][y]=color;

if(check(new Point(x, y), color)){

  // 이겼다.

  writer.println("[WIN]");       // 서버로 메시지를 전송한다.

}

 

아래는 네트워크 오목 게임 클라이언트의 전체 소스이다.

 

OmokClient.java

 

import java.awt.*;

import java.net.*;

import java.io.*;

import java.util.*;

import java.awt.event.*;

import java.awt.geom.*;

classOmokBoardextendsCanvas{               // 오목판을 구현하는 클래스

  public static final intBLACK=1,WHITE=-1;     // 흑과 백을 나타내는 상수

  private int[][]map;                            // 오목판 배열

  private intsize               // size는 격자의 가로 또는 세로 개수, 15로 정한다.

  private intcell;                          // 격자의 크기(pixel)

  private Stringinfo="게임 중지";           // 게임의 진행 상황을 나타내는 문자열

  private intcolor=BLACK;                 // 사용자의 돌 색깔

 

  // true이면 사용자가 돌을 놓을 수 있는 상태를 의미하고,

  // false이면 사용자가 돌을 놓을 수 없는 상태를 의미한다.

  private booleanenable=false;

 

  private booleanrunning=false;       // 게임이 진행 중인가를 나타내는 변수

  private PrintWriterwriter;            // 상대편에게 메시지를 전달하기 위한 스트림

  private Graphicsgboard,gbuff;    // 캔버스와 버퍼를 위한 그래픽스 객체

  private Imagebuff;                 // 더블 버퍼링을 위한 버퍼

 

  OmokBoard(int s, int c){           // 오목판의 생성자(s=15, c=30)

    this.size=s; this.cell=c;

 

    map=new int[size+2][];            // 맵의 크기를 정한다.

    for(int i=0;i<map.length;i++)

      map[i]=new int[size+2];

 

    setBackground(new Color(200,200,100));         // 오목판의 배경색을 정한다.

    setSize(size*(cell+1)+size, size*(cell+1)+size);    // 오목판의 크기를 계산한다.

 

    // 오목판의 마우스 이벤트 처리

    addMouseListener(new MouseAdapter(){

      public voidmousePressed(MouseEvent me){     // 마우스를 누르면

        if(!enable)return;            // 사용자가 누를 수 없는 상태이면 빠져 나온다.

 

        // 마우스의 좌표를 map 좌표로 계산한다.

        int x=(int)Math.round(me.getX()/(double)cell);

        int y=(int)Math.round(me.getY()/(double)cell);

 

        // 돌이 놓일 수 있는 좌표가 아니면 빠져 나온다.

        if(x==0 || y==0 || x==size+1 || y==size+1)return;

 

        // 해당 좌표에 다른 돌이 놓여져 있으면 빠져 나온다.

        if(map[x][y]==BLACK || map[x][y]==WHITE)return;

 

        // 상대편에게 놓은 돌의 좌표를 전송한다.

        writer.println("[STONE]"+x+" "+y);

 

        map[x][y]=color;

 

        // 이겼는지 검사한다.

        if(check(new Point(x, y), color)){

          info="이겼습니다.";

          writer.println("[WIN]");

        }

 

        else info="상대가 두기를 기다립니다.";

        repaint();                                   // 오목판을 그린다.

      

        // 사용자가 둘 수 없는 상태로 만든다.

        // 상대편이 두면 enable이 true가 되어 사용자가 둘 수 있게 된다.

        enable=false;

      }

    });

  }

 

  public booleanisRunning(){           // 게임의 진행 상태를 반환한다.

    return running;

  }

  public voidstartGame(String col){     // 게임을 시작한다.

    running=true;

    if(col.equals("BLACK")){              // 흑이 선택되었을 때

      enable=true; color=BLACK;

      info="게임 시작... 두세요.";

    }   

    else{                                // 백이 선택되었을 때

      enable=false; color=WHITE;

      info="게임 시작... 기다리세요.";

    }

  }

  public voidstopGame(){              // 게임을 멈춘다.

    reset();                              // 오목판을 초기화한다.

    writer.println("[STOPGAME]");        // 상대편에게 메시지를 보낸다.

    enable=false;

    running=false;

  }

  public voidputOpponent(int x, int y){       // 상대편의 돌을 놓는다.

    map[x][y]=-color;

    info="상대가 두었습니다. 두세요.";

    repaint();

  }

  public voidsetEnable(boolean enable){

    this.enable=enable;

  }

  public voidsetWriter(PrintWriter writer){

    this.writer=writer;

  }

  public voidupdate(Graphics g){        // repaint를 호출하면 자동으로 호출된다.

    paint(g);                             // paint를 호출한다.

  }

  public voidpaint(Graphics g){                // 화면을 그린다.

    if(gbuff==null){                             // 버퍼가 없으면 버퍼를 만든다.

      buff=createImage(getWidth(),getHeight());

      gbuff=buff.getGraphics();

    }

    drawBoard(g);    // 오목판을 그린다.

  }

  public voidreset(){                         // 오목판을 초기화시킨다.

    for(int i=0;i<map.length;i++)

      for(int j=0;j<map[i].length;j++)

        map[i][j]=0;

    info="게임 중지";

    repaint();

  }

  private voiddrawLine(){                     // 오목판에 선을 긋는다.

    gbuff.setColor(Color.black);

    for(int i=1; i<=size;i++){

      gbuff.drawLine(cell, i*cell, cell*size, i*cell);

      gbuff.drawLine(i*cell, cell, i*cell , cell*size);

    }

  }

  private voiddrawBlack(int x, int y){         // 흑 돌을 (x, y)에 그린다.

    Graphics2D gbuff=(Graphics2D)this.gbuff;

    gbuff.setColor(Color.black);

    gbuff.fillOval(x*cell-cell/2, y*cell-cell/2, cell, cell);

    gbuff.setColor(Color.white);

    gbuff.drawOval(x*cell-cell/2, y*cell-cell/2, cell, cell);

  }

  private voiddrawWhite(int x, int y){         // 백 돌을 (x, y)에 그린다.

    gbuff.setColor(Color.white);

    gbuff.fillOval(x*cell-cell/2, y*cell-cell/2, cell, cell);

    gbuff.setColor(Color.black);

    gbuff.drawOval(x*cell-cell/2, y*cell-cell/2, cell, cell);

  }

  private voiddrawStones(){                  // map 놓여진 돌들을 모두 그린다.

    for(int x=1; x<=size;x++)

     for(int y=1; y<=size;y++){

       if(map[x][y]==BLACK)

         drawBlack(x, y);

       else if(map[x][y]==WHITE)

         drawWhite(x, y);

     }

  }

  synchronized private voiddrawBoard(Graphics g){      // 오목판을 그린다.

    // 버퍼에 먼저 그리고 버퍼의 이미지를 오목판에 그린다.

    gbuff.clearRect(0, 0, getWidth(), getHeight());

    drawLine();

    drawStones();

    gbuff.setColor(Color.red);

    gbuff.drawString(info, 20, 15);

    g.drawImage(buff, 0, 0, this);

  }

  private booleancheck(Point p, int col){

    if(count(p, 1, 0, col)+count(p, -1, 0, col)==4)

      return true;

    if(count(p, 0, 1, col)+count(p, 0, -1, col)==4)

      return true;

    if(count(p, -1, -1, col)+count(p, 1, 1, col)==4)

      return true;

    if(count(p, 1, -1, col)+count(p, -1, 1, col)==4)

      return true;

    return false;

  }

  private intcount(Point p, int dx, int dy, int col){

    int i=0;

    for(; map[p.x+(i+1)*dx][p.y+(i+1)*dy]==col ;i++);

    return i;

  }

}  // OmokBoard 정의 끝

 

public classOmokClientextends Frame implements Runnable, ActionListener{

  private TextAreamsgView=new TextArea("", 1,1,1);   // 메시지를 보여주는 영역

  private TextFieldsendBox=new TextField("");         // 보낼 메시지를 적는 상자

  private TextFieldnameBox=new TextField();          // 사용자 이름 상자

  private TextFieldroomBox=new TextField("0");        // 방 번호 상자

 

  // 방에 접속한 인원의 수를 보여주는 레이블

  private LabelpInfo=new Label("대기실:  명");

 

  private java.awt.ListpList=new java.awt.List();  // 사용자 명단을 보여주는 리스트

  private ButtonstartButton=new Button("대국 시작");    // 대국 시작 버튼

  private ButtonstopButton=new Button("기권");         // 기권 버튼

  private ButtonenterButton=new Button("입장하기");    // 입장하기 버튼

  private ButtonexitButton=new Button("대기실로");      // 대기실로 버튼

 

  // 각종 정보를 보여주는 레이블

  private LabelinfoView=new Label("< 생각하는 자바 >", 1);

 

  private OmokBoardboard=new OmokBoard(15,30);      // 오목판 객체

  private BufferedReaderreader;                         // 입력 스트림

  private PrintWriterwriter;                               // 출력 스트림

  private Socketsocket;                                 // 소켓

  private introomNumber=-1;                           // 방 번호

  private StringuserName=null;                          // 사용자 이름

  publicOmokClient(String title){                        // 생성자

    super(title);

    setLayout(null);                                // 레이아웃을 사용하지 않는다.

 

    // 각종 컴포넌트를 생성하고 배치한다.

    msgView.setEditable(false);

    infoView.setBounds(10,30,480,30);

    infoView.setBackground(new Color(200,200,255));

    board.setLocation(10,70);

    add(infoView);

    add(board);

    Panel p=new Panel();

    p.setBackground(new Color(200,255,255));

    p.setLayout(new GridLayout(3,3));

    p.add(new Label("이     름:", 2));p.add(nameBox);

    p.add(new Label("방 번호:", 2)); p.add(roomBox);

    p.add(enterButton); p.add(exitButton);

    enterButton.setEnabled(false);

    p.setBounds(500,30, 250,70);

    

    Panel p2=new Panel();

    p2.setBackground(new Color(255,255,100));

    p2.setLayout(new BorderLayout());

    Panel p2_1=new Panel();

    p2_1.add(startButton); p2_1.add(stopButton);

    p2.add(pInfo,"North"); p2.add(pList,"Center"); p2.add(p2_1,"South");

    startButton.setEnabled(false); stopButton.setEnabled(false);

    p2.setBounds(500,110,250,180);

    

    Panel p3=new Panel();

    p3.setLayout(new BorderLayout());

    p3.add(msgView,"Center");

    p3.add(sendBox, "South");

    p3.setBounds(500, 300, 250,250);

    

    add(p); add(p2); add(p3);

 

    // 이벤트 리스너를 등록한다.

    sendBox.addActionListener(this);

    enterButton.addActionListener(this);

    exitButton.addActionListener(this);

    startButton.addActionListener(this);

    stopButton.addActionListener(this);

    

      // 윈도우 닫기 처리

    addWindowListener(new WindowAdapter(){

       public void windowClosing(WindowEvent we){

         System.exit(0);

       }

    });

  }

 

  // 컴포넌트들의 액션 이벤트 처리

  public voidactionPerformed(ActionEvent ae){

    if(ae.getSource()==sendBox){             // 메시지 입력 상자이면

      String msg=sendBox.getText();

      if(msg.length()==0)return;

      if(msg.length()>=30)msg=msg.substring(0,30);

      try{  

        writer.println("[MSG]"+msg);

        sendBox.setText("");

      }catch(Exception ie){}

    }

 

    else if(ae.getSource()==enterButton){         // 입장하기 버튼이면

      try{

        

        if(Integer.parseInt(roomBox.getText())<1){

          infoView.setText("방번호가 잘못되었습니다. 1이상");

          return;

        }

          writer.println("[ROOM]"+Integer.parseInt(roomBox.getText()));

          msgView.setText("");

        }catch(Exception ie){

          infoView.setText("입력하신 사항에 오류가 았습니다.");

        }

    }

 

    else if(ae.getSource()==exitButton){           // 대기실로 버튼이면

      try{

        goToWaitRoom();

        startButton.setEnabled(false);

        stopButton.setEnabled(false);

      }catch(Exception e){}

    }

 

    else if(ae.getSource()==startButton){          // 대국 시작 버튼이면

      try{

        writer.println("[START]");

        infoView.setText("상대의 결정을 기다립니다.");

        startButton.setEnabled(false);

      }catch(Exception e){}

    }

 

    else if(ae.getSource()==stopButton){          // 기권 버튼이면

      try{

        writer.println("[DROPGAME]");

        endGame("기권하였습니다.");

      }catch(Exception e){}

    }

  }

 

  voidgoToWaitRoom(){                   // 대기실로 버튼을 누르면 호출된다.

    if(userName==null){

      String name=nameBox.getText().trim();

      if(name.length()<=2 || name.length()>10){

        infoView.setText("이름이 잘못되었습니다. 3~10자");

        nameBox.requestFocus();

        return;

      }

      userName=name;

      writer.println("[NAME]"+userName);    

      nameBox.setText(userName);

      nameBox.setEditable(false);

    }  

    msgView.setText("");

    writer.println("[ROOM]0");

    infoView.setText("대기실에 입장하셨습니다.");

    roomBox.setText("0");

    enterButton.setEnabled(true);

    exitButton.setEnabled(false);

  }

 

  public voidrun(){

    Stringmsg;                             // 서버로부터의 메시지

    try{

    while((msg=reader.readLine())!=null){

 

        if(msg.startsWith("[STONE]")){     // 상대편이 놓은 돌의 좌표

          String temp=msg.substring(7);

          int x=Integer.parseInt(temp.substring(0,temp.indexOf(" ")));

          int y=Integer.parseInt(temp.substring(temp.indexOf(" ")+1));

          board.putOpponent(x, y);     // 상대편의 돌을 그린다.

          board.setEnable(true);        // 사용자가 돌을 놓을 수 있도록 한다.

        }

 

        else if(msg.startsWith("[ROOM]")){    // 방에 입장

          if(!msg.equals("[ROOM]0")){          // 대기실이 아닌 방이면

            enterButton.setEnabled(false);

            exitButton.setEnabled(true);

            infoView.setText(msg.substring(6)+"번 방에 입장하셨습니다.");

          }

          else infoView.setText("대기실에 입장하셨습니다.");

 

          roomNumber=Integer.parseInt(msg.substring(6));     // 방 번호 지정

 

          if(board.isRunning()){                    // 게임이 진행중인 상태이면

            board.stopGame();                    // 게임을 중지시킨다.

          }

        }

 

        else if(msg.startsWith("[FULL]")){       // 방이 찬 상태이면

          infoView.setText("방이 차서 입장할 수 없습니다.");

        }

 

        else if(msg.startsWith("[PLAYERS]")){      // 방에 있는 사용자 명단

          nameList(msg.substring(9));

        }

 

        else if(msg.startsWith("[ENTER]")){        // 손님 입장

          pList.add(msg.substring(7));

          playersInfo();

          msgView.append("["+ msg.substring(7)+"]님이 입장하였습니다.\n");

        }

        else if(msg.startsWith("[EXIT]")){          // 손님 퇴장

          pList.remove(msg.substring(6));            // 리스트에서 제거

          playersInfo();                        // 인원수를 다시 계산하여 보여준다.

          msgView.append("["+msg.substring(6)+

                                         "]님이 다른 방으로 입장하였습니다.\n");

          if(roomNumber!=0)

            endGame("상대가 나갔습니다.");

        }

 

        else if(msg.startsWith("[DISCONNECT]")){     // 손님 접속 종료

          pList.remove(msg.substring(12));

          playersInfo();

          msgView.append("["+msg.substring(12)+"]님이 접속을 끊었습니다.\n");

          if(roomNumber!=0)

            endGame("상대가 나갔습니다.");

        }

 

        else if(msg.startsWith("[COLOR]")){          // 돌의 색을 부여받는다.

          String color=msg.substring(7);

          board.startGame(color);                      // 게임을 시작한다.

          if(color.equals("BLACK"))

            infoView.setText("흑돌을 잡았습니다.");

          else

            infoView.setText("백돌을 잡았습니다.");

          stopButton.setEnabled(true);                 // 기권 버튼 활성화

        }

 

        else if(msg.startsWith("[DROPGAME]"))      // 상대가 기권하면

          endGame("상대가 기권하였습니다.");

 

        else if(msg.startsWith("[WIN]"))              // 이겼으면

          endGame("이겼습니다.");

 

        else if(msg.startsWith("[LOSE]"))            // 졌으면

          endGame("졌습니다.");

 

        // 약속된 메시지가 아니면 메시지 영역에 보여준다.

        else msgView.append(msg+"\n");

    }

    }catch(IOException ie){

      msgView.append(ie+"\n");

    }

    msgView.append("접속이 끊겼습니다.");

  }

 

  private voidendGame(String msg){                // 게임의 종료시키는 메소드

    infoView.setText(msg);

    startButton.setEnabled(false);

    stopButton.setEnabled(false);

 

    try{ Thread.sleep(2000); }catch(Exception e){}    // 2초간 대기

 

    if(board.isRunning())board.stopGame();

    if(pList.getItemCount()==2)startButton.setEnabled(true);

  }

 

  private voidplayersInfo(){                 // 방에 있는 접속자의 수를 보여준다.

    int count=pList.getItemCount();

    if(roomNumber==0)

      pInfo.setText("대기실: "+count+"명");

    else pInfo.setText(roomNumber+" 번 방: "+count+"명");

 

    // 대국 시작 버튼의 활성화 상태를 점검한다.

    if(count==2 && roomNumber!=0)

      startButton.setEnabled(true);

    else startButton.setEnabled(false);

  }

 

  // 사용자 리스트에서 사용자들을 추출하여 pList에 추가한다.

  private voidnameList(String msg){

    pList.removeAll();

    StringTokenizer st=new StringTokenizer(msg, "\t");

    while(st.hasMoreElements())

      pList.add(st.nextToken());

    playersInfo();

  }

 

  private voidconnect(){                    // 연결

    try{

      msgView.append("서버에 연결을 요청합니다.\n");

      socket=new Socket("192.168.1.28", 7777);

      msgView.append("---연결 성공--.\n");

      msgView.append("이름을 입력하고 대기실로 입장하세요.\n");

      reader=new BufferedReader(

                           new InputStreamReader(socket.getInputStream()));

      writer=new PrintWriter(socket.getOutputStream(), true);

      new Thread(this).start();

      board.setWriter(writer);

    }catch(Exception e){

      msgView.append(e+"\n\n연결 실패..\n");  

    }

  }

  public static voidmain(String[] args){

    OmokClient client=new OmokClient("네트워크 오목 게임");

    client.setSize(760,560);

    client.setVisible(true);

    client.connect();

  }

}


 

 

 

 

연습 문제

 

 

1. 오목 서버 프로그램에 방 관리자 객체를 추가하여 보다 효율적인 프로그램이 되도록 수정해보자.

 

사용자 삽입 이미지

[그림 22-4] 오목 서버의 진화


Posted by 영웅기삼
,