Spkite Kitの物理エンジン向けデバッグドロー用ライブラリ「PhysicsDebugger」

Sprite Kitには物理エンジンが組み込まれており、簡単に物理シミュレーションが利用できて便利です。
ただ、デバッグ表示用の機能がないため、どこにPhysicsBodyを追加したか、見た目では知ることができません。

そんな不満を解決してくれるのが、PhysicsDebuggerです。

早速使ってみます。

インストール

CocoaPodsに対応してるので簡単です。

# ターミナルを起動してプロジェクトのフォルダまで移動

# CocoaPodsをインストールしてない人
$ gem install cocoapods
$ pod install 

# Podfile
platform :ios, '7.0'

pod 'PhysicsDebugger'

# インストール
$ pod
Analyzing dependencies
Downloading dependencies
Installing PhysicsDebugger (1.0.0)
Generating Pods project
Integrating client project

[!] From now on use `SJRolePlaying.xcworkspace`.

CocoaPodsを初めて使う場合、これ以降.xcodeprojではなく、.xcworkspaceを開く必要があることに注意してください。

利用方法

ヘッダをインポートして、描画対象のノードが作成される前に、initを呼び、 drawPhysicsBodiesを呼びます。

今回は、SJMapNodeに追加してみました。

#import "YMCPhysicsDebugger.h"
#import "YMCSKNode+PhysicsDebug.h"

- (void)createNodeContents {
    
    [YMCPhysicsDebugger init];

    /* ノードの作成・追加 */
    
    [self drawPhysicsBodies];
}

これで実行すると、以下のようにPhysicsBodyに赤枠がついて可視化されます。


赤枠が表示される

感想

手軽に使えて便利でした。

ただ、ちょっとコードに手を加える量が多いかなぁ、という印象。 まぁそれでも、毎回PhysicsBodyと同じ大きさのShapeNodeを作成して追加する、とかに比べればはるかに綺麗ですが。

あと、このライブラリに限った話ではないですが、デバッグドローのぶんNodeの数が増えて動作がもっさりになるので、常に使用しながらの開発は厳しいかもしれません。

しばらく使って様子を見てみようと思います。

コメント

お店の中に他のキャラクターを登場させる

今日はマップ内に他のキャラクターを表示してみます。

前回はSJCharacgterNodeの中に定数でいろいろと持たせていましたが、 characters.jsonというファイルで定義するようにしました。1

{
    "default" : {
        "stop_time" : 0.6,
        "walk_time" : 0.3,
        "speed": 0.2
    },
    "c0" : {
        "name" : "clotharmor",
        "stop_row" : 0,
        "stop_cols" : 2,
        "walk_row" : 1,
        "walk_cols" : 4,
        "up_row" : 3,
        "right_row" : 6,
        "left_row" : 6,
        "size": 64
    },
    "c1" : {
        "name" : "scientist",
        "stop_row" : 0,
        "stop_cols" : 2,
        "walk_row" : 0,
        "walk_cols" : 2,
        "up_row" : 0,
        "right_row" : 0,
        "left_row" : 0,
        "size": 48
    },
    "c2" : {
        "name" : "chest",
        "stop_row" : 0,
        "stop_cols" : 1,
        "walk_row" : 0,
        "walk_cols" : 1,
        "up_row" : 0,
        "right_row" : 0,
        "left_row" : 0,
        "size": 32
    }
}

マップデータでどこに配置するか指定します。

-,-,-,-,-,-,-,-,-,-
-,-,-,-,-,-,-,-,-,-
-,-,-,-,-,-,-,-,-,-
-,-,-,-,-,-,-,-,-,-
-,-,-,-,-,-,-,-,-,-
-,-,-,-,-,-,-,-,-,-
-,-,-,-,c2,c1,-,-,-,-
-,-,-,-,-,-,-,-,-,-
-,-,-,-,-,-,-,-,-,-
-,-,-,-,-,-,-,-,-,-
-,-,-,-,-,-,-,-,-,-
-,-,-,-,-,-,-,-,-,-
-,-,-,-,c0,-,-,-,-,-

SJMapNode内で、cから始まるデータの場合はキャラクターを生成するようにします。

} else if ([col hasPrefix:@"c"]) {
    tileSprite = [[SJCharacterNode alloc] initWithCharacterNamed:col];

    tileSprite.physicsBody = [SKPhysicsBody bodyWithRectangleOfSize:CGSizeMake(TILE_SIZE, TILE_SIZE)];
    tileSprite.physicsBody.affectedByGravity = NO;
    tileSprite.physicsBody.allowsRotation = NO;

    tileSprite.name = col;

} 

SJCharacterNodeではjsonから設定を読み取ります。 この値を定数の代わりに使います。

- (id)initWithCharacterNamed:(NSString *)name {
    
    NSString *path = [[NSBundle mainBundle] pathForResource:CHARACTERS_NAME ofType:@"json"];
    NSData *data = [NSData dataWithContentsOfFile:path];
    NSError *error = nil;
    NSDictionary *characters = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingAllowFragments error:&error];
    if (error) {
        NSLog(@"%@", [error localizedDescription]);
    }
    NSMutableDictionary *character = [characters[@"default"] mutableCopy];

    [character addEntriesFromDictionary:characters[name]];

    CGFloat size = [character[@"size"] floatValue];
    
    if (self = [super initWithColor:nil size:CGSizeMake(size, size)]) {
        _character = character;
        [self createNodeContents];
    }
    return self;
}

完成です。


ひとりじゃない

相変わらず全然お店ではないですが、見た目はゲームっぽくなってきました。

次は会話を実装する予定です。

ソースコード: sj-prototype-apps/SJRolePlaying at master · tnantoka/sj-prototype-apps

  1. Objective-C的にはplistを使うのが定石だと思いますが、個人的にplistが苦手なのでここではJSONにしています。 

コメント

Mac OS X 10.9 Mavericksがリリースされました

ついにリリースされましたね。

正直なところ、そこまで魅力は感じていなかったんですが、まさかの無料ということで早速アップグレードしました。 今まで作ったサンプルが、最新版のXcodeでも問題なく動いていて一安心です。

残念ながら、このタイミングでも、日本語ドキュメント - Apple DeveloperにSprite Kit Programming Guideは追加されませんでした。 こうなると、しばらくは翻訳されなさそうです…。

NDA期間も終わったことだし、今後は機会があればMac向けの情報も扱っていきたいと思います。

コメント

物理エンジンを使ってお店の中を歩く

今回は、キャラクターをマップ内で歩かせてみます。

以下が完成イメージです。 ややぎこちないですが、リアルタイムバトルをやるわけでもないので、よしとします。


お店の中を歩く

通行状態とプレイヤーの位置を設定するために、マップデータにレイヤーを追加しています。

x,x,x,x,x,x,x,x,x,x
x,x,x,x,x,x,x,x,x,x
x,x,x,x,o,o,o,o,x,x
x,x,o,o,o,o,o,o,x,x
x,x,o,o,o,o,o,o,x,x
x,x,o,o,o,o,o,o,x,x
x,x,o,o,o,o,o,o,x,x
x,x,o,o,o,o,o,o,x,x
x,x,o,o,o,o,o,o,x,x
x,x,o,o,o,o,o,o,x,x
x,x,o,o,o,o,o,o,x,x
x,x,x,x,o,o,x,x,x,x
x,x,x,x,o,o,x,x,x,x

-,-,-,-,-,-,-,-,-,-
-,-,-,-,-,-,-,-,-,-
-,-,-,-,-,-,-,-,-,-
-,-,-,-,-,-,-,-,-,-
-,-,-,-,-,-,-,-,-,-
-,-,-,-,-,-,-,-,-,-
-,-,-,-,-,-,-,-,-,-
-,-,-,-,-,-,-,-,-,-
-,-,-,-,-,-,-,-,-,-
-,-,-,-,-,-,-,-,-,-
-,-,-,-,-,-,-,-,-,-
-,-,-,-,-,-,-,-,-,-
-,-,-,-,p,-,-,-,-,-

oは通行可で何もしません。xは通行不可なので、phsyicsBodyを設定したSKNodeを配置します。
pがプレイヤーの場所でphysicsBodyを指定したNodeを配置します。-は何もなしです。

以下がその部分のコード抜粋です。

if ([col isEqualToString:@"o"]) continue;
if ([col isEqualToString:@"-"]) continue;

SKNode *tileSprite;

if ([col isEqualToString:@"x"]) {
    tileSprite = SKNode.new;
    
    tileSprite.physicsBody = [SKPhysicsBody bodyWithRectangleOfSize:CGSizeMake(TILE_SIZE, TILE_SIZE)];
    tileSprite.physicsBody.dynamic = NO;
    
} else if ([col isEqualToString:@"p"]) {
    tileSprite = [SJCharacterNode characterNode];
    tileSprite.name = kPlayerName;

    tileSprite.physicsBody = [SKPhysicsBody bodyWithRectangleOfSize:CGSizeMake(TILE_SIZE, TILE_SIZE)];
    tileSprite.physicsBody.affectedByGravity = NO;
    tileSprite.physicsBody.allowsRotation = NO;
    
}

なお、今回から、マップの処理をSJMapNodeという専用クラスに任せるように変更しています。 上のコードもSJMapNode内のものです。

プレイヤーは新たに追加したSJCharacterNodeを使っています。 アニメーションはチュートリアルのサンプルゲームとほぼ同じロジックなので、ここでは移動の処理を抜粋します。

といっても、特に特別なことはやっておらす、画面がタップされたら、x軸・y軸の順でその方向を向いて歩いていくだけです。移動はタイルサイズ(32px)の単位で行なうにしています。

アニメーションはもちろんSKActionを利用。
また、物理エンジンの衝突を利用することで、通行状態を考慮する必要がく、コードがシンプルになっています。1

- (void)moveTo:(CGPoint)location {
    
    NSMutableArray *actions = @[].mutableCopy;
    CGPoint diff = CGPointMake(floor((location.x - self.position.x) / TILE_SIZE), floor((location.y - self.position.y) / TILE_SIZE));
    
    CGFloat x = diff.x * TILE_SIZE;
    CGFloat y = diff.y * TILE_SIZE;
    
    SKAction *moveX = [SKAction moveByX:x y:0 duration:abs(diff.x) * SPEED];
    SKAction *moveY = [SKAction moveByX:0 y:y duration:abs(diff.y) * SPEED];
    
    SKAction *walk = [SKAction runBlock:^{
        [self walk];
    }];
    SKAction *stop = [SKAction runBlock:^{
        [self stop];
    }];
    
    SKAction *turnX = [SKAction runBlock:^{
        if (diff.x > 0) {
            _direction = SJCharacterDirectionRight;
        } else if (diff.x < 0){
            _direction = SJCharacterDirectionLeft;
        }
    }];
    SKAction *turnY = [SKAction runBlock:^{
        if (diff.y > 0) {
            _direction = SJCharacterDirectionUp;
        } else if (diff.y < 0){
            _direction = SJCharacterDirectionDown;
        }
    }];
    
    [actions addObject:turnX];
    [actions addObject:walk];
    [actions addObject:moveX];

    [actions addObject:turnY];
    [actions addObject:walk];
    [actions addObject:moveY];
    
    [actions addObject:stop];
    
    SKAction *sequence = [SKAction sequence:actions];
    
    [self runAction:sequence withKey:MOVE_KEY];
}

これでキャラクターが歩けるようになりました。

ソースコードは、sj-prototype-apps/SJRolePlaying at master · tnantoka/sj-prototype-appsです。

まだまだ続きます。

  1. physicsBodyを設定した物体同士はデフォルトで衝突するため、プレイヤーはxのマスに移動できない。 

コメント

簡易タイルマップでお店風の背景を表示

自作RPGのリリースに向けて開発を進めていきたいと思います。 まずは店風の背景を表示してみます。

使う素材は、BrowserQuestのtilesheet.pngです。

この画像は、32x32のタイルが縦に20個、横に98個並んだものです。 扱いやすくするためにそれぞれ番号を振ります。

せっかくなのでこれもSprite Kitでやります。

static const CGFloat TILE_SIZE = 32.0f;
static const CGFloat SCALE = 0.75f;

static NSString * const BG_NAME = @"bg";

- (void)createSceneContents {
    self.backgroundColor = [SKColor darkGrayColor];
    
    SKTexture *tilesheet = [SKTexture textureWithImageNamed:@"tilesheet"];
    
    SKSpriteNode *bgSprite = [SKSpriteNode spriteNodeWithTexture:tilesheet];
    bgSprite.xScale = bgSprite.yScale = SCALE;
    bgSprite.anchorPoint = CGPointMake(0, 0);
    bgSprite.name = BG_NAME;
    [self addChild:bgSprite];
    
    NSInteger cols = tilesheet.size.width / TILE_SIZE;
    NSInteger rows = tilesheet.size.height / TILE_SIZE;
    
    for (int i = 0; i < cols; i++) {
        for (int j = 0; j < rows; j++) {
            CGPoint position = CGPointMake(i * TILE_SIZE, j * TILE_SIZE);
            
            SKLabelNode *pointLabel = [SKLabelNode labelNodeWithFontNamed:@""];
            pointLabel.text = [NSString stringWithFormat:@"%d", i + j * cols];
            pointLabel.position = CGPointMake(position.x + TILE_SIZE / 2.0f, position.y + TILE_SIZE / 2.0f);
            pointLabel.fontSize = 14.0f;
            pointLabel.verticalAlignmentMode = SKLabelVerticalAlignmentModeCenter;
            [bgSprite addChild:pointLabel];
        }
    }
}

透過されている部分がわかりやすいようにSceneに背景色をつけています。

そして、tilesheet.pngからSKSpriteNodeを作成し、anchorPointを左下に設定してSceneに追加します。 そのままだと大きくて見づらいので、xScale・yScaleで調整しています。

その後、32px毎にSKLabelNodeで番号を表示していきます。 これで以下のような画面になります。


各タイルに番号を

それでは、この番号を使ってマップを作成します。

まずは、マップデータです。 今回はCSVファイルで表現します。

504,505,506,506,506,507,507,507,508,509
484,485,486,486,486,487,487,487,488,489
464,465,466,466,466,467,467,467,468,469
464,465,466,466,466,467,467,467,468,469
464,465,466,466,466,467,467,467,468,469
464,465,466,466,466,467,467,467,468,469
444,445,446,446,446,447,447,447,448,449
444,445,446,446,446,447,447,447,448,449
444,445,446,446,446,447,447,447,448,449
444,445,446,446,446,447,447,447,448,449
444,445,446,446,446,447,447,447,448,449
424,425,426,-1,446,447,-1,427,428,429
404,405,406,-1,-1,-1,-1,407,408,409

-1,-1,-1,-1,-1,-1,-1,-1,-1,-1
-1,-1,285,286,-1,-1,-1,-1,-1,-1
-1,-1,265,266,-1,-1,-1,-1,-1,-1
-1,-1,-1,-1,-1,-1,-1,-1,-1,-1
-1,-1,1124,1125,1125,1125,1125,1126,-1,-1
-1,-1,1104,1105,1105,1105,1105,1106,-1,-1
-1,-1,1104,1105,1105,1105,1105,1106,-1,-1
-1,-1,1104,1105,1105,1105,1105,1106,-1,-1
-1,-1,1084,1085,1085,1085,1085,1086,-1,-1
-1,-1,-1,-1,-1,-1,-1,-1,-1,-1
-1,-1,-1,345,346,346,347,-1,-1,-1
-1,-1,-1,325,326,326,327,-1,-1,-1
-1,-1,-1,305,306,306,307,-1,-1,-1

あとはこれを読み込んで表示するだけです。

- (void)createSceneContents {
    
    NSString *shop = [NSString stringWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"shop" ofType:@"csv"]  encoding:NSUTF8StringEncoding error:nil];
    
    SKTexture *tilesheet = [SKTexture textureWithImageNamed:@"tilesheet"];
    
    NSArray *layers = [shop componentsSeparatedByString:@"\n\n"];
    
    for (NSString *layer in layers) {

        NSArray *rows = [[[layer componentsSeparatedByString:@"\n"] reverseObjectEnumerator] allObjects];
        for (int i = 0; i < rows.count; i++) {
            NSString *row = rows[i];
            NSArray *cols = [row componentsSeparatedByString:@","];
            for (int j = 0; j < cols.count; j++) {
                
                NSInteger col = [cols[j] integerValue];
                
                if (col > -1) {
                    CGFloat x = col % (NSInteger)MAP_COLS * TILE_SIZE / tilesheet.size.width;
                    CGFloat y = col / (NSInteger)MAP_COLS * TILE_SIZE / tilesheet.size.height;
                    CGFloat w = TILE_SIZE / tilesheet.size.width;
                    CGFloat h = TILE_SIZE / tilesheet.size.height;
                    
                    CGRect rect = CGRectMake(x, y, w, h);
                    SKTexture *tile = [SKTexture textureWithRect:rect inTexture:tilesheet];

                    SKSpriteNode *tileSprite = [SKSpriteNode spriteNodeWithTexture:tile];

                    CGPoint position = CGPointMake(j * TILE_SIZE, i * TILE_SIZE);
                    tileSprite.anchorPoint = CGPointMake(0, 0);
                    tileSprite.position = position;

                    [self addChild:tileSprite];
                    
                }
            }
        }

    }
}

やっていることは単純でCSVを空行区切りでレイヤーにわけて、 あとは順番に、番号に合うタイルをtextureWithRect:inTexture:使って表示しているだけです。

なお、-1は何も表示しないという意味にしています。

これで以下のように表示できます。


お店?

ゲームに使うには、各タイルの通行可否フラグなど保持したりしないといけないので、 TileクラスやMapクラスが必要になってくると思います。

今日のところは表示するところまで。

ソースコードは、sj-prototype-apps/SJRolePlaying at master · tnantoka/sj-prototype-appsにあります。

rectの計算で割り切れないことなどが原因で、ノイズが出てしまうのが気になるところ。
解消できたらまた書きます。

コメント

SpriteKitではじめる2Dゲームプログラミング Swift対応 (Smart Game Developer)