programming/actionscript2014.06.30 15:01

마지막으로 AppWarp를 사용해서 박스옮기기를 구현해 보겠습니다.


AppWarp란?

앞서 설명한 YGN과 거의 비슷한 멀티플레이 게임 클라우드 서비스입니다. 해외 게임 관련 포럼을 뒤져보면 사람들 입에 꽤 많이 오르내리더라구요. 실제로 사용해보니 꽤 괜찮아서 소개해 보려고 합니다. 


여기서 조금 헷갈리는 것은 ShepHertz라는 회사에서 제공하는 서비스가 여러개 라는 점입니다. AppWarp는 그 중 하나일 뿐이구요. YGN처럼 Server-Side Code를 직접 짜서 올리는 AppWarp S2라는 서비스도 있고, 온라인 게임이나 앱용 API를 제공해 주는 App42CloudAPI라는 서비스도 있는데, 여기서는 AppWarp만 보도록 하겠습니다.


AppWarp에 대한 컨셉설명은 여기를 참고해 주세요.

http://appwarp.shephertz.com/realtime-multiplayer-game-engine-overview/

그냥 모바일 환경별 클라이언트 SDK 다 제공해주고 AppWarp서버에서 로비, 룸 등 네트워크 플레이에 필요한 기능들 다 제공해준다~~~ 라고 이해하시면 되겠습니다.


일단 AppWarp 가격 정책은 유료입니다만, 인디게임을 위해 한 달 "메세지 송신 200만 회" 까지만 무료입니다. 유료라고 해봐야 한 달 $100 수준이라 꽤 저렴한 편이죠. 자세한 가격정책은 여기서 확인해 보세요.



가입하기

무료가입이니 가입부터 합시다. 페이스북이나 GitHub 계정과도 연동 가능합니다. 가입과 초기설정 절차는 appwarp 사이트에 문서화가 매우 잘 되어 있으니 아래 링크를 보고 따라해 보시기 바랍니다.

참고 : http://appwarp.shephertz.com/game-development-center/Using-AppHQ/


[관리자 화면이 깔끔하게 구성되어 있습니다.]



클라이언트 구현하기

구현도 매우 쉽습니다. Flash로 구현하는 법은 튜토리얼로 잘 정리되어 있습니다. 하지만 직접 구현해봐야 성에 차겠죠? 이전과 마찬가지로 박스 옮기기를 만들어 봅시다.

일단 http://appwarp.shephertz.com/downloads/appwarp-downloads.php 에서 Adobe AIR SDK를 다운 받습니다.

압축을 풀어 보면 AppWarpLib.swc 파일이 있는데, 이걸 개발할 프로젝트에 Library로 추가합니다.

AppWarp는 이전 포스팅에서 본 YGN의 컨셉과 달리 Server-Side Code를 직접 작성하지 않습니다. 서버는 오로지 채팅서버와 같이 로비와 룸에 기반한 브로드캐스팅(접속된 모든 유저에게 알림)만 제공할 뿐이라, 게임 각각의 비지니스 로직 구현은 클라이언트에서 구현해야 합니다. (Shephertz에서는 서버코드(Java)를 직접 짤 수 있는 서비스인 AppWarp S2도 준비중이라고 합니다.)

클라이언트 코드는 접속관련 기능의 ConnectionRequestListner, 룸 관련 기능의 RoomRequestListner, 그리고 메세지를 주고 받는 기능인 NotificationListner를 interface를 상속받아 만들어야 합니다. 따라서 AppWarpListner라는 class를 만들어서 저 인터페이스들을 상속받아 구현해보겠습니다. (각 interface를 구현한 클래스를 각각 만들어도 되지만.. 귀찮으므로 한 클래스에 몰아넣도록 하겠습니다.)

AppWarpListner.as

package  
{  
    import com.shephertz.appwarp.listener.ConnectionRequestListener;  
    import com.shephertz.appwarp.listener.NotificationListener;  
    import com.shephertz.appwarp.listener.RoomRequestListener;  
    import com.shephertz.appwarp.messages.Chat;  
    import com.shephertz.appwarp.messages.LiveRoom;  
    import com.shephertz.appwarp.messages.Lobby;  
    import com.shephertz.appwarp.messages.Move;  
    import com.shephertz.appwarp.messages.Room;  
    import com.shephertz.appwarp.types.ResultCode;  
    import com.shephertz.appwarp.WarpClient;  
    import flash.utils.ByteArray;  
      
    public class AppWarpListener implements ConnectionRequestListener, RoomRequestListener, NotificationListener  
    {  
        private var _owner:Main;  
          
        public function AppWarpListener(owner:Main)  
        {  
            _owner = owner;  
        }  
          
        public function onConnectDone(res:int):void  
        {  
            if (res == ResultCode.success) {  
                _owner.log("onConnectDone");  
                  
                WarpClient.getInstance().joinRoom(_owner.roomID);  
                WarpClient.getInstance().subscribeRoom(_owner.roomID);  
                  
                //set udp  
                //WarpClient.getInstance().initUDP();  
            }  
            else if(res == ResultCode.api_not_found || res == ResultCode.auth_error){  
                _owner.log("Verify your api key and secret key");  
            }  
            else if(res == ResultCode.connection_error){  
                _owner.log("Network Error. Check your internet connectivity and retry.");  
            }  
            else{  
                _owner.log("Unknown Error : Result Code(" + res + ")");  
            }  
        }  
          
        public function onDisConnectDone(res:int):void  
        {  
            _owner.log("onDisConnectDone");  
        }  
          
        public function onInitUDPDone(res:int):void  
        {  
            _owner.log("onInitUDPDone");  
        }  
          
        public function onSubscribeRoomDone(event:Room):void  
        {  
            _owner.log("onSubscribeRoomDone");  
        }  
          
        public function onUnsubscribeRoomDone(event:Room):void  
        {  
            _owner.log("onUnsubscribeRoomDone");  
        }  
          
        public function onJoinRoomDone(event:Room):void  
        {  
            if(event.result == ResultCode.success){  
                _owner.log("onJoinRoomDone");  
                _owner.drawBox();  
            }  
            else{  
                _owner.log("Room join failed. Verify your room id.");  
            }  
        }  
          
        public function onLeaveRoomDone(event:Room):void  
        {  
            _owner.log("onLeaveRoomDone");  
        }  
          
        public function onGetLiveRoomInfoDone(event:LiveRoom):void  
        {  
            _owner.log("onGetLiveRoomInfoDone");  
        }  
          
        public function onSetCustomRoomDataDone(event:LiveRoom):void  
        {  
            _owner.log("onSetCustomRoomDataDone");  
        }  
          
        public function onLockPropertiesDone(result:int):void  
        {  
            _owner.log("onLockPropertiesDone");  
        }  
          
        public function onUnlockPropertiesDone(result:int):void  
        {  
            _owner.log("onUnlockPropertiesDone");  
        }  
          
        public function onUpdatePropertiesDone(event:LiveRoom):void  
        {  
            _owner.log("onUpdatePropertiesDone");  
        }  
          
        public function onRoomCreated(event:Room):void  
        {  
            _owner.log("onRoomCreated");  
        }  
          
        public function onRoomDestroyed(event:Room):void  
        {  
            _owner.log("onRoomDestroyed");  
        }  
          
        public function onUserLeftRoom(event:Room, user:String):void  
        {  
            _owner.log("onUserLeftRoom");  
        }  
          
        public function onUserJoinedRoom(event:Room, user:String):void  
        {  
            _owner.log("onUserJoinedRoom");  
        }  
          
        public function onUserResumed(roomid:String, isLobby:Boolean, username:String):void  
        {  
            _owner.log("onUserResumed");  
        }  
          
        public function onUserPaused(roomid:String, isLobby:Boolean, username:String):void  
        {  
            _owner.log("onUserPaused");  
        }  
          
        public function onUserLeftLobby(event:Lobby, user:String):void  
        {  
            _owner.log("onUserLeftLobby");  
        }  
          
        public function onUserJoinedLobby(event:Lobby, user:String):void  
        {  
            _owner.log("onUserJoinedLobby");  
        }  
          
        public function onPrivateChatReceived(sender:String, chat:String):void  
        {  
            _owner.log("onPrivateChatReceived");  
        }  
          
        public function onChatReceived(event:Chat):void  
        {  
            _owner.log("onChatReceived");  
        }  
          
        public function onUpdatePeersReceived(update:ByteArray, fromUDP:Boolean):void  
        {  
            var msg:String = update.readUTF();  
            var msgArray:Array = msg.split(",");  
            var sender:String = msgArray[0];  
            var cmd:String = msgArray[1];  
            var x:int = parseInt(msgArray[2]);  
            var y:int = parseInt(msgArray[3]);  
              
            if(sender != _owner.localUsername){  
                _owner.moveBox(x, y);          
            }  
        }  
          
        public function onUserChangeRoomProperties(room:Room, user:String, properties:Object, lockTable:Object):void  
        {  
            _owner.log("onUserChangeRoomProperties");  
        }  
          
        public function onMoveCompleted(moveEvent:Move):void  
        {  
            _owner.log("onMoveCompleted");  
        }  
          
        public function onGameStarted(sender:String, roomid:String, nextTurn:String):void  
        {  
            _owner.log("onGameStarted");  
        }  
          
        public function onGameStopped(sender:String, roomid:String):void  
        {  
            _owner.log("onGameStarted");  
        }  
    }  
}  
뭐 너무 간단하고.. 함수, 변수 네이밍이 직관적이라 별다른 설명이 필요가 없습니다.
이번엔 Main class입니다.

Main.as

package       
{      
    import com.shephertz.appwarp.types.ConnectionState;  
    import com.shephertz.appwarp.WarpClient;  
    import flash.display.Sprite;  
    import flash.display.StageAlign;  
    import flash.display.StageScaleMode;  
    import flash.events.MouseEvent;  
    import flash.text.TextField;  
    import flash.utils.ByteArray;  
      
    public class Main extends Sprite       
    {      
        // AppWarp String Constants          
        public var localUsername:String;  
        public var roomID:String = "셋팅한 RoomID";  
        private var apiKey:String = "당신의 API KEY"  
        private var secretKey:String = "당신의 Secret Key";  
          
        private var listener:AppWarpListener;  
        private var txtLog:TextField;      
        private var mcBox:Sprite;      
              
        public function Main():void       
        {      
            //init display      
            stage.scaleMode = StageScaleMode.NO_SCALE;      
            stage.align = StageAlign.TOP_LEFT;      
                  
            //init log  
            txtLog = new TextField;      
            txtLog.selectable = false;      
            txtLog.mouseEnabled = false;      
            txtLog.multiline = true;      
            txtLog.width = stage.stageWidth;      
            txtLog.height = stage.stageHeight;      
            addChild(txtLog);  
              
            //connect     
            localUsername = "GuestUser" + int(Math.random() * 100);  
            trace(localUsername);  
            listener = new AppWarpListener(this);  
            WarpClient.initialize(apiKey, secretKey);  
            WarpClient.getInstance().setConnectionRequestListener(listener);  
            WarpClient.getInstance().setRoomRequestListener(listener);  
            WarpClient.getInstance().setNotificationListener(listener);  
            WarpClient.getInstance().connect(localUsername);  
        }      
            
        private function onDrag(e:MouseEvent):void       
        {      
            this.stage.addEventListener(MouseEvent.MOUSE_MOVE, onMove);      
        }      
      
        private function onDrop(e:MouseEvent):void       
        {      
            this.stage.removeEventListener(MouseEvent.MOUSE_MOVE, onMove);      
        }      
              
        protected function onMove(e:MouseEvent):void      
        {      
            mcBox.x = this.mouseX - mcBox.width/2;      
            mcBox.y = this.mouseY - mcBox.height/2;      
            e.updateAfterEvent();      
                  
            //broadcast  
            if (WarpClient.getInstance().getConnectionState() == ConnectionState.connected)  
            {  
                //make msg  
                var msg:String = localUsername + ",move," + mcBox.x + "," + mcBox.y;  
                var msgBytes:ByteArray = new ByteArray();  
                msgBytes.writeUTF(msg);  
                  
                //tcp send  
                WarpClient.getInstance().sendUpdate(msgBytes);  //set udp : WarpClient.getInstance().sendUdpUpdate(msgBytes);  
            }  
        }    
              
        public function drawBox():void      
        {      
            mcBox = new Sprite();      
            mcBox.graphics.lineStyle(3,0x00ff00);      
            mcBox.graphics.beginFill(0x0000FF);      
            mcBox.graphics.drawRect(0,0,100,100);      
            mcBox.graphics.endFill();      
            addChild(mcBox);      
            setChildIndex(mcBox, 0);      
      
            mcBox.addEventListener(MouseEvent.MOUSE_DOWN, onDrag);      
            this.stage.addEventListener(MouseEvent.MOUSE_UP, onDrop);      
            log("drawBox...");      
        }  
          
        public function moveBox(_x:int, _y:int):void   
        {  
            mcBox.x = _x;  
            mcBox.y = _y;  
        }  
              
        public function log(msg:String):void      
        {      
            txtLog.appendText(msg + "\n");      
        }      
    }      
}      
코드를 보시면 제가 //set udp 로 주석처리된 코드를 보셨을 텐데, 이 코드를 대체하면 CS기반에서 P2P기반 서비스로 바뀌게 됩니다. 즉, 클라이언트간에 UDP 홀펀칭을 만들어서 P2P통신을 하게 해준다는거죠! 프라우드넷이 부럽지 않습니다. 
하지만 UDP를 쓰겠다는건 무슨뜻 일까요? AS3.0에서 UDP를 쓰려면 앞서서 포스팅한 DatagramSocket class를 사용할 수 밖에 없습니다.... 네... UDP를 쓰게 되면 AIR에서만 가능하고 Flash Player에서는 사용 할 수 없습니다.

모바일 전용 게임이라면 서버 부하도 줄이고 Peer간 전송속도도 가장 우수한 udp 방식을 선택하겠지만... 여기 블로그에 결과를 올려 돌려보기 위해서 그냥 tcp 기반으로 구현해서 올렸습니다.


구현결과
CS기반으로 해도 성능이 꽤 훌륭합니다. P2P(udp)로 모바일 네트워크(3G)망에서 테스트도 해봤는데, 웬만한 상용 리얼타임 액션게임을 개발해도 될 정도로 우수하고 안정적인 성능을 보여줬었습니다.

Client A


Client B



결론

무료 뿐만 아니라, 유료의 값어치를 충분히 하는 서비스인건 분명합니다. 게다가 미국, 브라질, 아일랜드, 일본, 싱가폴에 서버가 있기 때문에 해외를 타겟으로 한 클라우드 서비스로도 충분해 보이구요. 워낙 좋은 부가 서비스도 많아서 일정 규모 이상 키울만한 프로젝트라면 YGN보다는 AppWarp를 검토해 보시는게 좋을 것 같습니다.


SmartFox나 Photon 같은 다른 서비스들도 조금 검토해 봤는데, 설명이 너무 미비해서 구현방법이 너무 복잡하고 준비된 샘플코드도 정상적이게 돌아가지 않는 등 문제가 많았습니다. 서비스들 테스트는 여기까지 해보고 다음엔 조금 제대로 된 게임을 구현해보고 포스팅을 해보도록 하겠습니다. 이만...

Posted by 귀뫄뉘

댓글을 달아 주세요