BLOG ARTICLE NSImage | 2 ARTICLE FOUND

  1. 2007.08.14 1.8 간단한 슈팅게임 (1) (4)
  2. 2007.05.16 1.6 SimpleViewer 이미지 뷰어 (1) (2)

1.8.0 프로젝트 개요
이번 장에서는 NSView와 NSImage를 이용해서 간단한 슈팅게임을 만들어 보겠습니다.

사용자 삽입 이미지
좌측과 같이 배경을 출력하고, 우주선을 좌우로 조종하고, 미사일을 발사하는  작업까지 하겠습니다.

키입력에서 다중으로 입력 받을 수가 없어 이동시에 미사일을 발사하면 이동이 중지 됩니다. 이문제는 다음 장에서 해결하도록 하겠습니다.





1.8.1 프로젝트 생성
Xcode를 실행하고 아래와 같이 SimpleShot 코코아 프로젝트를 생성합니다.
  1. 메뉴바에서 File/New Project...를 선택합니다.
  2. Application/Cocoa Application을 선택합니다.
  3. project Name에 SimpleShot를 입력하고 finish 버튼을 클릭합니다.

1.8.2 인터페이스 빌더에서 작업
1) 서브 클래스 생성

사용자 삽입 이미지
Nib 파일을 클릭하여 인터페이스빌더를 열고 NSView 클래스의 서브클래스를 만들고(아래의 창에서 NSView에 마우스 우클릭 합니다. 그 다음 Subclass NSView 클릭) 이름을 StageView로 변경합니다


사용자 삽입 이미지
StageView를 우클릭 하고 좌측과 같이 Create Files for StageView를 클릭하여 소스파일을 생성합니다.





2) NSView 생성 및 속성 설정

윈도우를 열고 팔레트의 Containers 항목에서  CustomeView를 드래그하여 아래와 같이 윈도우에 배치 합니다. View의 크기를 300X320으로 변경하고,  Custom Class 항목에서 StageView로 선택합니다.
사용자 삽입 이미지사용자 삽입 이미지
(클릭하면 이미지가 확대됩니다.)

1.8.3 이미지 파일

1) 이미지 생성

이제 배경, 우주선, 미사일에 쓰일 이미지를 준비합니다. 배경은 상관없지만 우주선과 미사일은 윤곽선이외에는 투명색으로 보여야 하기 때문에 png를 사용했습니다.

사용자 삽입 이미지
저는 도스의 디럭스페인트 이후로는 그래픽 작업이 불가능하므로 초등학생인 아들에게 이미지를 부탁하였습니다. 그래픽툴들을 다루실 수 있는 분들은 새로 만드셔도 되고, 아니면 첨부파일에서 다운로드 받으셔서 사용하시면 됩니다.

좌측은 배경화면 이미지입니다. StageView와 같은 300X320의 크기를 가지고 있습니다.



사용자 삽입 이미지
우주선 이미지 입니다. 하얗게 보이는 부분을 투명하게 처리해야 하단의 배경화면에서 올바르게 보입니다. 이를 위해 png파일 포맷을 사용했습니다.

사용자 삽입 이미지
미사일 이미지 입니다. 위와 같이 png파일 포맷을 사용했습니다.

2) 이미지 등록

사용자 삽입 이미지
준비된 이미지 파일을 드래그하여 Xcode 좌측의  Group & Files 목록의 Resources에 가져다 놓습니다.



드래그 시 나오는 판넬에서 아래와 같이 Copy items... 항목의 체크를 확인합니다.
사용자 삽입 이미지

1.8.4 소스코드 작성

1) StageView.h 수정

#import <Cocoa/Cocoa.h>

@interface StageView : NSView
{
    NSTimer *timer;
    NSImage *backgroundImage;
   
    NSRect heroRect;
    NSImage *heroImage;

    BOOL isFire;
    NSRect missileRect;
    NSImage *missileImage;
}

- (void)processGame;
- (void)fireMissile;
- (void)keyDown:(NSEvent *)event;

@end


2) StageView.m 수정

#import "StageView.h"

#define SCREEN_WIDTH        300
#define SCREEN_HEIGHT        320

#define HERO_SPEED            3       /* 우주선 속도 */
#define MISSILE_SPEED        4       /* 총알발사 속도 */

@implementation StageView

- (id)initWithFrame:(NSRect)frameRect
{
    if ((self = [super initWithFrame:frameRect]) != nil) {
        // Add initialization code here
       
        isFire = NO;
       
        /* 배경 이미지 로드 */
        NSString* imageName = [[NSBundle mainBundle] pathForResource:@"background" ofType:@"png"];
        backgroundImage = [[NSImage alloc] initWithContentsOfFile:imageName];

        /* 우주선 이미지 로드 & 초기 좌표 설정 */
        imageName = [[NSBundle mainBundle] pathForResource:@"hero" ofType:@"png"];
        heroImage = [[NSImage alloc] initWithContentsOfFile:imageName];
        heroRect.size = [heroImage size];
        heroRect.origin = NSMakePoint((SCREEN_WIDTH - heroRect.size.width)/2, 0);
       
        /* 미사일 이미지 로드 & 초기 좌표 설정 */
        imageName = [[NSBundle mainBundle] pathForResource:@"missile" ofType:@"png"];
        missileImage = [[NSImage alloc] initWithContentsOfFile:imageName];
        missileRect.size = [missileImage size];
        missileRect.origin = NSMakePoint(heroRect.origin.x + heroRect.size.width/2 - missileRect.size.width/2, heroRect.size.height);
       
        /* 30프레임으로 타이머 설정 */
        timer = [[NSTimer scheduledTimerWithTimeInterval: (1.0f / 30.0f)
                                                  target: self
                                                selector:@selector(processGame)
                                                userInfo:self
                                                 repeats:true] retain];
    }
    return self;
}

- (void) dealloc
{
    [backgroundImage release];
    [heroImage release];
    [missileImage release];
       
    [super dealloc];
}

/* 키입력을 받기 위한 설정 */
- (BOOL)acceptsFirstResponder
{
    return YES;
}

- (void)keyDown:(NSEvent *)event
{
    int keyCode;
    
    /* 현재 눌려진 키값을 얻어 온다. */
    keyCode = [event keyCode];
   
    /* 좌측으로 이동 */
    if(keyCode == 123)
    {
        heroRect.origin.x -= HERO_SPEED;
        if(heroRect.origin.x < 0)
            heroRect.origin.x = 0;
    }

    /* 우측으로 이동 */
    if(keyCode == 124)
    {   
        heroRect.origin.x += HERO_SPEED;

        if(heroRect.origin.x >= SCREEN_WIDTH - heroRect.size.width)
            heroRect.origin.x = SCREEN_WIDTH - heroRect.size.width;
    }
   
    /* 발사(스페이스) */
    if(keyCode == 49)
    {
        [self fireMissile];
    }
}       

- (void)fireMissile
{
    if(isFire == NO)
    {
        /* 미사일이 처음 발사되었을 경우에 초기위치를 우주선 좌표로 설정 */
        missileRect.origin = NSMakePoint(heroRect.origin.x + heroRect.size.width/2 - missileRect.size.width/2, heroRect.size.height);
    }
    isFire = YES;
}

- (void)processGame
{
    if(isFire == YES)
    {
        /* 미사일이 발사중이면 y좌표를 이동 */
        missileRect.origin.y += MISSILE_SPEED;
        if(missileRect.origin.y >= SCREEN_HEIGHT)
        {
            /* 화면상단에 위치했을 경우에는 미사일을 출력하지 않는다 */
            isFire = NO;
        }
    }
   
    [self setNeedsDisplay:YES];
}

- (void)drawRect:(NSRect)rect
{
    NSRect imgRect;
    NSRect drawRect;
   
    /* 배경 이미지 출력 */
    imgRect.origin = NSZeroPoint;
    imgRect.size = [backgroundImage size];
   
    drawRect = [self bounds];
   
    [backgroundImage drawInRect:drawRect
                     fromRect:imgRect
                    operation:NSCompositeSourceOver
                     fraction:1.0];
   
    /* 우주선 출력 */
    imgRect.origin = NSZeroPoint;
    imgRect.size = [heroImage size];
   
    [heroImage drawInRect:heroRect
               fromRect:imgRect
              operation:NSCompositeSourceOver
               fraction:1.0];   
   
    if(isFire == YES)
    {
        /* 미사일 출력 */
        imgRect.origin = NSZeroPoint;
        imgRect.size = [missileImage size];
       
        [missileImage drawInRect:missileRect
                     fromRect:imgRect
                    operation:NSCompositeSourceOver
                     fraction:1.0];   
    }   
}

@end

#define HERO_SPEED            3       /* 우주선 속도 */
#define MISSILE_SPEED        4       /* 미사일발사 속도 */

한 프레임에서 우주선과 총알의 움직이는 속도를 설정합니다. 좌/우 방향키를 누르면 각각의 방향으로 우주선이 한 프레임당 3픽셀을 움직입니다. 미사일은 4픽셀씩 상단으로 이동합니다. 숫자를 바꾸어 주면 해당 오브젝트의 속도를 변경할 수 있습니다.

StageView가 생성될 때 자동으로 호출되는 - (id)initWithFrame:(NSRect)frameRect 메소드에서 이미지 파일을 로드하고 타이머를 세팅하는 작업을 합니다.

NSString* imageName = [[NSBundle mainBundle] pathForResource:@"background" ofType:@"png"];
backgroundImage = [[NSImage alloc] initWithContentsOfFile:imageName];

어플리케이션의 리소스 디렉토리에서 background.png 이미지 파일을 로드하고 NSImage를 생성 합니다. 우주선과 미사일 이미지를 차례로 로드합니다.

timer = [[NSTimer scheduledTimerWithTimeInterval: (1.0f / 30.0f)
                                                  target: self
                                                selector:@selector(processGame)
                                                userInfo:self
                                                 repeats:true] retain];

1/30초 마다 processGame 함수를 호출하도록 타이머를 설정합니다. processGame 함수에서는 [self setNeedsDisplay:YES];로 설정하여 DrawRect가 호출되어 이미지를 출력하게 합니다.
이제 컴파일을하고 실행을 합니다. 좌우 방향키로 우주선을 움직이며 스페이스키로 미사일을 발사할 수 있습니다. 미사일은 현재 한발씩만 발사 할 수 있습니다. 다음 장에서는 적기들을 만들고 움직여 보도록 하겠습니다.

1.6.0 프로젝트 개요
JPG, GIF등의 이미지 파일을 NSImage를 이용하여 화면에 출력하고 , 확대 축소를 해주는 간단한 이미지 뷰어 프로그램을 만들어 보겠습니다.
사용자 삽입 이미지

1.6.1 프로젝트 생성
이전 포스트에서 경험 해 본 코코아 프로젝트와 소스파일을 생성하고, 인터페이스 빌더와 연결 할 수 있다는 전제 하에 설명하겠습니다. 이 부분에 이해가 안되시면 1.2와 1.3 포스트를 해보시고 오시기 바랍니다.

Xcode를 실행하고 아래와 같이 SimpleDraw 코코아 프로젝트를 생성합니다.
  1. 메뉴바에서 File/New Project...를 선택합니다.
  2. Application/Cocoa Application을 선택합니다.
  3. project Name에 SimpleViewer를 입력하고 finish 버튼을 클릭합니다.

1.6.2 인터페이스 빌더에서 작업

1) 서브 클래스 생성

Nib 파일을 클릭하여 인터페이스빌더를 열고 NSView 클래스의 서브클래스를 만들고(아래의 창에서 NSView에 마우스 우클릭 하여 Subclass NSView 클릭) 이름을 ImgView로 변경합니다.
사용자 삽입 이미지

위와 같은 방법으로 AppController란 NSObject의 서브클래스를 만듭니다. 서브클래스를 만든 후에 AppController를 우클릭하여 나오는 메뉴에서 Instantiate AppController를 클릭하여 인스턴스를 생성합니다.
사용자 삽입 이미지


2) 컨트롤 생성 및 속성 설정

윈도우를 열고 팔레트의 Containers 항목에서  CustomeView와 Controls 항목에서 Slider를 드래그해서 아래와 같이 윈도우에 배치 합니다.

CutomeView를 클릭 후에 인스펙터(command+5)를 불러 내어 Custom Class 항목에서, 이전에 만들어 놓은 ImgView를 선택하고 아래와 같이 CustomeView가 ImgView로 변경됨을 확인합니다.
사용자 삽입 이미지

Size 항목에서 하단의 Autosizing 항목에서 아래와 같이 사각형내의 선들을  마우스로 클릭하여 스프링처럼 나오게 만듭니다. 이 작업은 윈도우의 크기가 변경될 때 상하좌우 크기가 윈도우에 맞게 자동으로 변경되게 합니다.
사용자 삽입 이미지

슬라이더의 속성을 아래와 같이 설정 합니다. Minimum은 최소값, Maximum은 최대값, Current는 현재값, Number of Markers는 슬라이더 상단의 눈금의 갯수를 의미합니다.
사용자 삽입 이미지

슬라이더도 윈도우의 크기에 맞추어 변경되게 하기위해, 아래와 같이 Size 항목에서 하단의 Autosizing의 사각형 내부의 좌우선을 클릭하여 스프링이 되게 만듭니다. 이 작업은 슬라이더의 높이는 유지하며 너비가 윈도우의 크기와 함께 변하도록 만들어 줍니다.
사용자 삽입 이미지

3) 연결

사용자 삽입 이미지
인터페이스 빌더의 Instances항목에서 AppController의 인스펙터를 불러내어 imgView 아울렛을 추가하고, Type을 ImgView로 선택합니다.

인스펙터는 항목을 마우스로 클릭하여 포커스를 준 후에 shift+command+i 또는 commad+숫자 를 입력하여 불러낼 수 있습니다.

이 작업은 AppController에서 ImgView를 제어할 수 있도록 합니다.










사용자 삽입 이미지
이제 Actions 항목을 선택하고 resizeImage와 openImage 액션을 추가합니다.

resizeImage는 하단의 슬라이더가 변경될 때, 이미지 크기를 변경하고, openImage는 메뉴에서 파일을 오픈할 때 처리하기 위한 액션입니다.




control 키를 누른 상태에서 AppController를 드래그하여  ImgView에 놓습니다. 아래와 같이 AppControll의 ImgView 아울렛에 연결(connect 버튼을 클릭 후, 원모양 아이콘을 확인)합니다.
사용자 삽입 이미지

control 키를 누른 상태에서 슬라이더를 드래그하여 AppController에 놓습니다. Taget/Action에서 resizeImage에 연결합니다.
사용자 삽입 이미지

아래와 같이 메인메뉴의 File에서 오픈을 control 키를 누른 상태에서 드래그하여 AppController에 놓습니다. Target/Action에서 openImage에 연결합니다. 이 작업으로 사용자가 메뉴에서 Open을 클릭하면 AppController의 openImage를 호출합니다.
사용자 삽입 이미지

이제 인스펙트빌더에서 1차적인 작업이 완료되었습니다. Classes에서 AppController와 ImgView를 각각 우클릭하여 CreateFiles  for AppController(and Img View)를 선택하여 소스파일을 생성하고 Xcode로 돌아 갑니다.


1.6.3 소스코드 수정
1) AppController.h 수정

인터페이스 빌더에서의 작업으로 헤더파일에서 해야 할 작업은 거의 없습니다. 다만 컴파일 시 오류를 막기 위해 아래와 같이 @class ImgView; 란 라인을 추가합니다.
#import <Cocoa/Cocoa.h>

@class ImgView;

@interface AppController : NSObject
{
    IBOutlet ImgView *imgView;
}
- (IBAction)openImage:(id)sender;
- (IBAction)resizeImage:(id)sender;
@end


2) AppController.m 수정

#import "AppController.h"
#import "ImgView.h"

@implementation AppController

- (void)openPanelDidEnd:(NSOpenPanel *)openPanel
             returnCode:(int)returnCode
            contextInfo:(void *)x
{   
    NSString *path;
    NSImage *image;
   
    if (returnCode == NSOKButton) {
        path = [openPanel filename];
   
        NSImageRep *imgRep = [NSImageRep imageRepWithContentsOfFile:path];
        NSSize frameSize = [imgRep size];
       
        image = [[NSImage alloc] initWithContentsOfFile:path];
       
        [imgView setImage:image];
        [imgView setFrameSize:frameSize];
        [image release];
    }
}

- (IBAction)openImage:(id)sender
{
    NSOpenPanel *panel = [NSOpenPanel openPanel];
   
    [panel beginSheetForDirectory:nil
                             file:nil
                            types:[NSImage imageFileTypes]
                   modalForWindow:[imgView window]
                    modalDelegate:self
                   didEndSelector:
        @selector(openPanelDidEnd:returnCode:contextInfo:)
                      contextInfo:NULL];
}

- (IBAction)resizeImage:(id)sender
{
    [imgView setRatio:[sender floatValue]];
}

@end

#import "ImgView.h"
ImgView 클래스를 사용하기 때문에 위와 같이 ImgView.h를 인클루드 시킵니다.

아래의 메소드는 파일 오픈을 클릭하였을 경우, 파일선택 창에서 파일 선택이 완료 된 후에 불리어 지는 콜백 함수입니다. 이 메소는 파일오픈 판넬을 오픈할 때 등록하여 주며, 나중에 다시 설명하겠습니다.
- (void)openPanelDidEnd:(NSOpenPanel *)openPanel
             returnCode:(int)returnCode
            contextInfo:(void *)x
{   
    NSString *path;
    NSImage *image;

사용자가 파일 창에서 [열기] 버튼을 클릭하였을 경우에만 실행되도록 합니다.  
    if (returnCode == NSOKButton) {
        path = [openPanel filename];
   
파일 원본 크기를 알기 위해 NSImageRep 오브젝트를 사용합니다.
        NSImageRep *imgRep = [NSImageRep imageRepWithContentsOfFile:path];
        NSSize frameSize = [imgRep size];
     
NSImage에 이미지 파일을 등록합니다.  
        image = [[NSImage alloc] initWithContentsOfFile:path];
       
imgView에 이미지를 설정하고, imgView의 크기를 이미지의 원본크기로 변경합니다.
        [imgView setImage:image];
        [imgView setFrameSize:frameSize];
        [image release];
    }
}

다음은 사용자가 파일오픈을 선택하였을 때 호출되는 - (IBAction)openImage:(id)sender 메소드에서 파일선택 판넬을 오픈하기 위해 아래와 같이 추가합니다.
    NSOpenPanel *panel = [NSOpenPanel openPanel];
   
    [panel beginSheetForDirectory:nil
                             file:nil
                            types:[NSImage imageFileTypes]
                   modalForWindow:[imgView window]
                    modalDelegate:self
                   didEndSelector:
        @selector(openPanelDidEnd:returnCode:contextInfo:)
                      contextInfo:NULL];
모달(판넬이 떠있을 때는 닫기전에는 소유 윈도우는 제어되지 않습니다)로 뛰우며 선택 가능한 파일 종류, 소유 윈도우, 콜백함수를 설정합니다.

@selector(openPanelDidEnd:returnCode:contextInfo:) 이 부분에서 파일선택이 완료되면 위에서 작성한 openPanelDidEnd 메소드가 호출되도록 하여 줍니다.

[imgView setRatio:[sender floatValue]];
슬라이더가 변경되면 호출되는 resizeImage 메소드에서 imgView가 크기를 변경하도록 setRatio를 호출합니다. 이 메소드는 다음 ImgView 클랙스에서 작성되어질 것 입니다.

3) ImgView.h 수정

#import <Cocoa/Cocoa.h>

@interface ImgView : NSView
{
    NSImage *image;
    NSSize orgSize;
    float ratio;
}

- (void)setImage:(NSImage *)img;
- (void)setRatio:(float)r;

@end

NSImage *image;

이미지 출력을 위해 NSImage 클래스를 선언 합니다.

NSSize orgSize;

이미지의 원본 크기를 저장합니다.

float ratio;
이미지 확대/축소 비율을 저장합니다. 0이 최소, 20이 최대, 10이 원본크기를 출력합니다.

- (void)setImage:(NSImage *)img;
선택된 이미지를 image에 설정하는 메소드입니다. 파일메뉴에서 파일 선택 시 AppController에서 호출합니다.

- (void)setRatio:(float)r;
확대/축소 비율을 설정합니다. 슬라이더 변경 시 AppController에서 호출합니다.

4) ImgView.m 수정

#import "ImgView.h"

@implementation ImgView

- (id)initWithFrame:(NSRect)frameRect
{
    if ((self = [super initWithFrame:frameRect]) != nil) {
        // Add initialization code here
        ratio = 10;
    }
    return self;
}

- (void)drawRect:(NSRect)rect
{
    [[NSColor whiteColor] set];
    [NSBezierPath fillRect:rect];
   
    if(image) {
        NSRect imgRect;
        NSRect drawRect;
       
        if(ratio != 0.0f) {
            imgRect.origin = NSZeroPoint;
            imgRect.size = orgSize;
       
            drawRect = [self bounds];
       
            NSLog(@"drawRect: %f, %f", drawRect.size.width, drawRect.size.height);
           
            [image drawInRect:drawRect
                     fromRect:imgRect
                    operation:NSCompositeSourceOver
                     fraction:1.0];
        }   
    }
}

- (void)setImage:(NSImage *)img
{
    [img retain];
    [image release];
   
    image = img;
   
    orgSize = [image size];
   
    [self setNeedsDisplay:YES];
}

- (void)setRatio:(float)r
{
    NSSize viewSize;
    ratio = r;
   
    NSLog(@"RATIO: %f", ratio);
   
    viewSize.width = (orgSize.width * ratio)/10;
    viewSize.height = (orgSize.height * ratio)/10;
   
    [self setFrameSize:viewSize];
    [self setNeedsDisplay:YES];
}

- (void) dealloc
{
    [image release];
    [super dealloc];
}

@end

ratio = 10;
initWithFrame는 ImgView가 초기화될 때, 자동으로 호출되는 메소드 입니다. 이곳에서 초기 오브젝트를 생성하거나 변수를 초기화 합니다. 이미지 선택시 원본 크기로 보이게 하기 위해 ratio를 10으로 설정합니다.

다음은 drawRect 메소드 내에 아래의 라인을 추가합니다. drawRect는 ImgView가 그려져야 할 필요가 있을 때 자동으로 불려지는 메소드 입니다. 이곳에서 그려질 내용들을 처리 합니다.

[[NSColor whiteColor] set];
[NSBezierPath fillRect:rect];
배경색을 흰색으로 채웁니다.

이미지 오브젝트가 설정되었을 때만 출력합니다.
    if(image) {
        NSRect imgRect;
        NSRect drawRect;

확대비율이 0일때는 출력하지 않습니다.  
        if(ratio != 0.0f) {
            imgRect.origin = NSZeroPoint;
            imgRect.size = orgSize;
       
            drawRect = [self bounds];

이미지를 출력합니다.  
            [image drawInRect:drawRect
                     fromRect:imgRect
                    operation:NSCompositeSourceOver
                     fraction:1.0];
        }   
    }
}

아래의 setImgae에서는 사용자가 선택한 파일을 image에 설정합니다.
- (void)setImage:(NSImage *)img
{
    [img retain];
    [image release];
 
image를 설정하고 원본 사이즈의 크기를 저장합니다.
    image = img;
    orgSize = [image size];
다시 그려지도록 알려주며, 이 위의 drawRect가 호출됩니다.  
    [self setNeedsDisplay:YES];
}

아래의 setRatio에서는 사용자가 슬라이더의 변경시에, 크기를 변경해 주는 작업을 합니다.
- (void)setRatio:(float)r
{
    NSSize viewSize;
    ratio = r;

10을 원본 크기로 비율에 맞게 크기를 조절합니다.  
    viewSize.width = (orgSize.width * ratio)/10;
    viewSize.height = (orgSize.height * ratio)/10;

ImgView의 크기를 현재 배율에 맞게 변경합니다.  
    [self setFrameSize:viewSize];
다시 그려지도록 알려주며, 이 위의 drawRect가 호출됩니다.
    [self setNeedsDisplay:YES];
}

메모리에서 해제합니다.
- (void) dealloc
{
    [image release];
    [super dealloc];
}

이제 모든 변경사항을 저장하고 프로젝트를 빌드 후에, 이미지 파일을 선택하여 테스트 해봅니다. 이미지를 불러보고 윈도우의 크기를 변경해 봅니다. 아직 스크롤을 처리하지 않아 커다란 이미지는 제대로 보실 수 없을 것입니다. 이는 다음장에서 처리해 보겠습니다.