Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Features
Speaker Deck
PRO
Sign in
Sign up for free
Search
Search
(ほぼ)標準ライブラリだけでスロットゲームを実装した話
Search
Tomohiro Imaizumi
August 24, 2018
Programming
1
2k
(ほぼ)標準ライブラリだけでスロットゲームを実装した話
ROPPONGI.swift #5 での登壇資料です。
Tomohiro Imaizumi
August 24, 2018
Tweet
Share
More Decks by Tomohiro Imaizumi
See All by Tomohiro Imaizumi
Feature Flagを使った開発で高速かつストレスフリーなデリバリーを実現する / Fast and stress-free delivery with Feature Flag-based development
imaizume
2
3.5k
プロダクトグロースと技術のベースアップを両立させるRettyのアプリ開発スタイル / Achieve Product Growth and Tech Update
imaizume
1
750
git branchを自由に操れるようになろう / Let's Play with Git branch!
imaizume
2
1.1k
スナップショットテスト実戦投入 / Practical Snapshot Testing
imaizume
13
7k
コーディング以外のエンジニアリング / About Engineering Without Coding
imaizume
1
1.7k
Firebase Remote Configの運用で知ったこと・知っておくと良いこと / Things I Learned from Operation of Firebase Remote Config
imaizume
6
3.8k
iOSアプリのテストを書きたいのに書けないあなたへ / How You Should Start to Write Your First Unit Test for iOS
imaizume
6
4.9k
循環的複雑度を上げないためのSwiftプログラミングTips / Tips of Swift Programming to Reduce Code Complexity
imaizume
11
7.9k
シングルトンではじめる状態管理と依存注入 / A way to control state using singleton pattern
imaizume
0
4.7k
Other Decks in Programming
See All in Programming
のびしろを広げる巻き込まれ力:偶然を活かすキャリアの作り方/oso2024
takahashiikki
1
400
Generative AI Use Cases JP (略称:GenU)奮闘記
hideg
0
150
gopls を改造したら開発生産性が高まった
satorunooshie
8
240
ECS Service Connectのこれまでのアップデートと今後のRoadmapを見てみる
tkikuc
2
210
From Subtype Polymorphism To Typeclass-based Ad hoc Polymorphism- An Example
philipschwarz
PRO
0
160
Piniaの現状と今後
waka292
5
1.4k
Vaporモードを大規模サービスに最速導入して学びを共有する
kazukishimamoto
4
4.3k
Kotlin2でdataクラスの copyメソッドを禁止する/Data class copy function to have the same visibility as constructor
eichisanden
1
120
PagerDuty を軸にした On-Call 構築と運用課題の解決 / PagerDuty Japan Community Meetup 4
horimislime
1
110
Kubernetes for Data Engineers: Building Scalable, Reliable Data Pipelines
sucitw
1
190
Realtime API 入門
riofujimon
0
100
Progressive Web Apps für Desktop und Mobile mit Angular (Hands-on)
christianliebel
PRO
0
110
Featured
See All Featured
Designing the Hi-DPI Web
ddemaree
280
34k
CSS Pre-Processors: Stylus, Less & Sass
bermonpainter
355
29k
We Have a Design System, Now What?
morganepeng
50
7.2k
Bootstrapping a Software Product
garrettdimon
PRO
305
110k
Speed Design
sergeychernyshev
24
570
Thoughts on Productivity
jonyablonski
67
4.3k
Teambox: Starting and Learning
jrom
132
8.7k
Responsive Adventures: Dirty Tricks From The Dark Corners of Front-End
smashingmag
250
21k
Building a Scalable Design System with Sketch
lauravandoore
459
33k
It's Worth the Effort
3n
183
27k
Code Reviewing Like a Champion
maltzj
519
39k
The MySQL Ecosystem @ GitHub 2015
samlambert
250
12k
Transcript
(΄΅)ඪ४ϥΠϒϥϦ͚ͩͰ εϩοτήʔϜΛ࣮ͨ͠ ROPPONGI.swift #5 @imaizume
今泉 智博 @imaizume 2017年株式会社ミクシィ新卒入社 株式会社 所属 iOS利用経験0から iOS版開発担当に (ほぼ)毎日健康COMP生活はもうすぐ1周年
1. 背景 2. スロットの仕様 3. スロット実装の解説とDEMO 4. 実装過程での課題と解決方法 5. 将来的な話
ຊͷINDEX
夏はマッチングアプリが盛り上がる大切な時期 Poiboyも夏にキャンペーンを実施 今年の企画: スロットゲームで豪華賞品プレゼント എܠ 実は昨年やるはずがビジネス的事情により1年寝かされていました 詳細は↓の登壇&インタビューを御覧ください https://mixil.mixi.co.jp/people/2103
(1)Symbol: 絵柄1つのこと (2)Reel: 絵柄のパターンの1セット (3)ReelSet: 複数リールのセット (imaizumeオリジナル用語) (4)Drum: スロットの中の1レーン (右の図では3ドラム)
ຊ…ͷલʹεϩοτήʔϜͷ༻ޠղઆ ギャンブルに詳しくない方のために このあとの説明で必要になるので (1) (2) (3) スロットの例 (4)
✓ ゲームの開始はユーザータイミング ✓ 当たり判定はゲーム開始時に決定済み ✓ 3つのドラムは独立して回転 ✓ 回転数不明=無限スクロール ✓ 適度な回転スピードと停止時の加速度
✓ 目標となる絵柄を各ドラムの中心で停止 ✓ 賞品ポップアップを全ドラムの停止後に表示 ✓ Auto Layout対応必須 ՆΩϟϯϖʔϯͷεϩοτήʔϜͷ༷ あなたならどうやってスロットを実装しますか❓ (※࣌SwiftΛॻ͖࢝Ίͯ4 ϲ݄
1: SpriteKitで絵柄単体を動かす ❌ 速度を変えたときに絵柄間の間隔が変わる (渋滞の車列みたいになる) 2: UICollectionView / UITableView 絵柄の余白調整や中心での停止が難しい
cellのreuseによる表示のバグも懸念 3. UIScrollView ⭕ 一番自由度が高く良さそう 中心で止められそう ͕ࣗߟ࣮͑ͨͷީิ (※લड़ͷొஃࢿྉͰUICollectionViewΛͬͨͱ͋ΔͷޡΓͰ͢ )
1.絵柄に対応するUIViewを作成 2.絵柄を連ねてReelを作成 3.複数のReel群 = ReelSetを縦につなげる 4.ReelSetをUIScrollViewに入れDrumにする Ͳ͏࣮ͬͯݱ͔ͨ͠(View) Symbol Reel ReelSet
Drum1 可視領域
࣮ࡍͷView֊
1.遠くの座標にcontentOffsetを設定 2.下に向かって自動スクロールで下端到達 3.上部の同絵柄にジャンプ 4.1~3を繰り返す Ͳ͏࣮ͬͯݱ͔ͨ͠(ಈ͖) setContentOffset (y: 5000) ~~~ 2.下端に到達
3.同じ絵柄にジャンプ drumScrollView. setContentOffset (500) すぐにゴールを 遠くに戻す y=5000 y=0 1.offsetを遠くに
ίϯϙʔωϯτͷਤ DrumScrollView: UIScrollView enum Symbol: Int symbol: [Symbol] numberOfReel: Int
symbolViews:[UIView] drum: DrumScrollView ✕ drumSet: [DrumScrollView] numberOfDrum: Int ✕
DrumScrollView(ύϥϝʔλઃఆ) var drumParam : DrumParam = DrumParam() { didSet {
// MARK: View設定 self.subviews.forEach({ $0.removeFromSuperview() }) self.contentSize = CGSize( width: self.frame.width, height: (CGFloat)(self.drumParam.numberOfSymbol) * self.symbolHeight) for (index, view) in self.drumParam.totalSetOfSymbolImageView.enumerated() { view.frame = CGRect( x: 0, y: (CGFloat)(index) * self.symbolHeight, width: self.frame.width, height: self.symbolHeight ) self.addSubview(view) } self.setNeedsLayout() self.layoutIfNeeded() // MARK: ゲーム設定 self.remainingScrollCount = self.drumParam.numberToScroll // 開始時のOffsetをランダムにずらず let startIndex: UInt32 = arc4random_uniform(UInt32(self.drumParam.numberOfReel)) self.contentOffset.y = ((CGFloat)(startIndex) + 0.5) * self.symbolHeight // 停止位置座標の決定 var targetY: CGFloat = self.symbolHeight * ((CGFloat)(self.drumParam.targetIndex) + 0.5) - self.frame.height / 2 // 頭のリールは切れてしまうので前半のリールは後半の後ろに持っていく if self.drumParam.targetIndex < self.drumParam.numberOfReel / 2 { targetY += (CGFloat)(self.drumParam.numberOfReel) * reelHeight } self.stopTargetPoint = targetY } } 縦にSymbolをつなげる contentViewの領域確保 ちょっとした気遣い パラメータ用Structを定義
DumScrollView(͖͍͠ઃఆ) /// 残り回転数 fileprivate var remainingScrollCount: Int = 0 ///
回転を減速するべきか fileprivate var shouldSlowDownScroll: Bool { let isNearTheTarget = abs(self.contentOffset.y - self.stopTargetPoint) < self.scrollTargetDistanceForSlowSpeed let isLastScroll = self.remainingScrollCount == 0 return isNearTheTarget && isLastScroll } /// 回転を停止するべきか fileprivate var shouldFinishScroll: Bool { let isOnTheTarget = abs(self.contentOffset.y - self.stopTargetPoint) < self.bufferForTargetPosition let isLastScroll = self.remainingScrollCount == 0 return isOnTheTarget && isLastScroll } /// 回転位置を戻すべきか fileprivate var shouldRewindScroll: Bool { return self.contentOffset.y <= self.reelHeight } fileprivate var stopTargetPoint: CGFloat = 0 /// 停止位置ちょうどに止めることはできないため許容される停止位置までの誤差 fileprivate let bufferForTargetPosition: CGFloat = 10.0 /// 通常回転時のスクロール目標位置 fileprivate let scrollTargetDistanceForNormalSpeed : CGFloat = 5000 /// 減速時のスクロール目標位置 fileprivate let scrollTargetDistanceForSlowSpeed : CGFloat = 100 停止までの回転数(5とか) 停止/減速までのバッファを作るのがポイント POINT: 停止位置にはバッファ(ズレの許容度)を与える 回転速度が早すぎるとバッファを通過してしまう ズレは小さくしたいので直前で減速させる
DrumScrollView(ಈ࡞ઃఆ) func updateViews() { if self.remainingScrollCount < 0 { return
} if self.shouldFinishScroll { // ఀࢭ࣌ self.setContentOffset(CGPoint(x: 0, y: self.stopTargetPoint), animated: true) self.gameFinishCallback() } else if self.shouldSlowDownScroll { // ݮ࣌ let scrollTargetPoint = -(100 + self.contentOffset.y) self.setContentOffset(CGPoint(x: 0, y: scrollTargetPoint), animated: !self.shouldRewindScroll) return } else if self.shouldRewindScroll { // ্ʹୡͨ࣌͠ let scrollTargetPoint = (self.contentSize.height / (CGFloat)(self.drumParam.numberOfReelSet) * 2) + self.contentOffset.y self.decreaseScrollCount() self.setContentOffset(CGPoint(x: 0, y: scrollTargetPoint), animated: !self.shouldRewindScroll) } else { // ௨ৗͷճస࣌ let scrollTargetPoint = -(5000 + self.contentOffset.y) self.setContentOffset(CGPoint(x: 0, y: scrollTargetPoint), animated: !self.shouldRewindScroll) } } 停止後に呼ぶコール バック後で解説
❇とりあえず時間をもらった (いろいろ試すしかない) チームの理解も得られていろいろとトライさせてくれた ໘ͨ͠՝ͱղܾํ๏ 1. ࣾ֎Θͣલྫ͕ͳ͍ 力技(ひたすらdebugポイントとprintを埋め込み) タイミングよくrevealを教えてもらったのはラッキー SymbolやDrumに処理を移譲してカプセル化 2.
ಈ͖ͷσόοά͕େม Offsetや回転スピードの関係を確認するため 値を計測してグラフ化したりもしました ➡
໘ͨ͠՝ͱղܾํ๏ UIScollViewはスクロールスピードを指定できない 現在のOffset + αにsetContentOffsetで定速スクロール 3. ఀࢭݮ࣌ͷՃௐ offset: 100 drumScrollView.
setContentOffset (100 + 200) offset: 100 drumScrollView. setContentOffset (100 + 1000) offset: 120 drumScrollView. setContentOffset (120 + 200) offset: 200 drumScrollView. setContentOffset (200 + 1000)
ここだけPromiseKitを使いました 各Drumの停止時コールバックでfulfillをcall 3つのDrumの停止後に当選ポップアップを表示 (当選結果自体はViewController側で保持) let games = self.drumScrollViews.map { drum
in return Promise<Void> { (fullfil, _) in drum.gameFinishCallback = { fullfil(()) } } } // MEMO: 当選ポップアップの表示 when(resolved: games).always { self.showWinPopup() } ໘ͨ͠՝ͱղܾํ๏ 4. ಠཱͨ͠ճసͷऴྃͷͪ߹Θͤ
কདྷతͳ ϥΠϒϥϦԽ͍ͨ͠ ‣ 今回はゲーム開始前に結果が決まっていた ‣ 本来は動的に停止位置や速度調整をする必要あり ‣ Viewの制約で上端や下端では止められない ‣ 横スクロールにすれば回転寿司みたいなUIも作れそう?
PromiseKitͷษڧձΛͦͷ͏ͪΓ·͢ ‣ 今回の実装でPromiseKitをうまく扱えるようになった ‣ コールバック直書きのコードもいい感じに書き換えた ‣ 現在勉強会企画中 (SupporterZ CoLabを予定)
ࢀߟࢿྉ 新規サービスのアプリ開発で経験したリアルな出来事 https://speakerdeck.com/imaizume/xin-gui-sabisufalseapurikai-fa-dejing- yan-sitariarunachu-lai-shi 失敗こそ学びの力に!貪欲にチャレンジし続ける ~新卒1年 目 成長の軌跡~#5|ミクシル https://mixil.mixi.co.jp/people/2103 mxcl/PromiseKit:
Promises for Swift & ObjC https://github.com/mxcl/PromiseKit