2015年5月29日金曜日

Objective-Cのプロトコル

正式プロトコル

通信におけるプロトコル(TCP-IPとか)なんかと同様、オブジェクトに共通するメソッドを規約として定義するやり方。

メソッドの中のアルゴリズムはおまかせだけど、メソッド名や引数、実行結果が同じであればいい。

以下のように宣言し、通常はヘッダファイルとして保存しておく。(実装ファイルはいらない)
@protocol プロトコル名
メソッドの宣言;
@end

そしてインポートして使う。
#import "プロトコル名.h"
@interface クラス名:スーパークラス名 <プロトコル名>
というのが書き方。
< >の間に何か書くのはObjective-Cで書いてれば知らず知らず使ってるよね。


クラス共通の規約だから、プロトコルに宣言されたメソッドは全部実装してやらないとダメ。



非形式プロトコル

プロトコルの一種だけど、意味合いがちょっと違う。
メソッドの宣言をするのにNSObjectのカテゴリとして宣言する。
メソッドの実装は必須じゃない。
delegateを利用するのによく使う。

よくわかんないでしょ? 俺もよくわかんないw まあそういうものがあるっつーことで。
delegateを自作するなら、こっちをよく使うよね。

Objective-Cのカテゴリ

なんてこたぁない。
数多くのメソッドを持つクラスを書く時、ひとつのファイルに書くとわかりにくい。
これを解消するための方法が「カテゴリ」。
要は、ひとつのファイルにまとめて書いていたメソッドを、いくつかの別ファイルに分けて書けるということ。
機能的に関係ないメソッドをまとめてもしょうがないから、同じような種類のカテゴリーごとに分けるからカテゴリって呼ばれるわけやね。
べつに違う種類のメソッドをまとめてもいいんだけど。

メインのファイルと、そこから分けたカテゴリのファイルがいくつか…という構成になる。
インスタンス変数はメインのファイルにしか書けない。

具体的な書き方は省略するので、別途調べてね。簡単だから。


慣例としてはヘッダファイルだけは@interfaceをいくつも書いてひとつにまとめてしまい、実装ファイルの方だけファイルを分けるんだそうな。
ヘッダファイル、実装ファイル両方とも分けたり、実装ファイルだけひとつにまとめて書くこともできる。


カテゴリ間をまたがってメソッドを呼ぶには、呼ばれる方のメソッドの入ったカテゴリがファイル内で先に定義されているか、#importしてなけりゃいけない。

2015年5月28日木曜日

SpriteKitでの画面切り替え効果SKTransition

SpriteKitで画面(Scene)を切り替える際のトランジションの種類。
newSceneに新たなSKSceneを事前に作っておき、self.view上にあるsceneを切り替える形となる。
具体的なコードは以下。
SKTransition *tr = [SKTransition crossFadeWithDuration:2.0];
[(SKView *)self.view presentScene:newScene transition:tr];

SKTransitionクラスで設定するトランジションの種類は以下。

duration(切り替え時間)は全て2秒としている。

SKTransition *tr = [SKTransition crossFadeWithDuration:2.0];
クロスフェイド。元画面がフェードアウトするのと一緒に新画面がフェードインする。

SKTransition *tr = [SKTransition doorsCloseHorizontalWithDuration:2.0];
SKTransition *tr = [SKTransition doorsCloseVerticalWithDuration:2.0];
SKTransition *tr = [SKTransition doorsOpenHorizontalWithDuration:2.0];
SKTransition *tr = [SKTransition doorsOpenVerticalWithDuration:2.0];
水平方向/垂直方向に、真ん中から扉が開く/真ん中に閉じるように新画面が現れる。

SKTransition *tr = [SKTransition doorwayWithDuration:2.0];
真ん中から左右に扉が開き、奥から少し小さい新画面が拡大しながら出てくる。
お城で殿の御成りといった感じ。

SKTransition *tr = [SKTransition fadeWithColor:[UIColor redColor] duration:2.0];
一度指定した色に向けてフェードアウトし、新画面がさらにフェードインする。

SKTransition *tr = [SKTransition fadeWithDuration:2.0];
元画面がフェードアウトして暗くなり、新画面がフェードインする。
crossFadeとの違いは、暗くブラックアウトすること。

SKTransition *tr = [SKTransition flipHorizontalWithDuration:2.0];
SKTransition *tr = [SKTransition flipVerticalWithDuration:2.0];
カードをめくるようにそれぞれ、Horizontalが縦回転、Verticalが横回転する。
感覚としては逆のような気がするが、回転軸が水平(Horizontal)か垂直(Vertical)ってことなんだろうな?

SKTransition *tr = [SKTransition moveInWithDirection:SKTransitionDirectionDown duration:2.0];
旧画面の上に、上下左右の方から滑るように新画面が入ってくる。
SKtransitionDirectionはUp/Down/Left/Rightがあるが、それぞれそちらの方向から画面が入ってくる。(Downなら下から新画面が入ってくる)

SKTransition *tr = [SKTransition pushWithDirection:SKTransitionDirectionUp duration:2.0];
旧画面を押し出すように上下左右から新画面が入ってくる。
これもSKtransitionDirectionで指定した方向から画面が入る。

SKTransition *tr = [SKTransition revealWithDirection:SKTransitionDirectionLeft duration:2.0];
moveInの逆に、旧画面が上下左右に滑り出て行って、背後の新画面が現れる。
SKtransitionDirectionの指定は他と違い、旧画面の出て行く方向。

SKTransition *tr = [SKTransition transitionWithCIFilter:(CIFilter)  duration:2.0];
CoreImageのフィルターを使うことができる。
扱えるフィルターは、inputImageinputTargetImageのパラメータを持つもののみだそうだ。(CIFilter)の部分にフィルターを書くのだが、よくわかんない。
単にフィルターを指定するだけでなく、事前にパラメータをいくつか設定する必要があるようだ。
トランジションの幅が広がるが、具体的なやり方はわかったら追記する。

2015年5月27日水曜日

SKAction customActionWithDuration: actionBlock:は要注意?

spriteにActionを設定するメソッドの一つ、
customActionWithDuration:actionBlock:
がよくわからん。

書き方は以下のとおりで、duration値の秒数だけblock構文を繰り返すというもの。
elapsedTimeというのは処理の経過時間秒数。

SKAction *action = [SKAction customActionWithDuration:5
                   actionBlock:^(SKNode *node, CGFloat elapsedTime) {
                     SKAction *ac = [SKAction rotateByAngle:M_PI duration:5.0];
                     [node runAction:[SKAction repeatActionForever:ac]];
                   }];

上の場合、5秒間spriteを回転させるコード。
rotateByAngleのdurationが同じ5秒なので、5秒かけてゆっくり180度回転して止まってくれると思ってた。

しかし実際やってみると、spriteが高速回転して止まらない。
repeatActionForeverを使わないと、5秒経過後、しばらくして止まる。でも高速回転に変わりなし。

durationを設定するメソッドが入れ子になるせいで、処理がおかしくなるんだろうな。

中に変数を使った計算をする処理を入れたら、途中からありえないでかい値になったこともあった。

customActionWithDuration:0
にしたところ、正常動作した。
おそらく0ということはcustomActionWithDurationメソッドは実行されず、block中のActionだけが実行されたから動いたんじゃないかと思う。
じゃあcustomActionWithDuration使う意味ないけどね。

まだよくわからん。

SKAction runBlock:について

Objective-Cの場合

SpriteにActionを設定するやり方の一つに、block構文を用いる runBlock: メソッドがある。

繰り返しActionを実行する場合、Actionの都度block構文中の処理を全部やってくれるのかと思ってたが、そうじゃないようだ。

たとえば以下の様な場合、設定したmoveByX:100 y:0 duration:1は繰り返し実行してくれるが、NSLogが表示されるのは一度きり。

SKAction *action = [SKAction runBlock:^{
        NSLog(@"hogehoge");
        SKAction *ac = [SKAction moveByX:100 y:0 duration:1];
        [_sprite runAction:[SKAction repeatActionForever:ac]];
    }];
[_sprite runAction:action];

変数使ったり乱数を埋め込んだりしても、一番最初に出た値がずっと保持されるようだ。
Actionのたびに移動量をランダムで変化させるとかどうやるんだ?
どうもこのActionってやつは、決まりきった処理をさせることはできても、インタラクティブな処理(状況に応じてActionの内容や動きを変える)なんてのはできないっぽいな。
それが必要な場合はActionの外のコードで状況をチェックして、再度Actionを設定するとかそういう処理が必要っぽい。なんだよ、使えねえな。


なお、block中で
[_sprite runAction:[SKAction repeatActionForever:ac]];
とやってやらないと、うまくいかない。
blockの外の
[_sprite runAction:action];
の位置で
[_sprite runAction:[SKAction repeatActionForever:action]];
とやってやると、block中の処理は繰り返されるようになるが、それ以外の処理を受け付けなくなったり、いいことない。
実際のActionを繰り返すのでなく、blockの定義自体を繰り返すことになっているんだろう。
(最後XcodeからもiPad実機からも終了できなくなり、リセットするに至った(ノД`))
なかなかむずかしい。



同様にblock構文が使える
customActionWithDuration: actionBlock:
を使ったら、こちらも一度しか呼ばれないし、Durationの数値を変えるとblock中の変数の値が、途中からとんでもなくでかい数値になったりしてますますわかんない。
これは別の記事に書く。

Swift4の場合

SKAction.run { } もしくは SKAction.run ({ }) を使ってクロージャの中のコードを実行させることができる。
クロージャの中にSpriteのrun()を書く必要があるのだが、以下のようにしてrepeatForeverとして繰り返し実行させ、中の値を乱数で毎回変化させたい場合、なかなかうまくいかない。

【注意】深刻なトラブルを起こすコード

まずは本筋と関係ないのだが、以下のコードを実行したところ、かなりこわいことになったので注意。
Objective-C + iPadの時にもなったし、iOS11 + Swift4のiPhone8でも再現したので、再現性もある。

以下のコードを実機で実行すると
  1. アプリの画面が固まり
  2. Xcodeを強制終了させなければならなくなり
  3. 実機確認しようとしてもできなくなり
  4. iPhoneのスリープボタンを押したら画面がブラックアウトし画面キャプチャー操作で音が鳴るので、起動はしてるらしい)
  5. iPhone強制再起動(iPhone8だと音量上、下を一度ずつ押した後、スリープボタン長押し)でようやく元に戻る
という事態に。iTunesから復元する事態には至らずに済んだがだいぶ困った。

クロージャ中の細かなコード云々より、repeatForeverの使い方がいけないんだろうな。(普通はアプリが落ちる程度で済むんだけど)

let spriteNode = SKSpriteNode(imageNamed: "イメージ名")
let furafura = SKAction.run {
        let x = CGFloat(arc4random_uniform(100)) - 50
        let y = CGFloat(arc4random_uniform(100)) - 50
        print("x \(x), y \(y)")
        SKAction.moveBy(x: x, y: y, duration: 0.5)
}
spriteNode.run(SKAction.repeatForever(furafura))

以下はアプリが落ちるだけで平気

落ちる例その1

以下のようにすると毎回乱数が発生し、print()も毎回違った値が表示される。
しかし画面の表示が止まってしまい、最後にXcodeに「iPhoneとの接続が切れたので、再接続してください」みたいなダイアログが出て落ちてしまう。
どうも乱数の部分だけが変更されるのでなく、次々と新たなクロージャが作られて実行される無限ループに陥っているようだ。まあrepeatForeverしてるんだから当然だけど。

let furafura = SKAction.run {
    let action = SKAction.repeatForever(
        SKAction.run({
            let x = CGFloat(arc4random_uniform(100)) - 50
            let y = CGFloat(arc4random_uniform(100)) - 50
            print("x \(x), y \(y)")
            SKAction.moveBy(x: x, y: y, duration: 0.5)
        })
    )
    spriteNode.run(action)
}
spriteNode.run(furafura)

落ちる例その2

以下の2例はrepeatForeverをクロージャの外に出したものだが、print()の実行も一度だけで、画面も固まり、しばらくするとメモリの問題で落ちる。

2の1

let furafura = SKAction.repeatForever(
    SKAction.run {
        let action = SKAction.run({
            let x = CGFloat(arc4random_uniform(100)) - 50
            let y = CGFloat(arc4random_uniform(100)) - 50
            print("x \(x), y \(y)")
            SKAction.moveBy(x: x, y: y, duration: 0.5)
        })
        spriteNode.run(action)
    }
)
spriteNode.run(furafura)

2の2

let furafura = SKAction.run {
    let action = SKAction.run({
        let x = CGFloat(arc4random_uniform(100)) - 50
        let y = CGFloat(arc4random_uniform(100)) - 50
        print("x \(x), y \(y)")
        SKAction.moveBy(x: x, y: y, duration: 0.5)
    })
    spriteNode.run(action)
}
spriteNode.run(SKAction.repeatForever(furafura))

動かない例

以下のようにしてrepeatForeverとして繰り返し実行させ、中の値を乱数で毎回変化させようとしても、値が変わってくれない。print()を入れてみたら、一度しか実行されていない。
どうやらクロージャを定義した時に得られた乱数で確定されているようだ。
let furafura = SKAction.run {
    SKAction.repeatForever(
        SKAction.run({
            let x = CGFloat(arc4random_uniform(100)) - 50
            let y = CGFloat(arc4random_uniform(100)) - 50
            print("x \(x), y \(y)")
            SKAction.moveBy(x: x, y: y, duration: 0.5)
        })
    )
}
spriteNode.run(furafura)


2015年5月22日金曜日

Spriteを物理体にするときの座標に注意

Spriteに物理シミュレーション(重力によって下に落としたり、別なスプライトとの衝突判定をつけたり)の対象とするためには、そのspriteに対して物理体というのを作ってやる必要がある。作る…というか、spriteのどの範囲を物理体にするか指定してやるのだな。

範囲が矩形の場合はsprite(ここでは_oni)を作った後に、以下のコードで指定する。

_oni.physicsBody = [SKPhysicsBody bodyWithEdgeLoopFromRect:rect];

このrectの指定だが、UIViewにあたるSKScene上の絶対座標でなく、指定するSpriteの原点(デフォルトではSprite中心)からの相対座標で指定しないといけない。
わかんないで、上の例で100,380からの縦横64ピクセルの範囲を指定したら、Spriteの原点から右に100、上に380もずれた画面外から始まる縦横64ピクセルの範囲になってしまい、Spriteの位置に当たり判定が付かずにだいぶ迷ってしまった。

このため、正しいrectの設定は以下のコードになる。

CGRect rect = CGRectMake(-(_oni.frame.size.width / 2), -(_oni.frame.size.height / 2), _oni.frame.size.width_oni.frame.size.height);

原点が0,0ということになるので、width、heightそれぞれの半分の値をマイナスにしてrectのx、yにしてるわけね。
width、heightはそのまま。

考えてみりゃ、位置を変えるSpriteに親オブジェクトのSKSceneの絶対座標なんか使うわけないんだけど、引っかかったので書いておく。'`,、('∀`) '`,、

2015年5月21日木曜日

SKSceneのデフォルトサイズが変な件

SpriteKitでゲーム作ろうとして、iPhoneのPortrait(縦長)画面の0,0の位置にSpriteを表示しようとしたら、表示されなかった。

CGRectGetMidXとか使ってpositionを画面中央に指定したら表示されたんだけど、絶対位置で指定するとかなり数字を大きくしてやらないと表示されない。

self.frame.size.widthと.heightを調べたらportrait画面なのに1024*768の横長だって。
GameView.sksで1024*768になってるプロパティを変更してみたけど変わらない。

調べたところ、Universalアプリの場合、SKSceneのデフォルトサイズが1024*768なんだって。
Universalアプリってことは、iPhone、iPad両用のアプリってことだよね? うちではXcodeでiPhone用だけを選んで同じことになってるので、Universalか否かは関係ないかも?

とりあえず、GameViewControllerでSKSceneを作る際、
scene.size = skView.frame.size;
のコードを入れておけば320*480の縦長サイズになってくれるので問題解決だ。
なんだかよくわかんないことするね、Appleも。

参考:【Swift】UniversalなSpriteKit開発 SKSceneが微妙なScaleをしてしまう問題の対処