今回は、キャラクターをマップ内で歩かせてみます。
以下が完成イメージです。 ややぎこちないですが、リアルタイムバトルをやるわけでもないので、よしとします。
通行状態とプレイヤーの位置を設定するために、マップデータにレイヤーを追加しています。
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です。
まだまだ続きます。
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の計算で割り切れないことなどが原因で、ノイズが出てしまうのが気になるところ。
解消できたらまた書きます。
Xcode 5.0で遭遇した問題。
BrowserQuestのtilesheetを使って、マップを表示しようとしてたんですが、何度やってもうまくいかなくて困りました。
いろいろ試していて判明したのは、大きなサイズ(容量ではなくピクセル数)を指定したSKTextureが真っ黒になるということでした。
こんな感じで、4097ピクセル以上になると表示されなくなってしまうようです。 幅を変えても関係なさそうだったので、1辺の長さが問題になる模様。
SKTexture Class Referenceには特に何も書いていない気がしますし、エラーログなども出ていません。
サイズを小さくするしかない?
そもそも全ての環境で発生する問題なのかも不明ですが…。
ソースコードは、
sj-posts-apps/SJLargeTexture at master · tnantoka/sj-posts-apps
に置いてありますので、ご自由にどうぞ。
何かわかったらまた書きます。
Hirohito Katoさんから、コメント欄で原因を教えていただきました。
GPUの制限により4096x4096までのサイズしか扱えないためのようです。
参考
情報提供ありがとうございました!
Gumroadからダウンロードできます。
iBooksで読むとこのようになります。
アニメーションGIFもちゃんと動くので、インタラクティブな感じで楽しめるかもしれません。
0円以上に設定しているため、無料でもダウンロード可能ですのでお気軽にご利用ください。
今のところ、公式の情報はこれで全てだと思います。
プログラミングガイドの日本語訳が待ち遠しいですね。
iOS 7 Tech Talksの資料も公開されるといいんですが。