【Swift】SpriteKitの使い方。2つの物理ボディをくっつけたり、ピンやヒモで結合する。(Swift 3.0、XCode 8.0)
SKPhysicsJointとは
本記事ではSKPhysicsJoint(以下、物理ジョイント)について説明する。
物理ジョイントとは、異なる物理ボディ同士をくっつけるクラスである。固定、ピン、ヒモ、バネなどくっつける方法は複数ある。
物理ジョイントを試してみる
実際に物理ジョイントを使って物理ボディをくっつけてみよう。
以降の手順を行う前のXcodeプロジェクトをGitHubに置いたので、試してみる方はご利用下さい。⇒「テスト用プロジェクト」
事前準備では2本の丸太をシーンに配置し、ドラッグで運べるようにしておいた。
以下のコードは変更前のソースコード。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 |
// // TestScene.swift // import Foundation import SpriteKit class TestScene:SKScene { var maruta1:SKSpriteNode! var maruta2:SKSpriteNode! var target:SKSpriteNode! //現在シーン設定時の呼び出しメソッド override func didMove(to view: SKView) { //SKSファイルに配置した丸太ノードを取得する。 maruta1 = self.childNode(withName: "maruta1") as? SKSpriteNode maruta2 = self.childNode(withName: "maruta2") as? SKSpriteNode //丸太をターゲットノードに設定する target = maruta1 } //画面タッチ開始時の呼び出しメソッド override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) { //タッチしたノードを取得する。 let location = touches.first!.location(in: self) let node = atPoint(location) as! SKSpriteNode //タッチしたノードが丸太の場合、ターゲットノードに設定する。 if(node == maruta1 || node == maruta2){ target = node } } //画面タッチ移動時の呼び出しメソッド override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) { //タッチした座標を取得する。 let location = touches.first!.location(in: self) //ターゲットノードをタッチした座標まで移動するアクションを実行する。 let action = SKAction.move(to: CGPoint(x:location.x, y:location.y), duration:0.1) target.run(action) } } |
固定する (SKPhysicsJointFixed)
物理ボディを接着剤でくっつけたのように固定する。
didMoveメソッドに以下のコードを追加する。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
//現在シーン設定時の呼び出しメソッド override func didMove(to view: SKView) { ‥省略‥ //物理ジョインを作成する。 let joint = SKPhysicsJointFixed.joint(withBodyA: maruta1.physicsBody!, bodyB: maruta2.physicsBody!, anchor: CGPoint(x: maruta1.frame.maxX, y: maruta1.frame.midY)) //物理ジョインを追加する。 self.physicsWorld.add(joint) } |
引数で指定するアンカーポイントには丸太と丸太のちょうど真ん中を指定した。しかし、固定の場合はアンカーポイントをどこにしようと動きは同じになる。
固定するために物理ボディ同士を接触させる必要は無い。接触していなくても透明の棒で繋げたかのように物理ボディは固定される。
以下は実際のプレイ動画。ノードは平行移動するのではなく、くっついている物理ボディの影響を受けて角度が変わる。
検証では画面端に物理ボディの壁を設置している。壁にぶつかると物理ボディがくっついて離れなくなるのが気になる。
ピンで結合する (SKPhysicsJointPin)
物理ボディと物理ボディをピンをつけて結合し、関節のように回転できる状態で結合する。
didMoveメソッドの物理ジョイント作成箇所を以下のコードに変更する。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
//現在シーン設定時の呼び出しメソッド override func didMove(to view: SKView) { ‥省略‥ //物理ジョインを作成する。 let joint = SKPhysicsJointPin.joint(withBodyA: maruta1.physicsBody!, bodyB: maruta2.physicsBody!, anchor: CGPoint(x: maruta1.frame.maxX, y: maruta1.frame.midY)) //物理ジョインを追加する。 self.physicsWorld.add(joint) } |
アンカーポイントは先ほどと同じように丸太と丸太の間にしている。ピンの場合はアンカーポイントが関節部のようになって物理ボディ同士に角度をつけれるようになる。
以下は実際のプレイ動画
物理ジョイントのプロパティを変更することで回転の抵抗や最小、最大角度を設定することができる。
以下のコードは最小角度を30度、最大角度を90度に設定したコード。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
//現在シーン設定時の呼び出しメソッド override func didMove(to view: SKView) { ‥省略‥ //回転の抵抗を設定する。 joint.frictionTorque = 0.5 //最小角度を30度に設定する。 joint.lowerAngleLimit = 0.5 //最大角度を90度に設定する。 joint.upperAngleLimit = CGFloat(M_PI_2) //回転角度の制限を有効にする。 joint.shouldEnableLimits = true } |
以下は実際のプレイ動画。30度〜90度以外の角度を維持できなくなった。
ヒモで結ぶ (SKPhysicsJointLimit)
物理ボディと物理ボディを紐で結んだように結合する。
didMoveメソッドの物理ジョイント作成箇所を以下のコードに変更する。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
//現在シーン設定時の呼び出しメソッド override func didMove(to view: SKView) { ‥省略‥ //物理ジョインを作成する。 let joint = SKPhysicsJointLimit.joint(withBodyA: maruta1.physicsBody!, bodyB: maruta2.physicsBody!, anchorA: CGPoint(x: maruta1.frame.maxX, y: maruta1.frame.midY), anchorB: CGPoint(x: maruta2.frame.minX, y: maruta2.frame.midY)) //物理ジョインを追加する。 self.physicsWorld.add(joint) } |
以下は実際のプレイ動画。丸太をドラッグしたとき、プレイ開始時の丸太間の距離より大きくなると、他の丸太が引きずられてついてくる。丸太同士がぶつからなくなるのが残念だ(Swift 3.0)。
動きが分かりづらいので、丸太間に線を描画してみよう。
TestScene.swiftを以下のコードに変更する。変更箇所は青色箇所。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 |
// // TestScene.swift // import Foundation import SpriteKit class TestScene:SKScene { var maruta1:SKSpriteNode! var maruta2:SKSpriteNode! var target:SKSpriteNode! var line:SKShapeNode! //現在シーン設定時の呼び出しメソッド override func didMove(to view: SKView) { //SKSファイルに配置した丸太ノードを取得する。 maruta1 = self.childNode(withName: "maruta1") as? SKSpriteNode maruta2 = self.childNode(withName: "maruta2") as? SKSpriteNode //丸太をターゲットノードに設定する target = maruta1 //物理ジョインを作成する。 let joint = SKPhysicsJointLimit.joint(withBodyA: maruta1.physicsBody!, bodyB: maruta2.physicsBody!, anchorA: CGPoint(x: maruta1.frame.maxX, y: maruta1.frame.midY), anchorB: CGPoint(x: maruta2.frame.minX, y: maruta2.frame.midY)) //物理ジョインを追加する。 self.physicsWorld.add(joint) //図形ノードを作成する。 line = SKShapeNode() line.strokeColor = SKColor.gray line.lineWidth = 5 //図形ノードを子ノードに追加する。 self.addChild(line) } //画面タッチ開始時の呼び出しメソッド override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) { //タッチしたノードを取得する。 let location = touches.first!.location(in: self) let node = atPoint(location) as! SKSpriteNode //タッチしたノードが丸太の場合、ターゲットノードに設定する。 if(node == maruta1 || node == maruta2){ target = node } } //画面タッチ移動時の呼び出しメソッド override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) { //タッチした座標を取得する。 let location = touches.first!.location(in: self) //ターゲットノードをタッチした座標まで移動するアクションを実行する。 let action = SKAction.move(to: CGPoint(x:location.x, y:location.y), duration:0.1) target.run(action) } //1フレームごとの呼び出しメソッド override func update(_ currentTime: TimeInterval) { //パスを作成する。 let path = CGMutablePath() //丸太に配置した空ノードを取得する。 let linePoint1 = self.childNode(withName: "//linepoint1") let linePoint2 = self.childNode(withName: "//linepoint2") //空ノードの座標をシーンを基準にしたものに変換する。 let position1 = convert(self.position, from: linePoint1!) let position2 = convert(self.position, from: linePoint2!) //線を引くパスを設定する。 path.move(to: CGPoint(x: position1.x, y: position1.y)) path.addLine(to: CGPoint(x: position2.x, y: position2.y)) //線の描画を更新する。 line.path = path } } |
下図のように丸太の子に空ノードを配置し、空ノードと空ノードの間に線を引いた。
丸太のアンカーポイントを丸太の端に移動してアンカーポイント間に線を引くと丸太が回らなくなってしまうためこの方法をとっている。空ノードを使わずに、計算などを使った上手い方法があるような気がする。
以下は実際のプレイ動画。
maxLengthプロパティで丸太間の最大距離を設定できる。設定しない場合は、プレイ開始時の丸太間の距離が最大距離になる。
1 2 3 |
//物理ボディ間の最大距離を設定する。 joint.maxLength = 150 |
以下は実際のプレイ動画。150ポイント以上離れたら丸太がついてくるようになった。