AppleのSpriteKitをAndroidネイティブで使う ― Kotlinライブラリ「SpriteKit」を公開しました
シーングラフ、物理演算、パーティクル、タイルマップといった2Dゲームエンジンの定番機能を、AppleのSpriteKitと同じAPIでAndroidネイティブに実装したオープンソースのKotlinライブラリ「SpriteKit」を公開しました。あわせて、以前公開した「GameplayKit」との橋渡しを行うライブラリ「GKSKBridge」、そして両方を実際に組み合わせて動かすサンプル「Tic-Tac-Toe」も公開しています。この記事ではそれぞれの役割と使い方、そしてApple公式のSpriteKitと比べた場合の対応範囲・制限事項を紹介します。
- リポジトリ: https://github.com/bitzgroup/SpriteKit
- 関連リポジトリ(GKSKBridge): https://github.com/bitzgroup/GKSKBridge
- 関連リポジトリ(Tic-Tac-Toe サンプル): https://github.com/bitzgroup/tic-tac-toe
- 関連リポジトリ(GameplayKit): https://github.com/bitzgroup/GameplayKit
- 製品ページ: https://www.bitz.co.jp/products.html
目次
- SpriteKitとは何か
- 何ができるか
- 対応範囲と制限
- GameplayKitとの橋渡し:GKSKBridge
- 使ってみる
- サンプル:Tic-Tac-Toeで組み合わせて動かす
- 現在の状況
- まとめ
- 参考資料
SpriteKitとは何か
SpriteKitは、Appleが2Dゲーム向けに提供しているフレームワークです。描画対象をノードとして階層的に管理するシーングラフを中心に、スプライトやシェイプ、ラベルの描画、アクションによるアニメーション、物理演算、パーティクル、タイルマップなど、2Dゲームに必要な機能をひとまとめに提供します。
今回公開した「SpriteKit」ライブラリは、このSpriteKitのAPI構成をそのままAndroid・Kotlinに移植したものです。クラス名や役割はAppleのSpriteKitに揃えつつ、実装はKotlinらしい書き方に直しています。以前公開した「GameplayKit」(ゲームAIのアルゴリズム群)と組み合わせることで、Androidネイティブだけで「ゲームロジック」と「2D描画」の両方をAppleと同じ設計で扱えるようになります。
何ができるか
現時点で、シーングラフから入力処理・オーディオ・シェーダーまで一通りの機能が実装されています。
| 機能 | 代表的なクラス | 用途 |
|---|---|---|
| シーン・ノード |
SKScene, SKNode
|
描画対象を階層構造で管理する |
| スプライト・シェイプ・ラベル |
SKSpriteNode, SKShapeNode, SKLabelNode
|
テクスチャ・図形・テキストの表示 |
| アクション | SKAction |
移動・回転・拡大縮小などのアニメーション |
| 物理演算 |
SKPhysicsBody, SKPhysicsWorld
|
重力・衝突判定などの2D物理 |
| パーティクル | SKEmitterNode |
爆発や煙などの粒子エフェクト |
| タイルマップ | SKTileMapNode |
タイルベースの2Dマップ表現 |
| カメラ・効果・制約 |
SKCameraNode, SKEffectNode, SKConstraint
|
画面追従やエフェクト、ノードの拘束 |
| 入力・トランジション | タッチ入力, SKTransition
|
タッチ操作の処理とシーン切り替え |
| オーディオ・シェーダー |
SKAudioNode, SKShader
|
音声再生とカスタムシェーダー |
例1: シーンとノード
val scene = SKScene(size = Vector2(1080f, 1920f)).apply {
scaleMode = SKSceneScaleMode.AspectFit
backgroundColor = Color.BLACK
}
val player = SKNode().apply { position = Vector2(scene.size.x / 2f, 200f) }
scene.addChild(player)
skView.presentScene(scene)
例2: スプライト・シェイプ・ラベル
val ship = SKSpriteNode(texture = shipTexture, size = Vector2(64f, 64f))
val marker = SKShapeNode(path = circlePath).apply {
fillColor = Color.CYAN
strokeColor = Color.WHITE
lineWidth = 4f
}
val scoreLabel = SKLabelNode(text = "Score: 0").apply {
fontSize = 48f
fontColor = Color.WHITE
}
scene.addChild(ship)
scene.addChild(marker)
scene.addChild(scoreLabel)
対応範囲と制限
このライブラリは、Apple公式のSpriteKitを一対一で丸ごと移植したものではありません。Xcodeのビルド時処理やCore Image、Metalなど、Androidに直接対応するものがない機能は移植の対象外としています。また、実装済みの機能でも、細部の挙動がAppleと異なる箇所や、簡略化して実装している箇所があります。
移植の対象外としている主な機能
-
SKVideoNode(動画テクスチャの再生) -
SKLightNode(法線マップによるライティング・シェーダー依存の陰影) -
SKReferenceNode(.sksシーンアーカイブの読み込み。Android に相当する形式がない) -
SKEffectNode.filter(Core Image フィルタ) -
SKAction.follow(_:asOffset:orientToPath:duration:)(パス追従アクション) - パーティクルの
.sksエディタ形式、タイルセットの.sks形式 - 位置的・空間的オーディオ、
SKWarpGeometry(メッシュのワープ描画)
実装はしているが、挙動が簡略化・近似されている主な箇所
- 物理演算:接触点由来のトルクは未対応で衝突応答は線形のみ、ブロードフェーズは
O(n²)、ジョイントの角度制限やreactionForce/reactionTorqueは未実装 - タイルマップ:グリッド形状のみに対応(Appleの等角・六角形タイルは非対応)、行数・列数は生成時に固定
- 入力:タッチはバッチではなく1つずつ配信、ヒットテストは簡易境界ボックスによる判定
- トランジション:
flipHorizontal/flipVerticalなどの3D的な効果は2Dでの近似表現 - シェーダー:
SKSpriteNodeにのみ適用可能で、シェーダーモディファイアの差し込みは非対応
これらはAndroidという異なるプラットフォーム上で、実務で使う範囲を優先して移植した結果です。詳細な差異はリポジトリ内の API_COMPATIBILITY.md、実装済み・対象外の機能一覧は ROADMAP.md にまとめています。導入の際は、自分のプロジェクトで必要な機能が対応範囲に含まれているか、事前にこれらのドキュメントで確認することをおすすめします。
GameplayKitとの橋渡し:GKSKBridge
GameplayKitのエンティティ・コンポーネントと、SpriteKitのシーングラフは、本来それぞれ独立したフレームワークです。エンティティにキャラクターのロジックを持たせても、それだけでは画面に表示されるノードと自動的には結び付きません。「GKSKBridge」は、この2つのライブラリをAndroid上で連携させるためのライブラリです。
主な機能は次の3つです。
-
GKSKNodeComponent:エンティティとシーングラフのノードを双方向に結合します -
GKScene:エンティティとノードをまとめて管理するシーンコンテナです -
GKAgentNodeComponent:GameplayKitのエージェント・ステアリングの計算結果(位置・回転)を、毎フレーム自動的にノードへ反映します
val node = SKNode()
val entity = GKEntity()
entity.addComponent(GKSKNodeComponent(node))
node.entity // == entity, set automatically by GKSKNodeComponent
entity.removeComponent<GKSKNodeComponent>()
node.entity // == null again
val scene =
GKScene().apply {
rootNode = SKScene(size = Vector2(1080f, 1920f))
addEntity(entity)
}
エージェントのステアリングとノードを同期させる例です。
val agent =
GKAgent2D().apply {
maxSpeed = 4f
behavior = GKBehavior.of(GKGoal.toSeekAgent(targetAgent), weight = 1f)
}
entity.addComponent(agent)
entity.addComponent(GKAgentNodeComponent())
class GameScene(size: Vector2) : SKScene(size) {
override fun update(deltaTime: Duration) {
entity.update(deltaTime) // advances the agent's steering simulation
}
override fun didFinishUpdate() {
// このフレームで確定したエージェントの位置・回転を、
// アクションや物理演算のシミュレーション後に各エンティティのノードへコピーする
scene.syncAgentNodes()
}
}
GameplayKitとSpriteKitはそれぞれ独自のVector2型を持っているため、両者を変換するユーティリティも用意されています。
val skPosition = agent.position.toSKVector2()
val gkPosition = node.position.toGKVector2()
使ってみる
GameplayKitはJitPack経由での配信を用意していましたが、SpriteKitとGKSKBridgeは現時点でMaven Central・JitPackへの配信は行っておらず、Gitサブモジュールとしてホストアプリに組み込む形を想定しています。
// settings.gradle.kts
include(":GameplayKit:gameplaykit")
project(":GameplayKit:gameplaykit").projectDir = file("GameplayKit/gameplaykit")
include(":SpriteKit:spritekit", ":SpriteKit:spritekit-compose")
project(":SpriteKit:spritekit").projectDir = file("SpriteKit/spritekit")
project(":SpriteKit:spritekit-compose").projectDir = file("SpriteKit/spritekit-compose")
include(":GKSKBridge:gkskbridge")
project(":GKSKBridge:gkskbridge").projectDir = file("GKSKBridge/gkskbridge")
いずれもminSdk 24、compileSdk / targetSdk 34、Kotlin 2.0+に対応しており、MITライセンスで公開しています。
サンプル:Tic-Tac-Toeで組み合わせて動かす
「Tic-Tac-Toe」は、GameplayKit・SpriteKit・GKSKBridgeを実際に組み合わせて動かす、○×ゲームのサンプルアプリです。同じゲームをiOS版とAndroid版の両方で実装しています。
- iOS版:Apple公式のSpriteKit・GameplayKitで実装
- Android版:今回のbitzgroup/SpriteKit・bitzgroup/GameplayKit・bitzgroup/GKSKBridgeで実装
両者は同じシーングラフ構成、GKGameModel / GKMinmaxStrategist / GKMonteCarloStrategistによる同じAI、GKStateMachineによる同じターン管理で作られており、AndroidのポートがApple公式フレームワークと同等に動作することを確認できるようになっています。ミニマックス法・モンテカルロ法による3段階の難易度、日英ローカライズにも対応しています。
現在の状況
SpriteKitはシーングラフ・スプライト・アクション・物理演算・パーティクル・タイルマップ・カメラ・入力・トランジション・オーディオ・シェーダーまでの実装が完了しています。GKSKBridgeもエンティティ・ノードの結合、シーンコンテナ、エージェント・ステアリングの同期という計画分の機能が実装・テスト済みです。前述の通り、現状はGitサブモジュールとしての組み込みが前提で、パッケージ配信は今後の課題です。
まとめ
GameplayKitに続けてSpriteKitとGKSKBridgeを公開したことで、Androidネイティブのコードだけで「ゲームロジック」と「2D描画」の両方を、Appleの設計に揃えた形で扱えるようになりました。Tic-Tac-Toeは、この3つのライブラリを実際に組み合わせて動かした最小のサンプルとして公開しています。
参考資料
- SpriteKit(Kotlin/Android版)リポジトリ: https://github.com/bitzgroup/SpriteKit
- SpriteKit API差分まとめ: https://github.com/bitzgroup/SpriteKit/blob/main/docs/API_COMPATIBILITY.md
- SpriteKit実装ロードマップ: https://github.com/bitzgroup/SpriteKit/blob/main/docs/ROADMAP.md
- GKSKBridgeリポジトリ: https://github.com/bitzgroup/GKSKBridge
- Tic-Tac-Toeサンプルリポジトリ: https://github.com/bitzgroup/tic-tac-toe
- GameplayKit(Kotlin/Android版)リポジトリ: https://github.com/bitzgroup/GameplayKit
- Apple SpriteKit公式ドキュメント: https://developer.apple.com/documentation/spritekit
- Apple GameplayKit公式ドキュメント: https://developer.apple.com/documentation/gameplaykit
元記事(Bitz Notebook): AppleのSpriteKitをAndroidネイティブで使う ― Kotlinライブラリ「SpriteKit」を公開しました