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.7k
プロダクトグロースと技術のベースアップを両立させるRettyのアプリ開発スタイル / Achieve Product Growth and Tech Update
imaizume
1
770
git branchを自由に操れるようになろう / Let's Play with Git branch!
imaizume
2
1.2k
スナップショットテスト実戦投入 / Practical Snapshot Testing
imaizume
13
7.1k
コーディング以外のエンジニアリング / About Engineering Without Coding
imaizume
1
1.7k
Firebase Remote Configの運用で知ったこと・知っておくと良いこと / Things I Learned from Operation of Firebase Remote Config
imaizume
6
4k
iOSアプリのテストを書きたいのに書けないあなたへ / How You Should Start to Write Your First Unit Test for iOS
imaizume
6
5k
循環的複雑度を上げないためのSwiftプログラミングTips / Tips of Swift Programming to Reduce Code Complexity
imaizume
11
8.1k
シングルトンではじめる状態管理と依存注入 / A way to control state using singleton pattern
imaizume
0
4.8k
Other Decks in Programming
See All in Programming
PicoRubyと暮らす、シェアハウスハック
ryosk7
0
220
Асинхронность неизбежна: как мы проектировали сервис уведомлений
lamodatech
0
1.3k
rails newと同時に型を書く
aki19035vc
5
710
技術的負債と向き合うカイゼン活動を1年続けて分かった "持続可能" なプロダクト開発
yuichiro_serita
0
300
ESLintプラグインを使用してCDKのセオリーを適用する
yamanashi_ren01
2
240
「とりあえず動く」コードはよい、「読みやすい」コードはもっとよい / Code that 'just works' is good, but code that is 'readable' is even better.
mkmk884
6
1.4k
見えないメモリを観測する: PHP 8.4 `pg_result_memory_size()` とSQL結果のメモリ管理
kentaroutakeda
0
940
DMMオンラインサロンアプリのSwift化
hayatan
0
190
オニオンアーキテクチャを使って、 Unityと.NETでコードを共有する
soi013
0
370
Jaspr Dart Web Framework 박제창 @Devfest 2024
itsmedreamwalker
0
150
為你自己學 Python
eddie
0
520
2025.01.17_Sansan × DMM.swift
riofujimon
2
560
Featured
See All Featured
Let's Do A Bunch of Simple Stuff to Make Websites Faster
chriscoyier
507
140k
Why You Should Never Use an ORM
jnunemaker
PRO
54
9.1k
The Psychology of Web Performance [Beyond Tellerrand 2023]
tammyeverts
45
2.3k
A better future with KSS
kneath
238
17k
The Cost Of JavaScript in 2023
addyosmani
46
7.2k
Principles of Awesome APIs and How to Build Them.
keavy
126
17k
Making the Leap to Tech Lead
cromwellryan
133
9k
The Pragmatic Product Professional
lauravandoore
32
6.4k
The Illustrated Children's Guide to Kubernetes
chrisshort
48
49k
No one is an island. Learnings from fostering a developers community.
thoeni
19
3.1k
What’s in a name? Adding method to the madness
productmarketing
PRO
22
3.2k
It's Worth the Effort
3n
183
28k
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