S49 스페셜 미션 – Alternativa3D를 이용해 과녁 모델 로딩하여 보여주기

No Comments

image오늘 미션은 첫 번째 수업 시간에 만들었던 과녁 모델을 A3D파일로 export한 후 Alternativa3D를 이용해 화면에 보여주는 것입니다. 기본적인 과정은 외부 A3D파일 로딩 – 로딩완료 – 파싱 – 맵핑 이런 순서대로 이루어집니다. 맵핑 부분은 생각만큼 잘 되지 않는군요. 맥스에서 언랩한 이미지를 엠베드하여 재질로 사용했는데 결과는 아래와 같이 엉뚱하게 맵핑이 되어버립니다. 대충 어떤 문제인지는 알겠지만 해결할 방법은 떠오르지 않네요; 아무튼 코드를 보겠습니다.

 

package{
	import alternativa.engine3d.core.*;
	import alternativa.engine3d.loaders.*;
	import alternativa.engine3d.materials.*;
	import alternativa.engine3d.objects.Mesh;
	import alternativa.engine3d.primitives.*;
	import alternativa.engine3d.resources.*;

	import flash.display.*;
	import flash.events.*;
	import flash.net.*;
	import flash.utils.*;

	[SWF( frameRate="60")]
	public class Alternativa3DBasic2 extends Sprite{

		[Embed( source = 'embed/target.jpg' )]
		private var _targetMap:Class;

		private var _camera:Camera3D;
		private var _stage3D:Stage3D;
		private var _root:Object3D;
		private var _model:Mesh;

		public function Alternativa3DBasic2(){
			initStage();
			initStage3D();
			initConatiner();
			initCamera();
		}
		private function initStage():void{
			stage.align = StageAlign.TOP_LEFT;
			stage.scaleMode = StageScaleMode.NO_SCALE;
		}
		private function initStage3D():void{
			_stage3D = stage.stage3Ds[0];
			_stage3D.addEventListener( Event.CONTEXT3D_CREATE, onContextCreate );
			_stage3D.requestContext3D();
		}
		private function onContextCreate( $e:Event ):void {
			loadModel();
		}
		private function loadModel():void{
			var modelLoader:URLLoader;

			modelLoader = new URLLoader();
			modelLoader.dataFormat = URLLoaderDataFormat.BINARY;
			modelLoader.load(new URLRequest( 'remote/target.A3D' ) );
			modelLoader.addEventListener( Event.COMPLETE, onModelLoad );
		}
		private function onModelLoad( $e:Event ):void{
			createModel( ( $e.target as URLLoader ).data );
			stage.addEventListener( Event.ENTER_FRAME, render );
		}
		private function createModel( $data:ByteArray ):void{
			var material:TextureMaterial, bitmapResource:BitmapTextureResource;

			bitmapResource = new BitmapTextureResource( new _targetMap().bitmapData );
			material = new TextureMaterial( bitmapResource );

			_model = getModel( $data );
			_model.x -= 10;
			_model.setMaterialToAllSurfaces( material );
			_model.geometry.upload( _stage3D.context3D );
			_model.getSurface( 0 ).material.getResources()[0].upload( _stage3D.context3D );
			_root.addChild( _model );
		}
		private function getModel( $data:ByteArray ):Mesh{
			return parseModel( $data );
		}
		private function parseModel( $data:ByteArray ):Mesh{
			var parser:ParserA3D, object:Object3D;

			parser = new ParserA3D();
			parser.parse( $data );

			for each ( object in parser.objects ) {
				if ( object.name === 'target' ) {
					return object as Mesh;
				}
			}

			return null;
		}
		private function render( $e:Event ):void {
			_model.rotationY -= 0.04;
			_model.rotationZ -= 0.04;
			_camera.render( _stage3D );
		}
		private function initConatiner():void{
			_root = new Object3D();
		}
		private function initCamera():void{
			_camera = new Camera3D( 0.1, 10000 );
			_camera.view = new View( stage.stageWidth, stage.stageHeight, false, 0, 0, 4 );
			_camera.rotationX = -120*Math.PI/180;
			_camera.y = -800;
			_camera.z = 400;
			_root.addChild( _camera );
			addChild( _camera.view );
			addChild( _camera.diagram );
		}
	}
}

기본적으로 앞서 작성했던 포스팅에서의 코드와 크게 다르지 않습니다. 저번 미션에서 지적 받은 부분을 수정하고, 모델을 로딩하여 보여주는 부분만 추가하였습니다. 지적 받은 리소스를 for each문을 통해 한꺼번에 업로드 하는 부분을 명시적으로 업로드하게 바꾸고 _container로 사용했던 더 알맞은 개념인 _root로 바꾸었습니다. 사실 저도 처음에 정확한 개념을 인지하지 못하고 코드를 짜서 그런 네이밍이 나온듯 합니다. A3D파일을 로딩하는 부분은 URLLoader를 사용하여 ByteArray형태로 로딩한 데이터를 ParserA3D클래스의 인스턴스를 통해 Mesh인스턴스를 가져옵니다. 여기서 중요한 점은 if문의 object.name === ‘target’ 이 ‘target’이란 문자열이 맥스에서 작업할 때 메쉬에 지정한 이름을 가리킨다는 것입니다. 이렇게 가져온 후 Mesh로 형 변환을 하여 런타임에서 사용할 수 있습니다. 나머지 코드들은 앞의 포스팅 코드와 다를게 없으므로 설명을 생략합니다. 재질을 어떻게 맵핑해야 맥스에서 보던 것처럼 할 수 있는지 궁금하군요. 소스 코드는 A3D파일을 로딩하는 버젼과 엠베드하는 버젼 2개 모두 포함하고 있습니다. 외부 로딩을 하면 보안 문제 때문에 블로그에 올려서 보기가 힘들더군요. 그래서 2가지 버젼으로 작성을 했습니다. 그럼 이만 마치겠습니다.

결과물

예제 소스

S49 스페셜 미션 – Alternativa3D 기초

No Comments

logo아 요즘 부쩍 S49관련 포스팅이 많아 지네요. 어차피 개인 블로그로써의 역할을 상실한지 오래라 이렇게라도 사용하게 되어 기쁩니다. ㅎㅎ; 이번 포스팅은 Alternativa3D로 가장 간단한 기본 도형을 만들어보고 텍스쳐를 입혀보는 것이 주된 내용입니다. 사실 저도 전에 Papervision3D나 Away3D는 많이는 아니지만 그래도 가끔 사용해 왔기 때문에 낯설지 않지만, Alternativa3D는 처음 사용해 보기 때문에 많이 낯선데요. 초심자 입장으로 포스팅을 작성하겠습니다. 그럼 이제 시작해 볼까요?

시작하기 앞서 필요한 것들을 살펴보면.. Flash Player 11, 11.0 버젼의 playerglobal.swc, Alternativa3D 등이 있어야 합니다. Flash Player 11로 개발하기 위한 설정은 이미 많은 포스팅들이 있기에 생략하겠습니다(구글링 하세요!)

이번 포스팅에서는 기초적인 내용을 다루기 때문에 간단한 박스와 구체를 만들고, 각각 색이 칠해진 재질과 외부 이미지를 사용한 재질을 입혀 보겠습니다. 그럼 바로 코드를 보며 설명하겠습니다.

package{
	import alternativa.engine3d.core.*;
	import alternativa.engine3d.materials.*;
	import alternativa.engine3d.primitives.*;
	import alternativa.engine3d.resources.*;

	import flash.display.*;
	import flash.events.*;

	public class Alternativa3D extends Sprite{

		[Embed( source = 'embed/hakase.jpg' )]
		private var BitmapTexture:Class;

		private var _camera:Camera3D;
		private var _stage3D:Stage3D;

		private var _container:Object3D;
		private var _box:Box;
		private var _sphere:GeoSphere;

		public function Alternativa3D(){
			initStage();
			initConatiner();
			initCamera();
			createBox();
			createSphere();
			initStage3D();
		}
		private function initStage():void{
			stage.align = StageAlign.TOP_LEFT;
			stage.scaleMode = StageScaleMode.NO_SCALE;
		}
		private function initConatiner():void{
			_container = new Object3D();
		}
		private function initCamera():void{
			_camera = new Camera3D( 0.1, 10000 );
			_camera.view = new View( stage.stageWidth, stage.stageHeight, false, 0, 0, 4 );
			_camera.rotationX = -120*Math.PI/180;
			_camera.y = -800;
			_camera.z = 400;
			_container.addChild( _camera );
			addChild( _camera.view );
			addChild( _camera.diagram );
		}
		private function createBox():void{
			var material:FillMaterial;

			material = new FillMaterial( 0xffe500 );

			_box = new Box( 200, 200, 200, 8, 8, 8 );
			_box.x = -200;
			_box.setMaterialToAllSurfaces( material );
			_container.addChild( _box );
		}
		private function createSphere():void{
			var material:TextureMaterial, bitmapResource:BitmapTextureResource;

			bitmapResource = new BitmapTextureResource( new BitmapTexture().bitmapData );
			material = new TextureMaterial( bitmapResource );

			_sphere = new GeoSphere( 200, 6 );
			_sphere.x = 200;
			_sphere.setMaterialToAllSurfaces( material );
			_container.addChild( _sphere );
		}
		private function initStage3D():void{
			_stage3D = stage.stage3Ds[0];
			_stage3D.addEventListener( Event.CONTEXT3D_CREATE, onContextCreate );
			_stage3D.requestContext3D();
		}
		private function onContextCreate( $e:Event ):void {
			var resource:Resource;

			for each ( resource in _container.getResources( true ) ) {
				resource.upload( _stage3D.context3D );
			}
			stage.addEventListener( Event.ENTER_FRAME, render );
		}
		private function render( $e:Event ):void {
			_camera.view.width = stage.stageWidth;
			_camera.view.height = stage.stageHeight;
			_camera.render( _stage3D );
			_box.rotationZ -= 0.03;
			_sphere.rotationZ -= 0.03;
		}
	}
}

호스트 코드의 생성자를 보시면 여러 초기화 함수들이 보입니다. 맨 위에 initStage()함수부터 살펴보면 내용은 그리 어렵지 않습니다. 자주 사용하는 Stage에 관한 설정입니다. 다음은 initContainer()함수입니다. 이 함수는 앞으로 만들 박스, 구체, 카메라를 담을 컨테이너를 초기화합니다. 다음으로 Alternativa3D의 Camera클래스를 초기화하는 initCamera()함수가 등장합니다. 생성자로 nearClipping과 farClipping을 지정해주고, view속성에 View클래스를 인스턴스화하여 지정합니다. 카메라의 위치를 조정하고 컨테이너에 자식으로 추가합니다. 조금 생소한 부분은 그 다음 줄의 addChild( _camera.view ), addChild( _camera.diagram ) 입니다. 정확하게 이 코드의 의미는 모르겠으나 화면에 도형을 보여주기 위해서 꼭 필요한 부분인 것 같습니다. 이제 박스를 추가하기 위한 createBox()함수가 등장합니다. 이 함수를 보시면 코드가 그리 어렵지 않습니다. 재질로 사용할 FillMaterial인스턴스를 생성하고 Box인스턴스를 생성하여 setMaterialToAllSurfaces()메서드를 통해 재질을 적용하고 컨테이너에 자식으로 추가합니다. 다음 createSphere()함수는 구체를 생성하고 재질은 앞선 FillMaterial클래스 보다 조금 복잡한 TextureMaterial클래스를 사용합니다. 제가 처음에 비트맵으로 재질을 입히려는데 예전 Papervision3D나 Away3D에서는 BitmapMaterial등이 존재해서 비슷하지 않을까 했으나 없더군요..; 그래서 찾아보니 비슷한 역할을 하는 TextureMaterial클래스가 있었습니다. 근데 또 엠베드한 이미지를 사용하기 위해서는 BitmapTextureResource클래스를 이용해서 BitmapTextureResource인스턴스를 TextureMaterial클래스의 생성자에 전달하여 생성해야 재질로 사용할 수 있습니다. 아 그리고 중요한 점이 있는데 이 또한 수업중에 나왔던 내용으로, 재질로 사용될 이미지의 크기는 2의 배수여야 합니다. 2의 배수가 아니면 에러가 발생합니다. initStage3D()함수는 Stage로부터 Stage3D인스턴스를 얻어와 이벤트를 등록하고 Context3D의 인스턴스를 요청합니다. S49 수업 중에 Stage3D의 동작 방식에 대해 설명한적이 있습니다. 그 내용을 생각해 보면 코드가 그리 낯설지 않을 것입니다. 이벤트를 등록 후 Stage3D 인스턴스를 요청하고, 요청이 수락된 후 Context3D인스턴스에 데이터를 업로드하면 화면에 보이게 되는 것이죠. onContextCreate()함수를 살펴 보면 이러한 내용이 코드로 표현되어 있습니다. 컨테이너에 자식으로 추가된 리소스를 하나 씩 얻어와 Context3D에 업로드를 합니다. 그리고 함수 끝 부분에는 렌더링을 하기 위해 ENTER_FRAME이벤트를 등록합니다. render()함수에서는 카메라의 view속성의 너비와 높이를 스테이지의 너비와 높이로 지정해주고, 2개의 메쉬(박스, 구체)를 Z축으로 회전 시킵니다.

이렇게 간단하게 Alternativa3D의 기초적인 코드를 살펴보았습니다. 저도 처음 접해 보는 것이기 때문에 설명이 좀 부족한 것이 사실입니다. 그래도 처음 하시는 분들에게 조금이라도 도움이 되지 않을까 싶습니다. 그럼 이만 마치겠습니다. 미소

결과물

예제 소스

S49 네 번째 시간–언랩하기2

No Comments

S49 네 번째 시간에는 저번 시간에 배웠던 언랩을 하여 만들어낸 jpg파일로 텍스쳐를 생성하여 맵핑하는 방법을 응용해 Box와 Sphere를 합쳐 언랩하여 만들어낸 jpg파일을 맵핑하는 방법을 공부했습니다.

1. 저번 시간에 배웠던 방법으로 언랩하여 매핍을한 Box부터 시작하겠습니다.

image

2. x축으로 50정도 Box를 이동하여 살짝 치워 둡니다.

image

3. 오른쪽 패널의 Create탭을 선택하고 Sphere를 선택합니다.

image

4. 적당한 위치에 Sphere를 생성합니다. 색이 비슷한 경우, Sphere가 선택된 상태에서 오른쪽 패널에서 Name and Color항목을 통해 색을 바꿀 수 있습니다.

image

image

image

5. 오른쪽 패널의 Parameters항목에서 Sphere의 Radius와 Segments를 각각 30, 32로 지정합니다.

image

6. Box를 언랩했던 방식과 같이 언랩을 하여 만들어낸 jpg파일로 맵핑을 합니다.

image

7. 이제 각 메쉬들(Sphere, Box)를 폴리곤으로 편집할 수 있도록, 선택한 후 오른쪽 마우스 버튼을 클릭하면 나오는 컨텍스트 메뉴에서 Conver to Editable Poly선택합니다.

image

image

8. 다음 Sphere선택한 후, 마우스 오른쪽 클릭하면 나오는 컨텍스트 메뉴에서 Attach를 선택합니다.

image

9. 이제 Box를 선택합니다. 그 후 나오는 창에서 OK버튼을 클릭합니다. 두 개의 메쉬가 합쳐진 것을 볼 수 있습니다.

image

image

image

10. 이 합쳐진 메쉬를 언랩하여 jpg파일을 만듭니다. 이번에 언랩할 때 중요한 점은 오른쪽 패널의 Parameters항목의 Channel에서 Map Channel의 수치를 2로 지정하는 것입니다. 그리고 Render To Texture창에서도 Mapping Coordinates항목의 Channel의 수치도 2로 지정해야 합니다. 이렇게 하는 이유에 대해서는 추후에 포스팅 수정을 통해 보강할 예정입니다 ^^;

image

11. 만들어진 jpg를 통해 맵핑을 합니다.

image

12. 맵핑을 하면 메쉬의 텍스쳐가 이상하게 입혀져 있는 것을 볼 수 있습니다. Channel의 수치가 달라서 그런 것이므로 Material Editor창에서 Coordinates항목의 Map Channel의 수치를 2로 지정하면 제대로 맵핑이 됩니다.

image

image

image

저번 시간에는 실습 녹화 중간에 녹화가 끊겨버린 바람에 내용이 확실치 않을 수 있으니 보신 후 이상한 점이나 다른 점이 있다면 바로 피드백을 주시기 바랍니다. 미소

S49 세 번째 시간 – 언랩 하기

No Comments

s49 세 번째 시간에는 언랩을 하여 추출해 낸 텍스쳐를 가지고 메쉬에 맵핑하는 실습을 했습니다. 이번 포스팅은 언랩까지만을 설명할 예정입니다. 녹화를 딱 언랩하는 부분까지 밖에 하질 못해서 복습을 하는데 잘 되지 않는군요. 일단 언랩까지는 성공했습니다. 그 이후는 다음 번에 올리겠습니다. 미소

1. 3D맥스를 켭니다!

2.  상단의 Snaps Toggle을 활성화 시킵니다.

image

3. 우측 패널의 Create 패널에서 Geometry의 Box를 선택합니다.

image

4. 좌 상단 탑뷰에서 클릭한 후 드래그 하여 Box를 생성합니다.

image

image

5. 그대로 Box 선택된 상태에서 오른쪽 패널을 보면 Box의 속성(length, width, height)이 보이는데, 각각 50, 50, 70으로 수정합니다.

image

6. 다음 오른쪽 패널 상단의 Modify탭을 선택한 후, Modifier List를 선택하면 리스트가 쫙 뜨는데, u키를 누르면 커서가 바로 Unwrap UVW로 가게 됩니다. 언랩을 해야하므로 이것을 선택합니다.

image

image

7. Unwrap UVW를 선택하고 나면 다음 스샷과 같이 오른쪽 패널이 변하게 되는데, parameter항목을 보시면 Edit…이 보이실 겁니다. 이걸 눌러주세요.

image

8. Edit UVW 창이 활성화 되면 아래 Face Sub-object Mode를 선택한 후 Ctrl + A를 눌러 Box의 Face를 모두 선택합니다.

image

image

9. 이제 합쳐져 있는 Face를 분리해야 합니다. 상단의 메뉴중에 Mapping을 클릭합니다. 그 후 기본 수치대로 나두고 Ok버튼을 눌러 Face를 분리합니다. 그러면 6개로 분리된 Face를 볼 수 있습니다.

image

image

image

10. 이제 이걸 저장을 하기 전에 렌더링 모드를 변경합니다. 단축키 9번을 누른 후 Common탭을 선택하여 스크롤을 아래로 내리면 Assign Renderer항목이 보이는데, Production의 렌더러를 오른쪽 …버튼을 클릭하여 mental ray Renderer를 선택합니다.

image

image

11. 다음으로 단축키 0번을 눌러 Render To Texture창을 활성화 합니다.

image

12. 스크롤을 조금 내려보면 Mapping Coordinates항목이 보이는데, 우리는 이미 언랩을 했기 때문에 여기서 Object를 Use Existing Channel로 선택합니다.

image

13. 다음 스크롤을 좀 더 아래로 내려 Output항목에서 Add…버튼을 선택합니다. 그리고 DiffuseMap을 선택한 후 Add Elements버튼을 클릭합니다.

image

image

14. 바로 아래 Selected Element Common Settings항목에서 뽑아낼 텍스쳐의 크기, 형식, 경로를 설정할 수 있습니다. …버튼을 클릭하여 파일 형식은 jpg로 하고 품질은 마음대로, 저장할 위치는 편한 위치를 지정합니다. 크기는 512×512를 선택합니다.

image

15. Baked Material항목에서 Output Into Source를 선택합니다.

image

16. 이제 아래의 Render버튼을 클릭하면 렌더링이 시작되고, 언랩된 텍스쳐가 생성됩니다.

image

image

image

 

자 이렇게 언랩을 하는 방법을 복습해 보았습니다. 최대한 포스팅을 보시는 분들이 이해하기 쉽도록 스샷을 많이 찍었습니다. 이 포스팅을 통해 조금이라도 도움을 받으셨으면 좋겠네요 ㅎㅎ

미니 게임 만들기 : 세 번째

4 Comments

끙…

오늘은 ‘비어진 자리 보석 채우기’를 구현해 볼 텐데요. 뭔가 매우 복잡할 거 같다는 생각이 드네요. 일단 비어 있는 부분의 위에 보석이 존재하면 그 보석이 내려와 채우고, 아래쪽 빈 부분이 다 차면 새로 생성된 보석들이 채워야 할 듯 합니다. 음 그래도 글로 써 보니 뭔가 정리가 되는 느낌이!?

  1. 빈 공간은 기존의 보석으로 채운다.
  2. 나머지 빈 공간은 새로운 보석으로 채운다.

그래서!

먼저, 빈 공간을 기존의 보석으로 채우는 기능부터 구현 하는 게 좋을 것 같습니다. 이 부분에서도 엄청난 고민을 했습니다. 어떻게 해야지 효율적으로 빈 공간을 찾아 채울 것인가(…), 근데 역시 효율까지 따지고 그럴 수가 없더군요; 머리에 과부하가 걸려서 그냥 일단 구현을 목표로 삼고 다시 생각해 봤습니다. 빈 공간과 보석이 채워진 공간을 알아 내고, 적당히 알맞게 채우면 되겠다고 생각했습니다. 여기서 적당히 알맞게 란, 다른 비쥬얼드류 게임을 보면 보석이 사라지고 위에 있는 보석이 아래에 비워진 공간을 채우죠. 그래서

  1. 빈 공간과 보석이 채워진 공간을 알아 내고
  2. 이 두 가지 정보를 통해 배열의 몇 번째 보석이 어디로 가야 한다는 것을 알아 내고
  3. 모델상으로 수정하고
  4. 뷰상으로 수정하면 끝!

뭔가 말로 정리해 보면 매우 간단합니다(…) 사실, 1, 3, 4 번은 그리 오래 걸리지 않았는데 2 번은 좀 걸리더군요. 자연스레 생각해 본다면 위에 있는 보석이 아래로 내려가는 건데, 이걸 배열에서 표현하자니 헷갈리더군요. 덕분에 그림을 엄청 그려가면서 했습니다. 처음엔 어차피 한번에 한 라인에서 두 라인만 사라지고, 두라인일 경우 인접한 두 라인이 사라지기 때문에 아래 그림과 같이,  저런 형태로만 일어난다면 빈공간이 연속적으로 일어날거라는 가정으로 알고리즘을 생각했습니다.

ex1

하지만 다른 비쥬얼드 게임을 해보니, 아이템을 쓰거나 할 때 여기 저기서 보석들이 사라질 가능성이 있더군요. 그래서 빈 공간이 듬성 듬성 생길 수 있다는 가정으로 다시 알고리즘을 생각했습니다.

ex2

역시 연속적으로 빈 공간이 생긴다는 가정으로 알고리즘을 짰을 때 보다 코드 량이 많아 지더군요.

GameControl 클래스 fillJewel.as

import com.asnike.P.bejeweled.Jewel;

private function fillBlanks():void{
	var i:int, j:int, map:Array, result:Object;

	map = getMap();
	for( i = 0, j = map.length ; i < j ; ++i ){
		result = getFillsAndBlanks( map, i );
		moveJewelToBlanks( result, i );
	}
}
private function getFillsAndBlanks( $map:Array, $indexY:int ):Object{
	var i:int, j:int, result:Object;

	result = {jewels:[], fills:[], blanks:[]};
	for( i = 0, j = $map[$indexY].length ; i < j ; ++i ){
		if( isFill( $map[i][$indexY] ) ){
			result.jewels[result.jewels.length] = $map[i][$indexY];
			result.fills[result.fills.length] = i;
		}
		if( isBlank( $map[i][$indexY] ) ){
			result.blanks[result.blanks.length] = i;
		}
	}
	return result;
}
private function isFill( $target:Jewel ):Boolean{
	return !isBlank( $target );
}
private function isBlank( $target:Jewel ):Boolean{
	return $target === null;
}
private function moveJewelToBlanks( $data:Object, $indexY:int ):void{
	var moveData:Object;

	moveData = getMoveAmounts( $data.fills, $data.blanks );
	moveJewelsInModel( $data.jewels, moveData.indices, $indexY );
	moveJewelsInView( $data.jewels, moveData.amounts );
}
private function getMoveAmounts( $fills:Array, $blanks:Array ):Object{
	var i:int, j:int, k:int, l:int, blankIndex:int, result:Object, amount:int;

	result = {indices:{original:[], target:[]}, amounts:[]};

	for( i = 0, j = $fills.length ; i < j ; ++i ){
		for( k = 0, l = $blanks.length ; k < l ; ++k ){
			if( hasToMove( $fills[i], $blanks[blankIndex], $blanks.length ) ){
				amount = $blanks.length - blankIndex;
				result.indices.original[result.indices.original.length] = $fills[i];
				result.indices.target[result.indices.target.length] = $fills[i] + amount;
				result.amounts[result.amounts.length] = amount;
				break;
			}else{
				++blankIndex;
			}
		}
	}
	return result;
}
private function hasToMove( $value1:int, $value2:int, $blanksLength:int ):Boolean{
	return $value1 < $value2 && $blanksLength > 0;
}
private function moveJewelsInModel( $jewels:Array, $indices:Object, $indexY:int ):void{
	var i:int, j:int, jewelIndex:int, map:Array;

	map = getMap();
	for( i = 0, j = map[0].length ; i < j ; ++i ){
		if( $indices.original.indexOf( i ) > -1 ){
			map[i][$indexY] = null;
		}
		if( $indices.target.indexOf( i ) > -1 ){
			map[i][$indexY] = $jewels[jewelIndex++];
		}
	}
}
private function moveJewelsInView( $jewels:Array, $amounts:Array ):void{
	var i:int, j:int;

	for( i = 0, j = $amounts.length ; i < j ; ++i ){
		_view.moveDownToJewel( $jewels[i], $amounts[i] );
	}
}

먼저, 빈 공간을 찾습니다. 빈 공간을 기존에 있는 보석들 중에 위쪽에 있는 보석들로 채웁니다. 각각 세로 행 별로 검사하여 채워 넣습니다. 코드를 보시면 상당히 복잡하죠(…) 빈 공간을 찾는 건 쉬운데, 그  빈 공간에 남아 있는 보석들로 적절히 채우는 게 복잡했습니다. 그래서 코드 또한 복잡해졌습니다. 그래도 최대한 서술형으로 코딩 했어요! 제가 생각한 방법 말고 다른 효율적인 방법이 있다면 알려주세요!! 매우 궁금합니다.

마무리

이번 글은 길이가 매우 짧군요. 첫 번째와 두 번째 글이 길다 보니 상대적으로 짧아 보이는(…), 오히려 길면 내용이 많아져서 더 복잡할 것 같아서 짧게 쓰는 게 나을 듯 합니다! 다음 글 내용은 나머지 빈 공간을 새로운 보석들로 채우는 기능을 구현하는 내용이 되겠습니다.

결과물

미니 게임 만들기 : 두 번째

2 Comments

첫 번째 이후

첫 번째 글에서 구현한 내용이 보석을 선택해서 다음 보석을 선택 할 때 근접해 있는 적합한 보석인지 구분하는 부분과 2개 선택 했을 때 두 보석의 위치를 변경해 주는 부분 까지 했는데, 생각해 보니 뷰는 바꿔 줬지만 데이터(모델)은 변경을 안했더군요; 그래서 첫 번째 글 결과물을 보시면 보석을 이동하고, 이동했던 보석을 다시 눌러서 다른 보석을 선택하려 하면 맵엔 해당 보석이 아직 예전 자리에 남아있어서 근처에 보석이 선택되지 않는 현상이 일어나더군요; 항상 데이터를 변경하고 뷰를 수정해야 헷갈리지 않을 텐데, 자꾸 뷰에 집착하게 되는(…) 어쨌든 그 부분을 첫 번째 글을 올린 후에 바로 수정하였습니다.

GameControl 클래스 selectJewel.as 수정 부분

private function shiftSelectedJewels():void{
	shiftJewelsIndex( getSelectedJewels()[0], getSelectedJewels()[1] );
	_view.shiftJewels( getSelectedJewels()[0], getSelectedJewels()[1], checkSameJewelsOneLine );
}
private function shiftJewelsIndex( $jewel1:Jewel, $jewel2:Jewel ):void{
	var jewel1Index:Array, jewel2Index:Array;

	jewel1Index = getJewelIndexInMap( $jewel1 );
	jewel2Index = getJewelIndexInMap( $jewel2 );
	setJewelToMap( $jewel1, jewel2Index[0], jewel2Index[1] );
	setJewelToMap( $jewel2, jewel1Index[0], jewel1Index[1] );
}
private function setJewelToMap( $target:Jewel, $indexX:int, $indexY:int ):void{
	getMap()[$indexX][$indexY] = $target;
}
private function removeAllFromSelectedJewels():void{
	var selected:Array;
	selected = getSelectedJewels();
	removeJewelFromSelectedJewels( selected[0] );
	removeJewelFromSelectedJewels( selected[0] );
}

선택된 보석들을 맵에서의 위치를 바꿔주는 메서드를 구현하고 보석을 2개 선택하면 shiftSelectedJewels() 메서드를 호출하여 뷰, 데이터를 변경하도록 수정하였습니다.

오늘의 목표!!

오늘 구현할 기능은 게임의 핵심 기능이라고 볼 수 있는, 보석들이 일렬로 3개 이상 줄지어 있는지(가로든, 세로든) 확인하는 기능입니다. 맨 처음 생각했던 방법은 선택한 보석들 주위만 검사하도록 해서 할 생각이었는데, 굳이 그럴 필요 없이 그냥 맵 전체를 다 검사하는 편이 낫다고 생각했습니다. 그래서 생각해 낸 방법은!?

  1. 가로줄의 경우, 한 줄 안의 보석들을 for문으로 돈다.
  2. 돌면서 현재 인덱스의 보석타입과 다음 인덱스의 보석 타입을 비교한다.
  3. 같으면 변수(현재 보석과 다음 보석이 같은 수가 몇인지 나타내는)를 1증가 시킨다.
  4. 변수가 2이상 4이하를 검사.

이렇게 단순하게 생각해 보았습니다. 저런 방법으로 세로도 가능할 거 같네요. 그래서 이걸 구현 한 코드를 보면

GameControl 클래스 checkJewel.as

private function checkSameJewelsOneLine():void{
	checkSameJewelsHorizonLine();
	checkSameJewelsVerticalLine();
}
private function checkSameJewelsHorizonLine():void{
	var i:int, j:int, k:int, l:int, map:Array, check:int, sameStartIndex:int;

	map = getMap();
	for( i = 0, j = map.length ; i < j ; ++i ){
		for( k = 0, l = map[i].length - 1 ; k < l ; ++k ){
			if( isSameWithNextJewel( map[i], k ) ){
				++check;
			}else if( check < 2 ){
				check = 0;
			}
			if( isStartSame( check ) ){
				sameStartIndex = k;
			}
			if( isSameThreeMore( check ) && isLineEnd( k, l ) ){
				trace( 'bomb horizon jewel!!!' + 'index : 행 = ' + i + ', 열 = ' + sameStartIndex + ' ~ ' + ( sameStartIndex + check ) );
				check = 0;
				return;
			}else if( isLineEnd( k, l ) ){
				check = 0;
			}
		}
	}
}
private function isSameWithNextJewel( $map:Array, $index:int ):Boolean{
	return getJewelType( $map[$index] ) === getJewelType( $map[$index + 1] )
}
private function getJewelType( $target:Jewel ):int{
	return $target.getType();
}
private function isStartSame( $value:int ):Boolean{
	return $value === 1;
}
private function isSameThreeMore( $value:int ):Boolean{
	return $value >= 2;
}
private function isLineEnd( $k:int, $l:int ):Boolean{
	return $k === $l - 1;
}
private function checkSameJewelsVerticalLine():void{
	var i:int, j:int, k:int, l:int, map:Array, check:int, sameStartIndex:int;

	map = getMap();
	for( i = 0, j = map.length ; i < j ; ++i ){
		for( k = 0, l = map[i].length - 1 ; k < l ; ++k ){
			if( isSameWithDownJewel( map, k, i ) ){
				++check;
			}else if( check < 2 ){
				check = 0;
			}
			if( isStartSame( check ) ){
				sameStartIndex = k;
			}
			if( isSameThreeMore( check ) && isLineEnd( k, l ) ){
				trace( 'bomb verticla jewel!!!' + 'index : 행 = ' + i + ', 열 = ' + sameStartIndex + ' ~ ' + ( sameStartIndex + check ) );
				return;
			}else if( isLineEnd( k, l ) ){
				check = 0;
			}
		}
	}
}
private function isSameWithDownJewel( $map:Array, $indexX:int, $indexY:int ):Boolean{
	return getJewelType( $map[$indexX][$indexY] ) === getJewelType( $map[$indexX + 1][$indexY] );
}

checkSameJewelsOneLine() 메서드는 뷰의 두 개의 보석이 교체되는 움직임이 끝나면 호출되게 되어 있습니다. 그리고 메소드 내부에선 각각 가로와 세로를 검사하는 메서드를 호출 합니다. 이번에도 역시 코딩을 할 때 종종 걸음으로 기록을 하면서 했어야 하는데(…) 그러지 못해서 좀 휙~ 건너 뛴 부분이 보이네요. 설명을 하자면 원래 저 위의 생각했던 대로 구현을 했는데 계속 코드를 테스트 해 보니 문제가 많더군요. 그래서 다시 개선한 알고리즘은

  1. 현재 보석타입과 다음 보석 타입이 같으면 check변수 증가
  2. 만약 다르다면, 2보다 작으면 check변수 초기화(연속되지 않은 같은 2개의 보석 무리를 체크 하기 위해(ex:★★ㅇ★★ㅇ)
  3. 같은 보석이 시작되면 인덱스를 변수에 저장 (같은 보석들의 위치를 알기 위해)
  4. 같은 보석의 수가 3개 이상이고 한 줄이 끝난다면, 같은 보석일 때 처리. 그리고 어차피 게임 중 한 번에 움직임만 할 수 있기 때문에 이후엔 같은 보석이 올 수 없다고 판단하고 검색을 중단
  5. 같은 보석의 수가 3개 미만이고, 한 줄이 끝난다면 check 변수 초기화

여기 까지 구현하면 이제 검색한 후 결과로 같은 보석 3개 이상인 그룹을 처리하는 기능을 구현할 차례입니다. 먼저 맵 데이터를 수정 해야겠네요. 만약 가로로 3개의 보석이 검색되었다면, 그 3개의 보석의 배열 상의 인덱스에 null 할당하면 될듯합니다. 그리고 뷰도 3개의 보석을 사라지도록 해야겠습니다.

헉!?

기능이 어느 정도 잘 돌아간다 생각했는데, 역시나 우연한 스냅샷 이였습니다(…) 테스트 하고 바로 결과물과 함께 글을 올리려 했는데 알고리즘에 엄청난 오류가 있었네요. 첫 번째 오류로, 보석이 이렇게( ★★★o★★ ) 있는 경우 지금의 알고리즘에선 앞의 보석과 뒤의 보석이 같을 때 check변수를 증가 시켜 check변수의 양 만큼, 똑같은 보석이 시작된 위치에서부터 보석을 삭제해서 앞의 ★모양 보석 3개를 없애야 하는데, 뒤에 ★모양 보석 2개까지도 check 변수에 포함돼서 ‘★★★o’ 가 사라지게 됩니다. 그리고 제 짧은 생각으로 한번에 가로, 세로 각각 한번만 3줄이 돼서 삭제 하게 될 줄 알았는데 동시에 3줄 이상이 생기는 경우가 존재하더군요. 역시 관찰을 잘 해야겠습니다. 이렇게 되면 3이상을 발견하는 즉시 삭제하고 검색을 이어 나가는 알고리즘으로 바꿔야겠네요.

GameControl 클래스 checkJewel.as 수정 부분

import com.asnike.P.bejeweled.*;

private function checkSameJewelsOneLine():void{
	checkSameJewelsHorizonLine();
	checkSameJewelsVerticalLine();
}
private function checkSameJewelsHorizonLine():void{
	var i:int, j:int, k:int, l:int, map:Array, check:int, sameStartIndex:int;

	map = getMap();
	for( i = 0, j = map.length ; i < j ; ++i ){
		for( k = 0, l = map[i].length - 1 ; k < l ; ++k ){
			if( isSameWithNextJewel( map[i], k ) ){
				++check;
			}else if( check < 2 ){
				check = 0;
			}
			if( isStartSame( check ) ){
				sameStartIndex = k;
			}
			if( isSameThreeMore( check ) && isLineEnd( k, l ) || isSameThreeMore( check ) && isNotSameWithNextJewel( map[i], k + 1 ) ){
				clearSameJewelsHorizonLine( map[i], sameStartIndex, sameStartIndex + check );
				check = 0;
			}else if( isLineEnd( k, l ) ){
				check = 0;
			}
		}
	}
}
private function isSameWithNextJewel( $map:Array, $index:int ):Boolean{
	return getJewelType( $map[$index] ) === getJewelType( $map[$index + 1] )
}
private function getJewelType( $target:Jewel ):int{
	if( $target === null ){
		return GameModel.JEWEL_EMPTY;
	}
	return $target.getType();
}
private function isNotSameWithNextJewel( $map:Array, $index:int ):Boolean{
	return !isSameWithNextJewel( $map, $index );
}
private function isStartSame( $value:int ):Boolean{
	return $value === 1;
}
private function isSameThreeMore( $value:int ):Boolean{
	return $value >= 2;
}
private function isLineEnd( $current:int, $next:int ):Boolean{
	return $current === $next - 1;
}
private function clearSameJewelsHorizonLine( $map:Array, $startIndex:int, $endIndex:int ):void{
	var i:int, j:int;

	for( i = $startIndex, j = $endIndex + 1 ; i < j ; ++i ){
		removeJewel( $map[i] );
		$map[i] = null;
	}
}
private function checkSameJewelsVerticalLine():void{
	var i:int, j:int, k:int, l:int, map:Array, check:int, sameStartIndex:int;

	map = getMap();
	for( i = 0, j = map.length ; i < j ; ++i ){
		for( k = 0, l = map[i].length - 1 ; k < l ; ++k ){
			if( isSameWithDownJewel( map, k, i ) ){
				++check;
			}else if( check < 2 ){
				check = 0;
			}
			if( isStartSame( check ) ){
				sameStartIndex = k;
			}
			if( isSameThreeMore( check ) && isLineEnd( k, l ) || isSameThreeMore( check ) && isNotSameWithDownJewel( map, k + 1, i ) ){
				clearSameJewelsVerticalLine( map, i, sameStartIndex, sameStartIndex + check );
				check = 0;
			}else if( isLineEnd( k, l ) ){
				check = 0;
			}
		}
	}
}
private function isSameWithDownJewel( $map:Array, $indexX:int, $indexY:int ):Boolean{
	return getJewelType( $map[$indexX][$indexY] ) === getJewelType( $map[$indexX + 1][$indexY] );
}
private function isNotSameWithDownJewel( $map:Array, $indexX:int, $indexY:int ):Boolean{
	return !isSameWithDownJewel( $map, $indexX, $indexY );
}
private function clearSameJewelsVerticalLine( $map:Array, $col:int, $startIndex:int, $endIndex:int ):void{
	var i:int, j:int;

	for( i = $startIndex, j = $endIndex + 1 ; i < j ; ++i ){
		removeJewel( $map[i][$col] );
		$map[i][$col] = null;
	}
}
private function removeJewel( $target:Jewel ):void{
	if( $target ){
		_view.removeJewel( $target );
	}
}

같은 보석이 3개 이상일 때를 찾아 즉시 삭제하는 알고리즘으로 바꾸었습니다. 발견 즉시 가로의 경우clearSameJewelsHorizonLine() 메서드를 통해 삭제하고, 세로의 경우 clearSameJewelsVerticalLine()메서드를 통해 삭제합니다. 테스트 전 까진 매우 훌륭한 알고리즘이라 생각했습니다만(…) 테스트 후 또 약점을 발견했습니다. 순차적으로 삭제를 해버리는 바람에 가로와 세로 보석이 중첩되는 부분이 있을 때( ㄱ자의 형태로 보석이 가로 3, 세로 3 이 되는 경우 ), 가로를 먼저 검사하고 삭제하기 때문에 삭제된 후 세로를 검사할 땐 이미 보석이 없는 상황이 되어버려 삭제가 되지 않는 경우가 생기는군요. 그래서 검사를 할 때 삭제하지 않고, 검사를 마친 후 일괄적으로 삭제해주는 방법으로 알고리즘을 수정했습니다.

GameControl 클래스 checkJewel.as 2차 수정 부분

import com.asnike.P.bejeweled.*;

private function checkSameJewelsOneLine():void{
	checkSameJewelsHorizonLine();
	checkSameJewelsVerticalLine();
	removeSameJewels();
}
private function checkSameJewelsHorizonLine():void{
	var i:int, j:int, k:int, l:int, map:Array, check:int, sameStartIndex:int;

	map = getMap();
	for( i = 0, j = map.length ; i < j ; ++i ){
		for( k = 0, l = map[i].length - 1 ; k < l ; ++k ){
			if( isSameWithNextJewel( map[i], k ) ){
				++check;
			}else if( check < 2 ){
				check = 0;
			}
			if( isStartSame( check ) ){
				sameStartIndex = k;
			}
			if( isSameThreeMore( check ) && isLineEnd( k, l ) || isSameThreeMore( check ) && isNotSameWithNextJewel( map[i], k + 1 ) ){
				setWillBeRemoveSameJewelsHorizon( map[i], sameStartIndex, sameStartIndex + check );
				check = 0;
			}else if( isLineEnd( k, l ) ){
				check = 0;
			}
		}
	}
}
private function isSameWithNextJewel( $map:Array, $index:int ):Boolean{
	return getJewelType( $map[$index] ) === getJewelType( $map[$index + 1] )
}
private function getJewelType( $target:Jewel ):int{
	if( $target === null ){
		return GameModel.JEWEL_EMPTY;
	}
	return $target.getType();
}
private function isNotSameWithNextJewel( $map:Array, $index:int ):Boolean{
	return !isSameWithNextJewel( $map, $index );
}
private function isStartSame( $value:int ):Boolean{
	return $value === 1;
}
private function isSameThreeMore( $value:int ):Boolean{
	return $value >= 2;
}
private function isLineEnd( $current:int, $next:int ):Boolean{
	return $current === $next - 1;
}
private function setWillBeRemoveSameJewelsHorizon( $map:Array, $startIndex:int, $endIndex:int ):void{
	var i:int, j:int;

	for( i = $startIndex, j = $endIndex + 1 ; i < j ; ++i ){
		setWillBeRemove( $map[i] );
	}
}
private function setWillBeRemove( $target:Jewel ):void{
	if( $target ){
		$target.setWillBeRemove();
	}
}
private function checkSameJewelsVerticalLine():void{
	var i:int, j:int, k:int, l:int, map:Array, check:int, sameStartIndex:int;

	map = getMap();
	for( i = 0, j = map.length ; i < j ; ++i ){
		for( k = 0, l = map[i].length - 1 ; k < l ; ++k ){
			if( isSameWithDownJewel( map, k, i ) ){
				++check;
			}else if( check < 2 ){
				check = 0;
			}
			if( isStartSame( check ) ){
				sameStartIndex = k;
			}
			if( isSameThreeMore( check ) && isLineEnd( k, l ) || isSameThreeMore( check ) && isNotSameWithDownJewel( map, k + 1, i ) ){
				setWillBeRemoveSameJewelsVertical( map, i, sameStartIndex, sameStartIndex + check );
				check = 0;
			}else if( isLineEnd( k, l ) ){
				check = 0;
			}
		}
	}
}
private function isSameWithDownJewel( $map:Array, $indexX:int, $indexY:int ):Boolean{
	return getJewelType( $map[$indexX][$indexY] ) === getJewelType( $map[$indexX + 1][$indexY] );
}
private function isNotSameWithDownJewel( $map:Array, $indexX:int, $indexY:int ):Boolean{
	return !isSameWithDownJewel( $map, $indexX, $indexY );
}
private function setWillBeRemoveSameJewelsVertical( $map:Array, $col:int, $startIndex:int, $endIndex:int ):void{
	var i:int, j:int;

	for( i = $startIndex, j = $endIndex + 1 ; i < j ; ++i ){
		setWillBeRemove( $map[i][$col] );
	}
}
private function removeSameJewels():void{
	var i:int, j:int, k:int, l:int, map:Array;

	map = getMap();
	for( i = 0, j = map.length ; i < j ; ++i ){
		for( k = 0, l = map[i].length ; k < l ; ++k ){
			if( willBeRemove( map[i][k] ) ){
				removeJewel( map[i][k] );
				map[i][k] = null;
			}
		}
	}
}
private function willBeRemove( $target:Jewel ):Boolean{
	if( $target ){
		return $target.getWillBeRemove();
	}
	return false;
}
private function removeJewel( $target:Jewel ):void{
	if( $target ){
		_view.removeJewel( $target );
	}
}

Jewel 클래스에 _willBeRemove 라는 Boolean 타입의 속성을 만들어 주고, 같은 종류의 보석이 3개 이상인 그룹의 보석들은 저 속성을 true 로 만들어 주어 보석 검사 메서드( checkSameJewelsHorizonLine(), checkSameJewelsVerticalLine() )가 실행 된 후 실행되는 보석 삭제 메서드( removeSameJewels() )에서 저 속성이 true인 보석만 삭제하도록 변경하였습니다. 드디어 보석 체크, 삭제 기능이 어느 정도 안정되게(?) 구현되었습니다. 메서드의 이름들도 최대한 맥락을 이해할 수 있게 서술적으로 수정했는데(…) 역시나 이름 짓는 일은 참 힘드네요. 글에 소스가 많다 보니 스크롤이 엄청 나게 길어지네요. 오늘은 여기까지 올리겠습니다!

결과물

미니 게임 만들기 : 첫 번째

2 Comments

!?

다분히 개인적인 용도로 블로그를 운영하는데, 점 점 글도 안 쓰게 되는 것도 그렇고(…), 요즘 한창 여러 가지 책들을 읽는 중에 ‘실용 주의 사고와 학습’, ‘프로그래머의 길, 멘토에게 묻다‘를 보고 느낀 점이 많아, 아무 뻘 글이라도 올려야겠다는 생각에서 시작을 하게 되네요. 책 내용 중에 ‘무지를 드러내라’라는 부분에 크게 감명받아서 무지를 드러내보려( 가만히 있어도 드러나겠지만; ) 이렇게 글을 씁니다. 뭐 별로 보실 분들이 없을 수 도 있겠지만 어쨌든 넷 상이니 혼자 담고 있는 것보다야 낫지 않겠어요? ㅎㅎ 혼자 개발하면서 생각했던 내용들, 리팩토링 과정, 실수, 열폭(?) 등등을 편하게 써 나갈 예정입니다. 하지만 첫 번째 글에서는 이미 진행된 부분이 꽤 돼서 리팩토링 과정이나 실수 등이 잘 기억이 안나 적을 수가 없네요; 아마 두 번째 글부터는 이 부분을 고려하면서 개발하고 글로 남기도록 할 예정입니다.

잡설은 이 정도로 하고, 본론으로 들어가면 무지를 들어내기 쉬운 게 뭐가 있을까 생각해보니 역시 직접 짠 코드를 올리고 피드백을 받는 게 가장 좋다고 판단이 되었습니다. 안 그래도 요즘 혼자서 간단한 미니 게임 정도는 만들어 보려 생각 중이었는데, 이걸 활용하면 되겠다 싶더군요. 그래서 처음 만들어 보려는 게임은 Bejeweled 같은 종류의 게임을 만들어 보려 합니다. 그리고 게임을 완성시키는 것도 목적이지만, TDD와 Clean Code 등을 연습하려는 목적도 함께 있기 때문에 매우 종종걸음으로 진행이 될 것입니다. TestFramework로는 asunit을 사용하여 테스트를 할 것입니다.

Bejeweled

현재 만들어진 클래스는 Main, GameControl, GameView, GameModel, Jewel 이렇게 5개의 클래스가 존재합니다. GameControl, GameView, GameModel클래스는 이름을 보시면 아시겠지만 MVC로 구성하여 Main클래스에서 사용되어 집니다. Jewel클래스는 보석을 나타내는 클래스로 크기가 그리 커질 것 같지 않아 Jewel클래스 하나에 MVC를 모두 구현할 것입니다.

Main 클래스

package{

	import asunit.textui.TestRunner;

	import com.asnike.P.bejeweled.*;
	import com.asnike.net.*;
	import com.asnike.util.*;

	import flash.display.*;

	import tests.*;

	public class Main extends Sprite{

		static public var resources:Array = [];
		public function Main():void{
			loadResources();
		}
		private function loadResources():void{
			var i:int, j:int, xml:XML;

			ASLoader.loadImage( 'remote/jewels.png', function( $data:Array ):void{
				xml =

					
					
					
					
					
					
				;

				resources = ASImageCropper.getImages( $data[0], xml );
//				test();
				testGameView();
			} );
		}
		private function test():void{
			var testRunner:TestRunner;

			testRunner = new TestRunner;
			addChild( testRunner );
			testRunner.start( AllTests, null, TestRunner.SHOW_TRACE );

		}
		private function testGameView():void{
			var control:GameControl, model:GameModel, view:GameView;

			model = new GameModel;
			view = new GameView;
			control = new GameControl( model, view );
			control.setJewelSize( 6, 6 );
			control.gameReady();

			addChild( view );
		}
	}
}

Main 클래스는 호스트 클래스로 리소스를 전달하는 역할 외엔 아직 특별한 역할이 없습니다.

GameView 클래스

package com.asnike.P.bejeweled{
	import com.greensock.TweenMax;
	import com.greensock.easing.*;

	import flash.display.*;
	import flash.events.*;
	import flash.filters.*;

	public class GameView extends Sprite{

		static private var SELECT_FILTER:GlowFilter = new GlowFilter( 0x87f1ff, .9, 3, 3, 8, 3 );
		static private var SHIFT_JEWELS_DURATION:Number = .4;

		public function GameView(){
			super();
		}
		private function init():void{
		}

		internal function addJewel( $type:int, $name:String, $x:Number, $y:Number, $clickFunction:Function = null ):Jewel{
			var jewel:Jewel;

			jewel = Jewel.GET( $type, $name );
			jewel.x = $x;
			jewel.y = $y;
			jewel.buttonMode = true;
			if( $clickFunction is Function ){
				jewel.addEventListener( MouseEvent.CLICK, $clickFunction );
			}
			addChild( jewel );

			return jewel;
		}

		internal function selectJewel( $target:Jewel ):void{
			addSelectEffect( $target );
		}
		private function addSelectEffect( $target:Jewel ):void{
			$target.filters = [SELECT_FILTER];
		}

		internal function unselectJewel( $target:Jewel ):void{
			removeSelectEffect( $target );
		}
		private function removeSelectEffect( $target:Jewel ):void{
			$target.filters = [];
		}

		internal function shiftJewels( $target1:Jewel, $target2:Jewel ):void{
			TweenMax.to( $target1, SHIFT_JEWELS_DURATION, {ease:Circ.easeOut, x:$target2.x, y:$target2.y, onComplete:unselectJewel, onCompleteParams:[$target1]} );
			TweenMax.to( $target2, SHIFT_JEWELS_DURATION, {ease:Circ.easeOut, x:$target1.x, y:$target1.y, onComplete:unselectJewel, onCompleteParams:[$target2]} );
		}
	}
}

GameView 클래스에선 시각적인 효과나 움직임을 줄 수 있는 메서드를 구현해놓고 이를 GameControl에서 사용할 수 있게 합니다. GameControl클래스에서 사용할 수 있는 메서드들은 GameControl 클래스와 GameView 클래스가 같은 패키지 내에 있기 때문에 패키지 가시성인 internal로 선언합니다. 지금 구현되어 있는 기능은 선택된 보석에 표시하고, 선택된 두 보석을 움직여주는 기능까지 구현되었습니다.

GameModel 클래스

package com.asnike.P.bejeweled{
	public class GameModel{

		static private var TOTAL_JEWEL_TYPE:int = 6;

		private var _jewelMap:Array = [];
		private var _jewelSize:Array = [];
		private var _selectedJewels:Array = [];

		public function GameModel(){

		}

		internal function getJewelMap():Array{
			return _jewelMap;
		}

		internal function setJewelSize( $width:int, $height:int ):void{
			var i:int, j:int, result:Array;

			_jewelSize[0] = $width;
			_jewelSize[1] = $height;

			_jewelMap = [];
			for( j = 0 ; j < _jewelSize[1] ; ++j ){
				_jewelMap[j] = [];
				for( i = 0 ; i < _jewelSize[0] ; ++i ){
					_jewelMap[j][i] = getJewelType();
				}
			}
		}
		private function getJewelType():int{
			return int( Math.random()*TOTAL_JEWEL_TYPE );
		}

		internal function getSelectedJewels():Array{
			return _selectedJewels;
		}

		internal function getJewelSize():Array{
			return _jewelSize;
		}

		internal function getMapLength():int{
			return _jewelMap.length*_jewelMap[0].length;
		}
	}
}

GameModel 클래스에서는 보석의 위치를 저장할 변수와 현재 선택된 보석을 저장하는 변수를 가지고 있습니다. 모델에서 맵을 보석의 타입(현재 보석의 종류를 0~5까지로 만들었습니다.)만을 기록하여 만듭니다. 그럼 배열에 [1,3,4,5] 뭐 이런식으로 들어가게 되는데 컨트롤에서 다시 이 타입을 Jewel 클래스의 인스턴스를 만들면서 타입을 저장하고 그 Jewel클래스의 인스턴스를 맵에 저장하는 식으로 되어있습니다. 그럼 맵 배열안엔 처음 모델에서 생성했던 타입 대신, Jewel클래스의 인스턴스 들이 들어게있게 되겠죠. 사실 이렇게 해야만 했던 이유는 클릭을 했을 때 클릭한 보석이 배열에 어떤 인덱스에 위치되어있는지 알 수가 없어서 입니다. 처음엔 name 속성에 위치를 나타내는 메타데이터를 넣어볼까 했지만, 보석이 제거되고 다시 보석을 채울 때 이미 존재하는 보석들과 새로 추가된 보석들을 구분할 방법과 다시 다 name 속성에 메타데이터를 할당해야 하는 일이 그닥 올바른 접근법이 아닌것 같아서 맵 배열에 타입이 아닌 Jewel 클래스 인스턴스를 할당했습니다. 그런데 이 방법도 뭔가 살짝 꺼림찍하네요. ㅎㅎ;

GameControl 클래스

package com.asnike.P.bejeweled{
	import flash.events.MouseEvent;

	public class GameControl{

		include 'control/selectJewel.as';
		include 'control/controlTest.as';

		private var _model:GameModel;
		private var _view:GameView;

		public function GameControl( $model:GameModel, $view:GameView ){
			if( $model === null ){
				throw new TypeError( '$model은 null이 올 수 없습니다.' );
			}
			if( $view === null ){
				throw new TypeError( '$view은 null이 올 수 없습니다.' );
			}

			_model = $model;
			_view = $view;
		}
		public function setJewelSize( $width:int, $height:int ):void{
			if( $width < 0 || $height < 0 ){
				throw new RangeError( '$width, $height 파라미터엔 음수가 올 수 없습니다.' );
			}

			_model.setJewelSize( $width, $height );
		}

		public function gameReady():void{
			setJewels();
		}
		private function setJewels():void{
			var i:int, j:int, k:int, l:int, map:Array, jewel:Jewel;

			map = _model.getJewelMap();
			for( i = 0, j = map.length ; i < j ; ++i ){
				for( k = 0, l = map[i].length ; k < l ; ++k ){
					jewel = getJewel( map[i][k], Jewel.NAME_PREFIX + i + k, Jewel.WIDTH*k, Jewel.HEIGHT*i, JEWEL_CLICK );
					map[i][k] = jewel;
				}
			}
		}
		private function getJewel( $type:int, $name:String, $x:Number, $y:Number, $clickFunction:Function ):Jewel{
			return _view.addJewel( $type, $name, $x, $y, $clickFunction );
		}
	}
}

GameControl 클래스는 실제 게임을 진행하는 알고리즘들이 있는 중요한 클래스라고 할 수 있습니다. 현재는 보석을 2개 선택하는것과 하나의 보석을 선택했을 때 다음 선택한 보석이 적합한지 판단하여 선택하는 기능까지 구현되어있습니다. 이 선택하는 부분이 예상보다 양이 꽤 되어서 줄이 길어지는 바람에 따로 as파일로 분리해서 include해서 관리하였습니다. 그 외에 뷰와 모델을 참조하는 부분과 모델로 부터 맵 배열을 받아와 Jewel 클래스 인스턴스를 할당하는 부분이 있습니다.  그리고 테스트를 위한 함수를 모아 놓은 controlTest.as 파일도 include되어있습니다.

GameControl 클래스 selectJewel.as

private function JEWEL_CLICK( $e:MouseEvent ):void{
	setSelectJewel( $e.target as Jewel );
}
private function setSelectJewel( $target:Jewel ):void{
	if( isSelectedNone() ){
		addJewelFromSelectedJewels( $target );
	}else if( isJewelAlreadySelected( $target ) ){
		removeJewelFromSelectedJewels( $target );
	}else if( isEmptySelectedJewel() && isNearJewel( $target ) ){
		addJewelFromSelectedJewels( $target );
		shiftSelectedJewels();
		removeAllFromSelectedJewels();
	}
}
private function isSelectedNone():Boolean{
	return getSelectedJewels().length === 0;
}
private function isJewelAlreadySelected( $target:Jewel ):Boolean{
	return getSelectedJewels().indexOf( $target ) > -1
}
private function removeJewelFromSelectedJewels( $target:Jewel ):void{
	getSelectedJewels().splice( getSelectedJewels().indexOf( $target ), 1 );
	_view.unselectJewel( $target );
}
private function isEmptySelectedJewel():Boolean{
	return getSelectedJewels().length < 2;
}
private function isNearJewel( $target:Jewel ):Boolean{
	if( isEqualJewelIndexX( $target ) && isInvalidInRangeJewelIndexY( $target ) ){
		return true;
	}else if( isEqualJewelIndexY( $target ) && isInvalidInRangeJewelIndexX( $target ) ){
		return true;
	}else{
		return false;
	}
	return false;
}
private function isEqualJewelIndexX( $target:Jewel ):Boolean{
	return getJewelIndexX( getSelectedJewels()[0] ) === getJewelIndexX( $target );
}
private function isInvalidInRangeJewelIndexY( $target:Jewel ):Boolean{
	return ( getJewelIndexY( getSelectedJewels()[0] ) + 1 === getJewelIndexY( $target ) || getJewelIndexY( getSelectedJewels()[0] ) - 1 === getJewelIndexY( $target ) );
}
private function isEqualJewelIndexY( $target:Jewel ):Boolean{
	return getJewelIndexY( getSelectedJewels()[0] ) === getJewelIndexY( $target );
}
private function isInvalidInRangeJewelIndexX( $target:Jewel ):Boolean{
	return ( getJewelIndexX( getSelectedJewels()[0] ) + 1 === getJewelIndexX( $target ) || getJewelIndexX( getSelectedJewels()[0] ) - 1 === getJewelIndexX( $target ) );
}
private function getJewelIndexX( $target:Jewel ):int{
	return getJewelIndexInMap( $target )[0];
}
private function getJewelIndexY( $target:Jewel ):int{
	return getJewelIndexInMap( $target )[1];
}
private function getJewelIndexInMap( $target:Jewel ):Array{
	var i:int, j:int, k:int, l:int, map:Array, result:Array;

	result = [];
	map = _model.getJewelMap();
	for( i = 0, j = map.length ; i < j ; ++i ){ 		if( isFindingJewel( map[i], $target ) ){ 			result[result.length] = i; 			result[result.length] = map[i].indexOf( $target ); 			return result; 		}; 	} 	 	result = [-1, -1]; 	return result; } private function isFindingJewel( $map:Array, $target:Jewel ):Boolean{ 	return $map.indexOf( $target ) > -1;
}
private function addJewelFromSelectedJewels( $target:Jewel ):void{
	getSelectedJewels()[getSelectedJewels().length] = $target;
	_view.selectJewel( $target );
}
private function shiftSelectedJewels():void{
	_view.shiftJewels( getSelectedJewels()[0], getSelectedJewels()[1] );
}
private function removeAllFromSelectedJewels():void{
	var selected:Array;
	selected = getSelectedJewels();
	removeJewelFromSelectedJewels( selected[0] );
	removeJewelFromSelectedJewels( selected[0] );
}

private function getSelectedJewels():Array{
	return _model.getSelectedJewels()
}

기능을 구현하면서 최대한 Clean Code를 만들어 내기위해 노력을 했습니다(…) 서술형으로 읽기 쉽게 탑다운 형태로 메서드를 만들어 나갔는데 꽤 기네요(…) 대략 설명하자면

  1. 보석 선택
  2. 적합한가?(현재 선택되어있는 보석이 없는 상태인가?) 아니면 담지 않는다.
  3. 선택된 보석 배열에 담는다.
  4. 보석 선택
  5. 적합한가?(2번째로 선택한 보석이 중복인가?, 아님 근처[상-하-좌-우]에있는 보석이 아닌가?) 아니면 담지 않는다.
  6. 선택된 보석 배열에 담는다.
  7. 선택된 보석 둘의 위치를 바꾼다.
  8. 선택된 보석 배열을 비운다.

정도가 되겠습니다.

GameControl 클래스 controlTest.as

public function getMapLengthAtTest():int{
	return _model.getMapLength();
}
public function getJewelSizeAtTest():Array{
	return _model.getJewelSize();
}
public function getViewAtTest():GameView{
	return _view;
}
public function getJewelAtTest( $horizonIndex:int, $verticalIndex:int ):Jewel{
	return _model.getJewelMap()[$horizonIndex][$verticalIndex];
}
public function setSelectJewelAtTest( $target:Jewel ):void{
	setSelectJewel( $target );
}
public function getSelectJewelsAtTest():Array{
	return getSelectedJewels();
}
public function isNearJewelAtTest( $target:Jewel ):Boolean{
	return isNearJewel( $target );
}

이 as파일은 뭐 그냥 테스트를 위한 함수를 모아 놓은 거고 크게 설명할건 없네요.

Jewel 클래스

package com.asnike.P.bejeweled{
	import flash.display.Bitmap;
	import flash.display.Sprite;

	public class Jewel extends Sprite{

		static internal var NAME_PREFIX:String = 'jewel';
		static internal var WIDTH:Number = 35;
		static internal var HEIGHT:Number = 35;

		static internal function GET( $type:int, $name:String ):Jewel{
			var jewel:Jewel;

			jewel = new Jewel;
			jewel.init( $type, $name );

			return jewel;
		}

		private var names:Vector. = new Vector.();
		private var _type:int;
		private var _horizonNum:int;
		private var _verticalNum:int;

		public function Jewel(){
			super();

		}
		private function init( $type:int, $name:String ):void{
			setType( $type );
			setImage();
			setName( $name );
		}
		private function setType( $value:int ):void{
			if( isInvalidType( $value ) ){
				throw new RangeError( '$value의 범위는 0~5사이여야 합니다.' );
			}

			_type = $value;
		}
		private function isInvalidType( $value:int ):Boolean{
			return ( $value < 0 || $value > 5 );
		}
		private function setImage():void{
			addChild( new Bitmap( Main.resources[_type] ) );
		}
		private function setName( $name:String ):void{
			if( isInValidName( $name ) ){
				throw new TypeError( '$name매개 변수는 빈 문자열이 올 수 없습니다.' );
			}
			if( isOverLapName( $name ) ){
				throw new TypeError( '이 이름은 이미 사용된 이름 입니다. 중복될 수 없습니다.' );
			}
			names[names.length] = $name;
			name = $name;
		}
		private function isInValidName( $name:String ):Boolean{
			return ( $name === '' );
		}
		private function isOverLapName( $name:String ):Boolean{
			return ( names.indexOf( $name ) > -1 );
		}

		internal function getType():int{
			return _type;
		}
		internal function getHorizonNum():int{
			return _horizonNum;
		}
		internal function getVerticalNum():int{
			return _verticalNum;
		}
	}
}

Jewel 클래스는 보석의 뷰, 모델, 컨트롤을 다 한 클래스에 가지고 있습니다. 등치가 크지 않아서 하나로 만들어도 되겠다고 생각했습니다. 그런데 지금 포스팅을 하면서 코드를 리뷰해보니 쓸 데 없는 변수가 있네요. names 라는 Vector형 변수인데 원래 GameControl에서 이름 기반으로 Jewel 클래스의 인스턴스를 제어 할려고 했던 맥락일 때 Jewel클래스를 제작하여서 저렇게 된 듯 합니다. 이게 제 포스팅의 목적이기도 합니다. 포스팅을 하기 위해 코드를 다시 꼼꼼하게(원래 부터 꼼꼼하게 봐야하지만!) 하다가 이렇게 쓸 데 없는 변수도 찾고, 리팩터링도 하고(…) 아무래도 많은 사람들에게 보여지게 된다면 더욱 신경쓰게 될테니까요 ㅎㅎ; 아무튼 저 names 변수는 삭제해야겠네요. 그와 관련된 코드도 전부 리팩터링해야하구요. _horizonNum, _verticalNum 변수도 필요가 없어지네요. 맵 배열에 Jewel 클래스 인스턴스가 있기때문에 필요가 없어졌습니다. 보석의 위치가 필요할 때 Jewel 클래스 인스턴스를 검색해서 해당 인덱스를 알아내면 되기 때문이죠.

TestGameControl 클래스

package tests{
	import asunit.errors.AssertionFailedError;
	import asunit.framework.TestCase;

	import com.asnike.P.bejeweled.*;

	import flash.events.*;

	public class TestGameControl extends TestCase{
		public function TestGameControl(testMethod:String=null){
			super(testMethod);
		}

		private var _gameControl:GameControl;
		private var _gameModel:GameModel;
		private var _gameView:GameView;

		private var _testMap:Array = [[2,2,2,1,2,3],
			[0,2,3,5,4,1],
			[3,3,1,3,5,1],
			[0,2,1,1,3,2],
			[1,4,4,5,2,1],
			[2,3,4,5,4,2]];

		override protected function setUp():void{
			_gameModel = new GameModel;
			_gameView = new GameView;
			_gameControl = new GameControl( _gameModel, _gameView );
			_gameControl.setJewelSize( 6, 6 );
			_gameControl.gameReady();
		}

		public function testSetView():void{

			assertEquals( _gameView, _gameControl.getViewAtTest() );
			try{
				_gameControl = new GameControl( _gameModel, null );
			}catch( $e:TypeError ){
				assertTrue( true );
			}
		}

		public function testSetJewelSize():void{
			assertEqualsArrays( [6, 6], _gameControl.getJewelSizeAtTest() );

			try{
				_gameControl.setJewelSize( -1, 0 );
				fail( 'error!!!' );
			}catch( $e:RangeError ){
				assertTrue( true );
			}
		}
		public function testGenerateMap():void{
			assertEquals( 36, _gameControl.getMapLengthAtTest() );
		}

		public function testSelectJewel():void{
			var jewel0:Jewel, jewel1:Jewel;

			jewel0 = _gameControl.getJewelAtTest( 1, 1 );
			_gameControl.setSelectJewelAtTest( jewel0 );
			assertEquals( jewel0, _gameControl.getSelectJewelsAtTest()[0] );
		}

		public function testSelectedJewelNearJewel():void{
			var jewel0:Jewel, jewel1:Jewel;

			jewel0 = _gameControl.getJewelAtTest( 2, 2 );
			jewel1 = _gameControl.getJewelAtTest( 2, 3 );
			_gameControl.setSelectJewelAtTest( jewel0 );
			assertTrue( _gameControl.isNearJewelAtTest( jewel1 ) );

			jewel1 = _gameControl.getJewelAtTest( 2, 3 );
		}
	}
}

테스트 케이스를 수행하기 위한 클래스입니다. 아직 TDD에 익숙하지 않아 테스트가 적절한지를 모르겠습니다. 단순한 get/set 메서드를 검사하는 데에 의미가 있을지 늘 생각하지만, 어떻게 테스트를 해야할 지 아직도 감이 잘 안오네요.

마무리

아무래도 처음 부터 진행하면서 쓴 글이아니라 한 번에 많은 내용을 글로 옮기려 했더니 무리가 있군요; 글이 엄청 길고, 내용은 적은(…)  다음 글 부터는 최대한 짧은 단위로 글을 쓰는게 좋을 것 같습니다. 내용을 보시고 제가 이해하고있는게 틀렸다거나 이상한 부분이 있으면 댓글로 지적 바랍니다. 제가 글을 쓰는 주 목적이 피드백이기때문에 자기 자신의 피드백만으론 부족한것 같아서 말이죠 ㅎㅎ;

결과물

Terminal을 이용해 환경 변수 설정하기

No Comments

Terminal요즘 AIR로 안드로이드 어플을 만들어보려고 개발환경을 셋팅 중에 윈도우에선 sdk설치 후 환경 변수 설정을 무리 없이 했는데, osx에 대한 이해가 떨어져서 환경 변수를 어떻게 설정해야 하는지 몰라 구글링을 했습니다. 검색 결과, Terminal을 이용해서 환경 변수를 설정 할 수 있더군요. 안 그래도 요즘 Terminal을 잘 사용하기 위해 이것저것 해보던 저로써는 ‘도전~곧 돌아오겠음‘을 외치며 환경 변수 설정에 돌입했습니다. 하지만 Terminal에 대한 이해가 없는 상태라 매우 힘들더군요. 한참을 삽질 한 후 겨우 환경 변수 설정을 마쳤습니다. Terminal을 통해 vi로 파일을 만들어 실행해 설정하는 방법인데요. 처음에 vi가 뭔질 몰라 매우 당황스러웠습니다. 대체 어떻게 써야 하는 건지; 하지만 검색을 해보니 vi는 유닉스계열이라면 반드시 깔려있는 텍스트 에디터였습니다! 그럼 이제 환경 변수를 설정하는 과정을 보겠습니다.

 
  
 

먼저 터미널을 열어야겠죠~

Terminal

다음으로 vi를 이용해 .bash_profile파일을 생성합니다.

.bash_profile.bash_profile 파일이 존재하면 편집하겠냐고 묻고 없다면 바로 vi화면이 나타날 겁니다.

여기서 제가 한참 헤맸습니다(….) vi에 대해 무지한 저로써는 어찌 손을 대야 할지 모르겠더군요. vi에선 여러 가지 명령들을 간단하게 키보드 자판 하나로 대체하더군요. 열심히 구글링을 하다가 알아냈습니다. 환경 변수를 설정하는 여러 가지 글들을 봤지만 Terminal, vi에 무지한 사람들을 위한 글은 찾기가 힘들더군요 ㅜㅜ 그래서 이렇게 포스팅으로 남겨놓으렵니다. ㅎㅎ 어쨌든 i를 누르면 편집을 시작할 수 있습니다. ( vi명령어를 살펴보니 i말고도 더 있군요. ) 파일에 들어갈 내용을 적을 수 있죠. 파일 내용을 적은 후 다시 명령어 모드로 돌아오려면 esc를 누르면 편집을 종료하고 명령어를 입력할 수 있습니다. 저장은 :w 을 입력한 후 엔터를 치면 되고, vi를 종료하려면 :q를 입력하고 엔터를 치면 됩니다. 이외의 vi명령어는 구글링을 통해 쉽게 찾을 수 있습니다. 그럼 이제 파일에 내용을 적어보겠습니다.

안드로이드 sdk의 경로를 환경 변수로 설정하였습니다. ANDROID_SDK라는 변수에 안드로이드 sdk가 존재하는 경로를 적어주고, PATH변수에 하위 폴더인 tools, platform-tools경로를 할당하였습니다. export는 Terminal의 명령어로 환경 변수를 설정하는 명령어 입니다.

edit .bash_profile이렇게 작성한 후 저장을 하고 vi를 종료합니다. :wq명령어는 저장을 하고 종료하는 명령어입니다.

엔터를 치면, vi를 종료하게 됩니다.

exit vi이제 작성된 파일을 실행해야 합니다. Terminal에서 파일로부터 명령을 실행할 수 있도록 해주는 명령어는 source 입니다. 아래와 같이 적어주고 엔터를 치면 끝!

source하지만 생각해보니 꼭 파일을 만들어 실행시키지 않아도 그냥 Terminal명령만으로도 환경 변수를 설정할 수 있겠네요(….) 제가 아무것도 모르는 상태에서 검색에 의존해 하다 보니 이런 방법을 사용했습니다만, 그래도 Terminal, vi에 대해 공부가 되어서 좋은 것 같습니다 ㅎㅎ 앞으로도 Terminal, vi와 더욱 친해져야겠어요. 포스팅을 보시고 이상한 점이나 보충 설명을 적어주시면 감사하겠습니다. ^^

Data Driven Programming 소개

7 Comments

Data Driven Programming( 데이터 주도 프로그래밍 )을 간단하게 설명하자면 데이터가 주가 되도록 알고리즘을 만들어서, 데이터에 따라 다르게 작동하도록 하는 것 입니다. 요즘 제가 진행하는 프로젝트에서는 클라이언트 쪽의 감수가 매우 까다롭기 때문에 그 감수를 손쉽게 처리하기 위해선 Data Driven Programming으로 개발을 해야 합니다. 그럼 간단한 예제를 보면서 설명을 해 보겠습니다. 지역과 금액을 입력하면 지역별 세율에 따른 세금을 반환하는 함수입니다.

private function returnTax( $usState:String , $price:Number ):Number{
	var result:Number
	result = 0;

	switch ( $usState ){
		case 'MA':
			result = 0.05*$price;
			break;
		case 'NJ':
			result = 0.07*$price;
			break;
	}
	return result;
}

위의 코드가 일반적인 코드라면, Data Driven Programming을 하게 되면 다음과 같습니다.

private var _stateTax:Object; // 서버로 부터 얻어온 새금율 데이터
private function returnTax( $usState:String, $price:Number ):Number{
	var result:Number, key:*;

	result = 0;
	for( key in _stateTax ){
		if( key === $usState ){
			result = _stateTax[key]*$price;
		}
	}

	return result;
}

서버로부터 세율을 얻어와 알고리즘에 사용하고 있어서 서버의 데이터가 변동 되면 컴파일을 하지 않아도 됩니다. 반면 첫 번째 코드는 새로 지역이 추가 되거나 세율이 수정될 때 마다 컴파일을 해야 합니다. 코드를 통해 비교한 것을 보시면 금방 이해가 가실 겁니다.

그럼 끝으로 제가 프로젝트를 진행하면서 느낀 Data Driven Programming의 특징을 정리해보겠습니다. 먼저 데이터의 변동에 따라 결과물(swf)이 달라지기 때문에 개발 후 유지/보수가 빈번한 프로젝트에 매우 유용할 것 같습니다. 하지만 데이터를 만들어 내는 서버 사이드 쪽 알고리즘은 상당히 복잡해지겠지요. 그리고 데이터에 대한 프로토콜을 매우 잘 정해야 합니다. 그리고 개발을 하면서 항상 Data Driven Programming방식으로 개발중인 것을 인지해야 합니다. 저 같은 경우 막 코딩의 습관이 아직 남아 있어서 멍 때리는 순간 코드가 오염이 되더군요-_-; 그렇지만 매우 유용한 개발 방법임에는 틀림 없는 것 같습니다. ^^ 계속 프로젝트를 진행해 가면서 새로운 점을 배우면 또 포스팅을 하겠습니다.

올바른 클래스는 어떻게 만들어 질까?

8 Comments

역할 모델이란 말을 많이 들어 보셨을 겁니다. 일반적인 뜻으로는 다른 사람의 본보기가 되는 사람, 닮고 싶은 사람 정도가 되겠지요. 사람의 인생에 있어서 역할 모델이 중요하듯 개발 할 때 있어서도 이 역할 모델이 매우 중요합니다. 클래스를 만들 때 이 클래스의 역할 모델이 무엇인지 명확하게 알고 만들어야 클래스가 올바르게 만들어 집니다. 근데 이렇게 만들기가 참 힘든 게 사실입니다.

요즘 회사에서 큰 어플리케이션을 제작하고 있습니다. 큰 어플리케이션인 만큼 구조도 매우 복잡하고 저에겐 모든 것들이 어려운 일들 뿐입니다. 그런데 그 가운데 제일 이해하기 힘든 게 이 역할 모델인데, 저 같은 경우 아직 초급 개발자라 개발을 할 때, 습관적으로 알고리즘이나 구현에만 집중을 하게 됩니다( 안 하려고 해도 아직은 무의식적으로 그렇게 되더군요[...] ). 이렇게 알고리즘과 구현에만 집중한 결과, 만들어진 클래스들은 각각 어떤 일을 하는지도 명확하지 않고, 각 클래스마다 중복되는 알고리즘도 많아지고, 점점 개발할수록 악의 구렁텅이 속으로 빠지게 되지요. 다 제 경험입니다.(-_ㅜ) 하지만 이 악의 구렁텅이에서 쉽게 빠져 나오기가 힘듭니다. 항상 프로젝트를 시작할 땐 “아 이번엔 클래스나 함수들을 아름답게 만들어야지!”하고 다짐을 하지만 언제나 거의 비슷한 수준의 코드를 만들어 냈습니다. 아무튼 오늘도 그런 악순환을 반복하고 있었는데 히카형께서 제가 어려워하는 모습을 보시고, 메모장을 꺼내라고 하셨습니다. 그리고 메모장에 코드를 적기 시작했습니다. 제가 만들어야 하는 클래스는 CSmodal이라는 클래스로 역할이 alert창을 보여주고 사용자의 반응에 따라 다음 행동을 하도록 하는 클래스입니다. 제가 혼자 클래스를 만들 때는 구현에만 집착해서 “전역적으로 사용하는 alert창을 어떻게 그릴까?”에 대해서만 고민을 하면서 시간을 잡아먹고 코드는 산으로 가고 있었습니다. 아무튼 히카형 말씀대로 메모장에 먼저 생각해내기 쉬운 함수를 적었습니다.

static public function alert():void

전역적으로 사용해야 하기 때문에 static public을 사용했습니다. 다음은 이 함수가 하는 일을 생각해봤습니다. 먼저 alert창을 띄워야 하겠죠. 그래서 코드는

static public function alert():void{
	// alert창을 띄운다.
	alertWindow.visible = true;
}

이 정도가 되겠죠. 그리고 alert창이 띄워졌을 때 나머지 화면에서는 마우스 이벤트를 받으면 안되기 때문에 마우스 이벤트를 막아줄 창이 필요합니다. 그 창을 modalWindow라고 하면

static public function alert():void{
	// alert창을 띄운다.
	alertWindow.visible = true;
	// modal창을 띄운다.
	modalWindow.visible = true;
}

이렇게 되겠죠. 다음으로 alert창에서 표시 해야 할 텍스트를 넣어줘야 합니다.

static public function alert():void{
	// alert창을 띄운다.
	alertWindow.visible = true;
	// modal창을 띄운다.
	modalWindow.visible = true;
	// 텍스트를 넣어준다.
	alertContents.text = $contents;
}

이제 함수가 거의 다 만들어졌는데요. 아직 한가지가 남았습니다. alert창에서 확인버튼을 누르면 다음 동작으로 이어져야 합니다. 그래서 그 함수를 설정하는 부분이 남았습니다.

static public function alert():void{
	// alert창을 띄운다.
	alertWindow.visible = true;
	// modal창을 띄운다.
	modalWindow.visible = true;
	// 텍스트를 넣어준다.
	alertContents.text = $contents;
	// 확인 버튼을 누르면 작동할 함수를 설정한다.
	alertOK = $OK;
}

자 이 함수내부에서 하는 일들을 모두 기술했습니다. 이렇게 되면 이 함수가 필요로 하는 변수들이 무엇인지 알 수 있게 됩니다.  alertWindow, modalWindow, alertContents, alertOK 변수들은 이 프로젝트 내에서 계속적으로 사용되기 때문에 CSmodal클래스의 정적 멤버 변수로 잡아줍니다.

public final class CSmodal {
	static private var alertWindow:Sprite;
	static private var modalWindow:Sprite;
	static private var alertOK:Function;
	static private var alertContents:TextField;

그리고 alert() 함수의 코드를 보면 alertContents와 alertOK에 할당해주는 변수가 있습니다. 이 변수들은 alert() 함수가 실행 될 때 마다 달라질 경우가 많기 때문에 ( 매번 다른 텍스트내용을 보낼 수도 있고, 다음 동작을 변경 할 수 있어서 ) 인자로 받아오는 형태를 가지게 됩니다. 그러면 alert 함수의 시그니쳐가 다음과 같이 명확해 집니다.

static public function alert( $contens:String, $OK:Function ):void{
	// alert창을 띄운다.
	alertWindow.visible = true;
	// modal창을 띄운다.
	modalWindow.visible = true;
	// 텍스트를 넣어준다.
	alertContents.text = $contents;
	// 확인 버튼을 누르면 작동할 함수를 설정한다.
	alertOK = $OK;
}

이제 alert() 함수가 다 만들어졌습니다. 그렇다면 지금 저 상태로 제대로 작동이 될까요? 당연히 안되겠죠. alertWindow, modalWindow, alertContents 변수들은 선언만하고 생성이나 할당을 하지 않았기 때문에 제대로 작동하지 않겠죠. 그럼 이 변수들에 생성이나 할당을 어디서 할까요? 생성이나 할당하는 것을 보통 “초기화” 한다고 말하지요. 그러면 초기화를 하는 init()라는 함수를 만들어서 그 안에서 변수들을 초기화하면 되겠습니다.

static public function init( $alertWindow:Sprite, $modalWindow:Sprite ):void{
	// alertWindow 할당
	alertWindow = $alertWindow;
	// modalWindow할당
	modalWindow = $modalWindow;
	// alertContents할당
	alertContents = alertWindow.getChildByName( ‘contents’ );
}

코드를 보면 alertWindow와 modalWindow는 외부로부터 할당을 받는 형태로 되어있습니다. 그 이유는 CSmodal클래스의 역할은 창을 그리는 것이 아니고 창을 띄워 주고 확인, 취소 버튼을 눌렀을 때 그에 해당하는 동작을 하는 역할 이기 때문입니다. alertWindow와 modalWindow는 직접 넘겨 받아 할당하지만 alertContents는 alertWindow의 자식이기 때문에 alertWindow로 부터 할당 받습니다. 생각해보니 전역 변수로 잡을 필요도 없어지는군요. ^^; 이렇게 init()까지 완성이 되었습니다. 그러면 이제 실제로 사용할 수 있는 클래스가 되었습니다.

이렇게 차근 차근 코딩을 하다 보니 클래스가 뚝딱, 그것도 이쁘게 만들어 지더군요. 클래스를 만들 때 알고리즘과 구현에 집착하지 않고 역할을 꼼꼼히 따지고 생각해서 자신이 할 일을 명확하게 인지한 후 의사코드를 짜고 그 의사 코드를 통해 실제 코드로 변환하면 정말 깔끔한 클래스가 만들어 집니다. 매번 머릿속으로는 가지고 있던 생각이었지만 잘 되지 않았었는데 오늘은 잘 되어서 개인적으로도 매우 기뻤습니다 ^^; 어떤가요? 오늘 배운 내용을 복습하고, 짧게나마 공유해보고 싶어서 이렇게 포스팅을 합니다.

Older Entries

Powered by Prontovacanze.net