1.はじめに

はじめまして。AppStudioでエンジニアをやっています石川です。
私は4月までサーバーサイドエンジニアとしてサービスの開発、運営を行っていました。
今回はそんな私がサーバーサイドエンジニアから転向してスマホアプリ開発を始めて1ヶ月でリリースしたアプリのことについて書こうと思います。

2.moimoについて

今回リリースしたアプリは「moimo」というジェスチャーゲームで、複数人で遊ぶパーティーゲームとなっています。
プレイ中はスマホのインカメラを使って動画を撮影しており、ゲーム終了後に保存したりSNSにシェアしたりできるようになっています。
企画は4月の半ばくらいからスタートして、実際に実装を始めたのが4月の終わり頃、リリースしたのが5月の終わり頃とおよそ1ヶ月の開発期間でどうにかリリースしました。
moimoはSwiftで実装しており、iOSでの提供のみとなっています。
App Store https://itunes.apple.com/us/app/moim/id1234477224?ls=1&mt=8
AppStore

3.開発体制

moimoの開発はプロデューサー1人、デザーナー1人、エンジニア1人の3人体制で、スマホアプリ開発はほぼ初心者というメンバーで進めました。
デザインの共有には「Zeplin」というツールを利用しましたが、お題の追加や画像の差し替えはプロデューサーとデザイナーが直接リポジトリにプッシュする方式で進めました。
直接リポジトリにプッシュしてもらう方式は3人体制という少人数だからこそ特に競合する心配もないため、最低限のGitの知識のみで実現することができたのだと思います。

4.機能仕様

トップメニュー

アプリを起動させて最初(初回のチュートリアルを除く)に表示されるメニュー画面は、リリース前のものを含めると3度変更されています。
左から、リリース前、v1.0とv1.1、v1.2以降のメニュー画面です。
リリース前 v1.0とv1.1 v1.2以降

v1.1までのメニュー画面

v1.1までのメニュー画面(左側2つ)は大きく分けて3枚のビューで構成されています。
一番下に遊び方のビューを表示し、その上にbasicとspecialのカテゴリのビューを重ねて表示しています。
その2つのビューを上下にスライドさせることでカードを引き出したような動きを表現しています。
ビューの構成

動画合成

moimoの開発で一番実装に時間がかかったのが動画合成でした。
合成の要件としては、

  • moimoのロゴを入れる
  • 挑戦しているお題を入れる
  • クリア、パスした場合はその表示を入れる
  • タイムアップの表示を入れる

はじめはゲーム終了後に撮影した動画を合成する方法を試してみました。
しかし、moimoのロゴを合成するだけで15秒もかかってしまいました。
また、挑戦しているお題を入れるためには、どの秒数のときにどのお題に挑戦していたかを保持しておく必要があり、この方法で実現するのは現実的ではないと判断しました。
その後、試行錯誤した結果、AVCaptureVideoDataOutputを利用すれば、動画を撮影しながら合成を行うことが可能となり、撮影後に合成時間を要してしまう問題と、挑戦しているお題をどうやって保持しておくのかの問題を解決することができました。
では、どのようにしてリアルタイム合成を行っているかですが、このクラスはフレーム毎に映像を取得できるため、その取得した映像に対して以下の順に処理を行って合成をしています。

  1. CMSampleBufferをUIImageに変換
  2. 変換したUIImageを加工して新しいUIImageを作成
  3. 作成したUIImageをCVPixelBufferに変換

// CMSampleBufferをUIImageに変換
func uiImageFromCMSampleBuffer(buffer: CMSampleBuffer) -> UIImage {
    let pixelBuffer: CVPixelBuffer = CMSampleBufferGetImageBuffer(buffer)!
    let ciImage: CIImage = CIImage(cvPixelBuffer: pixelBuffer)
    let image: UIImage = UIImage(ciImage: ciImage)
    return image
}

// 変換したUIImageを加工して新しいUIImageを作成
func synthesis(image: UIImage) -> UIImage {
    // 合成処理    

    return newImage
}

// 作成したUIImageをCVPixelBufferに変換
func pixelBufferFromUIImage(image: UIImage) -> CVPixelBuffer {
    let cgImage: CGImage = image.cgImage

    let options = [
        kCVPixelBufferCGImageCompatibilityKey as String: true,
        kCVPixelBufferCGBitmapContextCompatibilityKey as String: true
    ]

    var pxBuffer: CVPixelBuffer? = nil

    let width: Int = cgImage.width
    let height: Int = cgImage.height

    CVPixelBufferCreate(
        kCFAllocatorDefault,
        width,
        height,
        kCVPixelFormatType_32ARGB,
        options as CFDictionary?,
        &pxBuffer
    )

    CVPixelBufferLockBaseAddress(pxBuffer!, CVPixelBufferLockFlags(rawValue: 0))

    let pxData: UnsafeMutableRawPointer = CVPixelBufferGetBaseAddress(pxBuffer!)!

    let bitsPerComponent: size_t = 8
    let bytePerRow: size_t = 4 * width

    let rgbColorSpace: CGColorSpace = CGColorSpaceCreateDeviceRGB()
    let context: CGContext = CGContext(
        data: pxData,
        width: width,
        height: height,
        bitsPerComponent: bitsPerComponent,
        bytesPerRow: bytePerRow,
        space: rgbColorSpace,
        bitmapInfo: CGImageAlphaInfo.noneSkipFirst.rawValue
    )!

    context.draw(cgImage, in: CGRect(x: 0, y: 0, width: CGFloat(width), height: CGFloat(height)))

    CVPixelBufferUnlockBaseAddress(pxBuffer!, CVPixelBufferLockFlags(rawValue: 0))

    return pxBuffer!
}

5.さいごに

サーバーサイドから転向して新しい挑戦を始めたばかりで、実装に時間がかかったり設計が拙かったりと周りに迷惑ばかりかけてしまっていますが、今回moimoを短期間でリリースしたことは今後他のアプリを開発する上で非常に大きな経験となりました。
今後も少人数での開発が続くので、サーバーサイドも担当できるエンジニアとして技術面で貢献していきたいと思います。

Masayuki Ishikawa
2012年新卒入社。サーバーサイドエンジニアとして、ピグアイランド、ピグライフの開発を担当。現在はAppStuidoにてiOS/Androidのアプリ開発をやってます。