Blog
ゲームのユーザに合わせた広告画像を生成して配信する仕組み
この記事は、CyberAgent Developers Advent Calendar 2016 12日目の記事です。
アドテクスタジオ Dynalystの黒崎です。
昨年は新卒とか初心者というパワーワードをお借りしてDockerの記事を書きました。(ネットワーク初心者の新卒がDockerでネットワークの勉強をしてみた)
が、2年目なのでもうその手は通用しなくなりました。。
今年はDynalystが新しくリリースした機能の仕組みを紹介させて頂きます。
これはDynalystのDynamic Creative配信機能で生成された広告のイメージです。
中央の画像のみ取り出すと、こんな画像が表示されています。
広告にゲームのステータスが表示されていて、達成すればどんな報酬が貰えるのかがわかりますね。
もちろんユーザが違えばそのユーザの状態に合わせた別の広告が表示されます。
要するに、ユーザに合わせて表示される広告の画像が変化するわけです。
Dynalystとは
スマートフォンゲームアプリを利用しているユーザーのプレイ状況に合わせて、一人ひとりに最適な広告を自動生成し広告配信します。
例えば、ゲームが進まずやめてしまいそうなユーザーが、プレイを進めるために必要なアイテムが手に入るガチャを引けるチケットを5枚持っていた場合、そのことを広告に反映し情報を届けることで、アプリ利用やゲーム内のアクションを促進することが可能になります。
全体構成
Dynamic Creative生成機能を実現するために必要な構成は以下のようになっています。
Dynalystではサーバで動いているアプリケーションはほぼ全てScalaで書いています。Scalaはいいぞ。
各機能について
ユーザ情報更新
広告主と連携し、配信に必要なゲームのユーザの情報が更新されるとKinesisのストリームに更新されたデータが流れてきます。
※ 本記事で言う「ユーザ」とは具体的にはIDFAやAdIDといった広告用のIDで識別される端末の事を指します。
画像生成
広告はhtmlで配信するのだから、画像生成せずにSVG等を用いて画像の組み立てをすれば良いのでは?という疑問が浮かぶ方も居るのではないかと思いますが、今回の用途では画像を生成する必要がありました。ネイティブ広告にも対応する必要があるからです。
ネイティブ広告では、ロゴ画像、タイトル、テキスト、CTA(Call to Action)、メイン画像などのパーツを渡すとメディア側でレイアウトに合った配置がされます。
Dynalyst側からは広告に必要なパーツを渡すだけでhtmlでカスタマイズする事はできないので、画像に埋め込んでレンダリングしてしまうことで表現しています。
画像の合成
Kinesisストリームから更新されたユーザ情報を取り出し、その情報を元に画像を合成していきます。
先程の画像だと必要な画像素材は以下の3つです。
これらを合成することで以下のような1枚の画像が出来上がります。
JSONの生成方法次第で様々なパターンの画像生成に応用可能です。
画像生成後にRedisに画像生成済であることが分かるように記録しておきます。
Scalaで画像生成して面倒だったこと
ScalaはJVM言語なのでJavaのGraphics2Dを使って画像の生成をしていました。
このあたりで面倒だった事がいくつかあったので紹介します。
テキストのアウトライン描画
アウトラインとは、文字色と背景色が似ている場合に字を目立たせるためにふちに線を描画するやつです。
標準ではこの機能がなかったため、自前で実装しました。
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 |
def renderTextBox(textBox: TextBox, g: Graphics2D): Unit = { val fontRenderContext = g.getFontRenderContext textBox.lines.zipWithIndex.foreach { case(line, index) => val layout = new TextLayout(line, textBox.font, fontRenderContext) val shape = layout.getOutline(null) val textTransform = new AffineTransform() textTransform.translate(textBox.x, textBox.y + layout.getAscent + textBox.lineHeight * index) g.setTransform(textTransform) g.setColor(textBox.color) g.fill(shape) (textBox.outlineWidth, textBox.outlineColor) match { case(Some(w), Some(c)) => { g.setStroke(new BasicStroke(w)) g.setColor(c) g.draw(shape) } case _ => } } g.setTransform(new AffineTransform()) g.setStroke(new BasicStroke()) } |
透過情報付き画像がJpegに書き出せない
背景画像にアイコン画像などを合成する場合、アイコン画像は透過付きにしないとかっこ悪くなってしまいます。
なので内部的には透過情報付きの画像を配信用にJPEGで書き出す事になります。
そのまま書き出そうとするとエラーになってしまうので、透過なしの画像バッファを用意してそこに生成済画像をコピーするという方法を取りました。
1 2 3 4 5 6 7 8 9 |
val outputImage = new BufferedImage(image.getWidth, image.getHeight, BufferedImage.TYPE_INT_RGB) val g = outputImage.getGraphics g.drawImage(image, 0, 0, null) g.dispose val bout = new ByteArrayOutputStream ImageIO.write(outputImage, "JPEG", bout) bout.toByteArray |
メモリ使用量が気になるところですが、実際に動かすサーバはメモリよりCPUマターだったので今回はそこまで気にしていません。
JREによって日本語/中国語/韓国語が描画できない
フォントは別途サーバにインストールしたものを使って描画するようにしたのですが、いわゆるCJKフォントと言われるものがOracleJREだとうまく扱えず、あれこれ悩んだ結果、OpenJDKのJREなら扱えそうだという事がわかり、急きょOracleJREからOpenJDKのJREに入れ替えるといった対応をしました。
原因究明に手間取ったのは手元開発環境ではOpenJDKを使っていて、ステージング/本番環境ではOracle JREが入っていたためでした。開発環境とステージング/本番環境の構成をできるだけ近づけておくことは大事です…。
配信
生成した画像はCloud Front(CDN)でキャッシュしたものが配信されます。
どうやって開発したのか
「ゲームのユーザに合わせて画像を生成し配信する」というアイデアが固まったあと設計から開発、リリースまでの期間は1ヶ月程度とかなり短い期間でした。
期間は短かったものの、アイデアをもとにこの構成の設計に落ち着くまで、何度も議論を重ねました。
マイクロサービスが流行っていますが、Dynalystでは特に細かく分割する意識はしていなくて、必要な粒度で分割するという姿勢でいます。
周辺のアプリケーションと合わせて、アプリケーションごとに人を分担して開発するというような流れで進めました。
さいごに
ユーザに合わせた画像を動的に生成するお話でした。
おおよそこのような構成でリリースされ、現在順調に稼働中ですが、この構成が最適だとは思ってはおらず、今後この仕組を利用した配信が拡大しこの構成では生成が間に合わない場合が出てくるかもしれませんが、その時は必要に応じて作り変えて行くつもりです。
先日のAWSのre:InventでElastic GPUが発表され、GPU利用の障壁が下がったのでGPUを利用して画像生成をやってみるのもありかもしれませんね。
Dynalystでは他にも新しいゲーム特化の配信機能を開発中です。
明日は @k_enoki さんです。
昨年のCyberAgentエンジニア アドベントカレンダーでも @k_enoki さんの前日だったような気がします。偶然ですね。
Author