Quantcast
Channel: Cygames Engineers' Blog
Viewing all 78 articles
Browse latest View live

【CEDEC 2019 フォローアップ】Shadowverseのeスポーツ展開 —ゲームがつなぐコミュニティと地域活性化について—

$
0
0

Cygamesメディアプランナーの松本です。
2019年9月4日(水)に開催されたCEDEC 2019にて、「Shadowverseのeスポーツ展開 —ゲームがつなぐコミュニティと地域活性化について—」と題する講演を行いました。ご参加いただいた皆様には、改めて御礼申し上げます。

講演のポイント

『Shadowverse』のイベントサポート(ES大会)の仕組みや実績について発表しました。私たちは『Shadowverse 』を10年、20年と続くタイトルにしていきたいと考えています。そのためにはこのゲームを「文化」にしていく必要があり、文化は人が作るものであるため、コミュニティ形成が最も大切だと考えています。現在、1カ月で380大会が開かれ、参加者数は3,800人以上。8割以上の都道府県で毎月1回以上イベントが開催されています。定期的なイベント開催をきっかけにして、各地域でコミュニティが生まれています。

様々な”場所”でイベントが開かれています

CEDECでは大阪、北海道、岡山、富山の事例をご紹介しましたが、それ以外にも多数イベントが開かれています。バーやカードショップ、書店、うさぎカフェ、ホテル、コーヒースタンド、レンタルビデオショップなどでも『Shadowverse』のイベントを開催いただいています。

美容院でShadowverse

講演でご紹介した富山のほかにも、美容室で『Shadowverse』の大会が開催されています。ここでは、東京都町田市の「Claw」という美容室でのイベントをご紹介したいと思います。

店長さん自身が『Shadowverse』のプレイヤーということからイベント開催を思いついたそう。大会は勝ち抜きトーナメントで、対戦者は別室でイヤホンを装着し対戦する方式。待機中の方々は待合スペースでパソコンの画面で観戦されていました。

視察に行った際には高校生から大学生の8名の方が参加されており、うち2名が女性でした。また、1名の方は大阪から来られたそうで、ES大会は初参加とのこと。みんなで観戦しながら、「このカードの能力がすごい」「こんな使い方あったのか!」などと盛り上がっており、見ていてとても楽しそうな空間でした。美容院だと座る場所が確保できるので、ゲームをするのにいいのかもしれませんね。

ShadowverseES_01ShadowverseES_02ShadowverseES_03

対面ならではの楽しさ

開催者の方が様々な工夫を凝らし、各地のイベントに特色が出ることで、『Shadowverse』の体験がより良いものとなっています。イベントに参加される皆さんには、対面する緊張感や初めて勝利する高揚感、一手間違えたときの後悔など、人と向かいあう楽しさを体験していたければと思います。

Shadwoverseを使ったイベント開催を
ご検討いただいている皆様へ

少ない人数でも、相手と対面してプレイすることでいつもと違うドキドキ感を味わえます。そんな体験をお一人でも多くの方に届けるために、今後も各地の方と様々な取り組みを行っていきたいと考えております。

『Shadwoverse』を使ったイベントの開催をご検討されている団体や企業様などいらっしゃいましたら、マニュアルなどもご用意しておりますので、ぜひ一度イベントサポートページよりお問い合わせください。


【CEDEC 2019 フォローアップ】AAAタイトル開発における、ハイエンドオーディオ制作技術の研究成果と取り組み事例

$
0
0

Cygames サウンド部 サウンドデザインチームの牧村です。
2019年9月4日(水)~6日(金)に開催されたCEDEC 2019において「AAAタイトル開発における、ハイエンドオーディオ制作技術の研究成果と取り組み事例」という講演を、弊社サウンドデザイナー安井、コンポーザー久保と共におこないました。
ご参加いただいた皆様には、改めて御礼申し上げます。

以下が講演資料となります。

本講演では、以下3項目についての解説・提案をさせていただきました。このエントリーでは、それぞれの概要をお伝えいたします。

  • 「リアルな音場表現の採取とその再現方法」:音の距離減衰や遮蔽を表現する手法に関する研究成果等
  • 「オプション設定のすすめ」:ゲームオーディオのオプション設定に関する提案
  • 「最高の楽曲を生み出すための取り組み」:海外のスタジオを使用した収録での課題と解決案

「リアルな音場表現の採取とその再現方法」

まず、サウンドデザイナーの研究成果として、ゲームサウンドの音場表現における「距離減衰」「遮蔽」「ダイナミックレンジ」といった3要素について、「リアリティの向上」および「開発工程の標準化」という目標を達成するための手法を解説させていただきました。

ゲームにリアリティのある距離減衰を導入するためのアイデアとして、ゲーム上の空間反響(リバーブ)に用いるためのIR(インパルス反応)を現実の空間から採取する際、音源からの距離を変えて複数のサンプルを採取し、それらの変化率をリバーブのウェット成分に反映させる手法を解説しました。

また、遮蔽についても、マイクと音源の間に様々な材質の壁でさえぎった状態でIRをそれぞれ採取し、それらと遮蔽のないIRとの周波数帯域ごとの差分を割り出し、遮蔽処理として利用するという手法を解説しました。

ダイナミックレンジについては、音のdB SPLを実際に計測し、おおよそ現実的な値を設定しつつ、ミドルウェア上のバスルーティングを駆使することにより、リアリティのある音量関係を実現する手法を解説しました。

「オプション設定のすすめ」

昨今のゲーム中のオーディオにまつわるオプション設定項目の、必要性と目指すべき形をお伝えしました。

「パートごとの音量」「音量の初期設定値」「出力媒体への対応」「チャンネル数の設定」「コントローラースピーカーの設定」「ダイナミックレンジの設定」「イコライザーの採用」について、各項の解説と、それぞれミドルウェアを用いた際の導入手法を解説しました。

「最高の楽曲を生み出すための取り組み」

今回ロンドンのアビー・ロード・スタジオで音楽収録を行った際のスタジオ選び、オーケストラの特殊演奏収録、そして現場で感じたことの3つについてお話しさせていただきました。

まずスタジオを選んだ理由についてお伝えしました。本作の世界観とコンセプトを考慮し、「激しめの海外映画サントラに慣れている、豊かな響きと空気感が録れる海外のスタジオ」での収録を目指したのですが、人気のスタジオの予約は埋まっていて、思うようにはいきませんでした。そんな中「challenge(すでに予約が入っている日程の中で、使わない日があれば使わせて欲しいと申し入れすること)」に成功し、数日だけスタジオを空けてもらえたため、アビー・ロード・スタジオで収録する運びとなりました。

次に本作の音楽のコンセプトと、それに沿ったオーケストラの特殊奏法の収録についていくつか例を上げつつご紹介しました。シンセサイザーの音と生のストリングスの音を重ねて新しい音色にしてみる、楽器を叩く奏法やホラーなどでよく使われる奏法を収録するなどの方法で、作品の世界観を演出しつつオリジナリティを出すことを目指しました。

最後に、現場で感じた2つのことについてお話ししました。「クリエイティビティがぶつかり合う雰囲気」について、素晴らしいアイデアが数多く出てくる現場で迅速に判断を下していかなければならない難しさと、現場にいる全員で音楽を一緒に作っている実感をお伝えしました。また「音楽のために、チームのために」と題して今回のコピーイストさんが行っている細かい配慮についてご紹介しました。

さいごに

まず音場表現については、いずれの手法も、特別に高度で先端的なテクノロジーを用いるものではなく、マイクや騒音計さえあれば誰でも再現可能な手法です。CEDECのセッションを受講いただいた皆様に、それぞれのゲーム開発に応用可能な、実利のある知見を提供できたのであれば幸いです。

次にオプション設定については、ユーザーの使用環境に最適な音を提供するとともにユーザーに不便をかけないようにすることを念頭に、ゲームの仕様に合わせ熟慮して用意すべきであることを、皆様にご認識いただきたく存じます。

そして音楽収録については、海外収録への壁は決して低くはありあせんが、実施してみて予想以上の結果と体験ができることを知りました。今後の選択肢の一つとして、少しでも海外での音楽収録を身近に感じていただければ幸いです。

Cygamesでは今後も、新しいことや面白いことにつながる技術やアイデアを追求し、ユーザーに喜んでいただけるコンテンツを創っていきます。ご賛同いただける方は、ぜひご一緒に、世界に通じるタイトルを創りませんか?

詳細はこちらをご覧ください。

【AWS DevDay Tokyo 2019 フォローアップ】Chaos Engineering 〜入門と実例〜

$
0
0

こんにちは、インフラチームの和田です。

10月3日(木)、4日(金)に神田明神ホールで開催されたAWS DevDay Tokyo 2019にて、アマゾン ウェブサービス ジャパン株式会社の畑様とともに登壇し、「Chaos Engineering 〜入門と実例〜」と題するセッションを行いました。前半の畑様のパートではChaos Engineeringの歴史、関連するツール、最新のトレンドを紹介していただき、後半の私のパートでは社内での取り組みと今後どのようなアプローチを考えているかをシェアさせていただきました。
当日お越しいただいた皆さまや中継をご覧になっていた皆さまのChaos Engineeringに対する考え方に、何かしらのインパクトを与えられたのであれば幸いです。

資料は下記のリンク先から確認できます。
[Chaos Engineering 〜入門と実例〜]

セッション終了後に「Ask the speaker」で質問いただいた内容も踏まえて、Q&A形式で補足いたします。

Q: そもそもChaos Engineeringを始めたきっかけは?

2017年頃より新規タイトルのリリース前に耐障害性を高める取り組みをしたことがきっかけとなります。会社設立以後、モバゲー向けのゲーム提供が主軸でしたが、ネイティブアプリの台頭でモバイルゲームのトレンドが変化しました。インフラ環境もオンプレからクラウドへと拡大、幸いなことにユーザー数も増加し、弊社のコンテンツに対する期待値も高くなりました。そういった中でシステムの稼働率が低下することになれば、ユーザーや信頼を失うことにもつながりますし、諸々のオペレーションも発生します。これに対してChaos Engineeringは、システムを意図的に不安定にすることで事前に脆弱性を探り、対策を考えることができるという点で、最適なエンジニアリング手法であると考えています。

Q: ツールはどういったものを使ったか?

AWSコンポーネントの故障・障害やネットワーク遮断などのイベントはAWS APIを用いて発生させました。また、インスタンスリソースの枯渇については、Linuxベースの標準コマンドやディストリビューションのコマンドを利用して実施しました。その他、プログラム中に直接レイテンシーを注入するも実験もしました。[Awesome Chaos Engineering]では多くのツールが列挙されていますが、Chaos Engineeringを行う必須のツールではないと考えています。

Q: 本番環境ではやらないのか?

もちろん実トラフィックが流れる本番環境で実施することが望ましいですが、現状では開発環境のみでの実績となります。開発環境で実践しても意味がないのではないか、というとそうでもないです。事例で一部ご紹介したようなインスタンス内部のプロキシが抱えていた障害の種を事前に発見し、対策できたことからも、開発環境でも十分価値のあるプラクティスができるという感触を持っています。ハードルが高いという印象をお持ちの方は、まずは開発環境から試すことをお勧めします。

終わりに

Cygamesのインフラチームでは、高速かつ安定的にゲームを提供できるように、目立たないながらもコツコツと業務に取り組んでおります。今回ご紹介した取り組みに限らず、ゲームシステムの構築・運用から、システムパフォーマンスの改善、オンコールや開発者が抱える繰り返し作業の効率化、新規のミドルウェア・サービスの導入検証、などと幅広い業務があります。
また、最近では、コンテナオーケストレーションツールとしてデファクトスタンダードになっている、Kubernetesに移行するプロジェクトも出てきており、モダンなシステムへの転換も着々と進んでいます。そんなインフラチームでは、ゲームインフラに興味のある方、またセッションでご紹介したChaos Engineeringの取り組みにご興味のある方を募集しております。詳細はこちらをご確認ください。

最後に予告となります。セッション内でChaos Conf 2019に参加した様子に触れましたが、もう少し詳しい内容をこちらのブログに掲載予定です。お楽しみに。

【Chaos Conf 2019 視察レポート】Chaos Engineeringの盛り上がりを実感

$
0
0

こんにちは、インフラチームの和田です。

9月25日(水)、26日(木)にサンフランシスコで開催されたChaos Conf 2019に参加してまいりました。Gremlin,Incが主催するChaos Confは、Chaos Engineeringの事例や知見を集めた開発者向けのカンファレンスです。同社はChaos Engineeringを「Resilience as a Service」として提供する企業で、そのプロダクト名は『Gremlin』です。Day 0がブートキャンプ、Day 1がセッション、参加総数はおよそ600人ということで、昨年(およそ400名)よりも規模は拡大しています。では、Day 0とDay 1の内容について振り返ってみます。

Day 0: HANDS-ON CHAOS ENGINEERING BOOTCAMP

chaosconf_01

Day 0は、EKS(Amazon Elastic Kubernetes Service)上に構築された架空のショッピングサイトに対して、Gremlinを使用してChaos Engineeringを体験するもので、参加者は60名ほどでした。4人ずつでチームを組み、「General」「Commander」「Scribe」「Observer」のロールに別れて作業をします。「General」が全体統括、「Commander」がGremlinを操作、「Scribe」が結果の記述、「Observer」がモニタリングを行います。全体向けに簡単な自己紹介をした後、ファシリテーターが示すシナリオに対して、EKSクラスタ上にFault Injectionをしていきます。

EKS上に構築された架空サイトの構成は下記ようなマイクロサービス設計となっており、グループワークではファシリテータから示された3つのシナリオを検証しました。

  1. CPU使用率の段階的上昇に対してもオートスケールが実行されるか?
  2. 特定のServiceにデプロイされたコンテナを停止しても自律的に復旧するか?
  3. フロントサーバーへの段階的な遅延の上昇に対してブラウザ上ではサイトが表示されるか?

chaosconf_02

では、3つのシナリオを振り返ってみます。

1.CPU使用率の段階的上昇に対してもオートスケールが実行されるか?

まずは、ホストのCPU使用率を20%、40%、60%⋯⋯と上昇させるシナリオです。モニタリングツールを見ると、CPU使用率が意図通りに変化しているのがわかります。ただ、一向にオートスケールはしません。後から聞いたのですが、もともとオートスケールの設定はなく、オートスケールが正しく設定されているかどうかを確認する手法としても使えることを示したかったようです。また、Datadog上では、攻撃中であることを示すバー(赤色)がグラフ上に表示されるため、実験中であること直感的にも判断しやすくなっていました。

chaosconf_03

chaosconf_04

2.特定のServiceにデプロイされたコンテナを停止しても自律的に復旧するか?

次は、先の設計図にあったCartServiceをシャットダウンするというシナリオです。1コンテナをシャットダウンすると、クラスタ内部ではKubernetesによる自己回復が働いて自動的にdesired stateに戻ります。インスタンスのメトリックは無変化でした。この実験自体は平凡でしたが、コンテナが異常停止した場合でも可用性を確保できているかどうかを確認することは、運用視点からすると非常に重要な点だと思います。

chaosconf_05

chaosconf_06

3.フロントサーバーへの段階的な遅延の上昇に対してブラウザ上ではサイトが表示されるか?

最後は、先の設計図にある全てのAPIに対して100ms、200ms、500ms⋯⋯と徐々にLatencyを追加するシナリオです。レスポンスタイムを計測する手段がなかったため、ブラウザの開発者ツールで遅延を確認しました。このシナリオでは、遅延が最大で5sまで増加するため、サイト表示に重大な影響を与える結果となりました。少しシナリオが大袈裟でしたが、Service単位のLatency Injectionもできるため、Serviceの追加・更新における遅延を検証する用途では重宝しそうです。

以上、ハンズオンではGremlinのプロダクトを利用してChaos Engineeringを体験しました。仮説、Fault Injection、そして結果検証に至るまでのワークフロー管理をGUIで操作できるため、よく作り込まれたツールであると感じました。

余談にはなりますが、当日のサンフランシスコは最高気温が34℃。会場に空調設備はなく窓は全開で日本ほどの暑さは感じませんでしたが、騒音が常時入り込み、ファシリテーターはマイクなしで説明していたので、正しく聞き取ることはなかなか困難でした。

続いてDay 1です。

Day 1: SESSION

chaosconf_07

Day 1は、Keynoteが2本、LTが4本、一般が6本という構成でした。登壇企業の業界としては、IT6社、小売3社、アパレル・メディア・旅行がそれぞれ1社で、Eコマース系の会社が目立った印象です。Gremlin,IncのCEOのKolton氏が「今年はWHYではなくHOWにフォーカスしたセッションを用意している」と言っていたように、概念よりも事例がよく聞けたセッションだったかと思います。

今回は私の印象に残ったセッションを3つほど紹介したいと思います。こちらは私の解釈も含まれますので、オリジナルを参照したい方は、セッション動画およびセッションスライドをご確認ください。

“A Roadmap Towards Chaos Engineering” Jose Esquivel, Backcountry

Chaos Engineeringを実現するためのロードマップ、実践する中で見えた8つの安定性のパターン、オブサーバビリティを獲得するための4つの方法を提示していました。パターンが整理されたノウハウの詰まったセッションの一つでした。

chaosconf_08b

Chaos Engineeringを行う前準備として、「Observability」「Alerting」「Incident Management」「Test Harness」が必須であると述べています。中でもObservabilityについては、ログ、トレース、メトリックス、アラートの4つを整備しなければ、本当にchaosな状態になると付け加えています。

では、Jose氏が挙げている8つの安定性パターンについて見ていきます。

  • Timeouts & Retries: タイムアウトについては、デフォルトでは長すぎるのでニーズに合わせて適切に設定をするべき。リトライ回数についてはリトライの嵐でTCPやDB接続プールを減少させて影響が出ないよう特に注意が必要である。
  • Circuit Breaker: 一定の閾値を超えるリクエストが到達した場合に制限する。
  • Bulkhead Pattern: 接続プールにリトライやタイムアウトを委譲してエラーを分離する。
  • Steady State: 定常状態を定義する。
  • Fail Fast: 失敗は早期に検出する。
  • Handshaking: 呼出しを拒否させる。メモリーオーバーフローを回避する。
  • Uncoupling via Middleware: 待機が必要な処理をキューに押し出して、他のプロセスが処理できるようにする。
  • Test Harness: Unit Testなどに時間を多く割くよりもChaos Testsにジャンプした方が弱点は早く見つかる。

これらは何か設計に悩むような場合にはぜひとも見直したい項目になると思います。特に、タイムアウトとリトライの設定については依存ライブラリに固有の設定があったり、マイクロサービス化が進むと障害ポイントになったりしやすいので、ファーストステップとして確認したいパターンです。

“Forming Failure Hypotheses” Subbu Allamaraju

Subbu氏は組織内でChaos Engineeringを実施して苦悩した経験を話しています。特に印象的な点は、ログやメトリックスはバイアスがかかるから、実際に発生するインシデントが最も学びになる、それ以外にはないと言い切っていました。

彼は、その経験をストーリーで話しています。まずクラウド移行から始まり、障害を前提としたシステムを作るためにChaos Engineeringを導入しました。しかし、チームからは拒否反応を示されたり、ランダムにサーバーを落としても大した発見もなかったり、クリティカルな故障を発生させたくても実問題に繋がるのでできなかったりと、葛藤の日々が続いたそうです。その中で、「システムを不安定な状態でも耐えられる設計にできるか」と考えるよりも「今現在プロダクション環境ではシステムはどういう振る舞いをしているか」を考えるようになったそうです。ドキュメントや図、ログ、メトリックスなどの人間の手でデザインされた眼鏡を通すと本来の姿が隠れてしまうことがあり、システムの現状、「as it is」な状態を見るようになったそうです。

結局のところChaos Engineeringなんて必要ない!? と言いたかったのか真因はわかりませんが、プロダクションのシステムで発生するインシデントが最も学びになることは確かだと思いました。

“Keynote: Chaos Engineering For People Systems” Dave Rensin, Google

人間組織が最も複雑な分散システムであると述べ、分散システムに適用するChaos Engineeringを人間組織に適用したというセッションです。そもそも人間はバグだらけの生物学的なマイクロサービスと定義し直した上で、冒頭で「皆さん左手を挙げてください」と言って壇上から撮影した写真には、右手・両手を挙げる人が何人かいたことを示し、この説を証明していました。

chaosconf_09b

Dave氏がChaos Engineeringを企業に適用したという4つストーリーの中でも、チームメイトから無作為に選ばれた1名に急な近場での休暇(staycation)を取らせるという話は非常に興味深い内容でした。対象者はリモートで仕事をするが、チームメイトからの質問には答えず、ミーティングにも参加せず、メッセージにも反応しません。対象者がその場に存在していなくとも、チームは機能するかどうかを検証するというものです。ひと月に1回、staycationのテストによりSPOF(Single Point Of Failure)が存在しないかをレビューして、SPOFがあるならメンバー間でのナレッジを共有する場を設けるなどして改善をしていったそうです。

Chaos Engineeringをチームビルディングに適用したこの例は、かなり奇抜な印象を受けるものでした。Chaos Engineeringはあらゆる「分散システム」に適用できるという、思考の幅の広がるセッションでした。

以上、Day 1のセッションをいくつか紹介しました。

オフショット

企業のブースも出ていました。朝イチだったので人の入りはまだ少ないです

企業のブースも出ていました。朝イチだったので人の入りはまだ少ないです

DevOpsエンジニアが、Oopsしないように(やってしまわないように)というメッセージ

DevOpsエンジニアが、Oopsしないように(やってしまわないように)というメッセージ

会場では朝食や昼食などが提供。ディナーは撮り忘れました⋯⋯

会場では朝食や昼食などが提供。ディナーは撮り忘れました⋯⋯

アフターパーティー。会場中央に巨大なジェンガが設置されており、倒れると「Chaos!!!」と叫ぶ人がいるなど、2日間で最もChaosを感じられる瞬間でした

アフターパーティー。会場中央に巨大なジェンガが設置されており、倒れると「Chaos!!!」と叫ぶ人がいるなど、2日間で最もChaosを感じられる瞬間でした

宿は会場から1マイルほどの場所にAirbnbで取りました。ダウンタウンから離れている閑静な住宅街で、夜は静かに眠れました

宿は会場から1マイルほどの場所にAirbnbで取りました。ダウンタウンから離れている閑静な住宅街で、夜は静かに眠れました

終わりに

Day 0、Day 1と参加して、昨年比での参加規模の拡大や登壇企業の多業種化からもわかるように、Chaos Engineeringを実践する企業は広がりを見せていました。「マイクロサービス設計をした上でのChaos Engineering」という風潮も受けますが、3TierでMonolithな構成に適用している事例もあるように、分散システムで冗長構成をとっているシステムであれば適用できるものです。今後はアプリケーションレイヤーにFault Injectionを組み込んだServerlessへの適用事例も増えそうな予感がしています。

また、アマゾン ウェブサービス ジャパン株式会社の畑様のご協力により、Chaos Conf 2019のrecapイベントをAWS Loft Tokyoにて開催することになりました。詳細はこちら
をご覧ください。参加無料ですので、ご興味ある方はふるってご参加ください。

【CEDEC 2019 フォローアップ】「現場最優先のススメ」~最高の開発環境を生み出す情報システム部門の在り方~

$
0
0

2019年9月5日(木)に開催されたCEDEC 2019にて、「『現場最優先のススメ』~最高の開発環境を生み出す情報システム部門の在り方~」という講演を行いました。
講演にご参加いただいた皆様には、改めて御礼申し上げます。

はじめに

本講演では、情報システム部門(以下、情シス部門)の方向性がコスト意識に向いてしまうことが多いことについて、クオリティの高い対応が求められるゲーム業界においては必ずしも正解とは言えないということを説明し、Cygamesの情シス部門の考え方である「現場最速」と「満足度最重要」の説明と、それらが基となっている様々な取り組み例の一端をご紹介しました。

以下が当日の発表資料です。本記事ではフォローアップということで、「現場最速」「満足度最重要」という考え方の要点を説明いたします。

「現場最速」

現場最速とは、開発現場のスタッフがクリエイティブな時間に割ける割合を最大化できるよう、「スピード」を追求することです。
具体的な取り組み内容は、「機材の準備やソフトウェアの設定をやらせない」「要望があった際の待ち時間を最小化する」「問い合わせに迷う時間を無くす」といった対応です。

現場最速を求めていくと、現場の開発スピード、対応スピードが上がります。
例を挙げると、カスタマーサポート対応における端末不具合調査なども同じ環境を即日で再現できることで、バグ修正の対応速度も早くなりますし、新しいソフトウェアを検証したいとなった時もすぐに準備が整うことで迅速に最新技術の検証を始めることができます。

また、Cygamesはスピード感を大事にしている会社で、意思決定のスピードもかなり速く、それが会社の強みの一つでもありますが、そういった素早い意思決定の価値をより高めることがバックオフィスの立場でありながら可能です。せっかくスピード感のある意思決定がなされても、機材やソフトウェアの準備がボトルネックとなってしまうようでは意味がありません。

現場最速を追求することで、情シス部門の立場から会社のスピード感を高めることができ、より競争力のある会社の下支えとなることができます。ゲーム業界は変化が激しく、また技術進歩のスピードも速いので、この点において特にゲーム業界で情シス部門の果たす役割は非常に大きいと言えます。

「満足度最重要」

満足度最重要は、ストレスなく業務に集中してもらうために現場スタッフの「満足度」を追求することです。Cygamesでは「ストレスフリーな業務環境の提供」「対応の標準化ではなく専門特化」「ユーザーの意見を取り入れた改善サイクル」といった観点で様々な取り組みを行っています。

「満足度最重要」を追求することで、業務環境の充実さや情シス部門の対応の良さが会社のアピールポイントになります。実際Cygamesのスタッフが受けるメディアのインタビューなどでも、Cygamesの良いところとして、情シス部門の体制を挙げてくれる人も少なくありません。様々なゲーム会社を経験してきたベテランの方にそう言ってもらえることは、もちろん会社の魅力の発信となりますし、情シス部門のメンバーにとっても非常に励みになり、改善サイクルの原動力にもなります。人材の流動性が激しいゲーム業界において、この点でも情シス部門の担う役割は大きいと言えます。

また、情シス部門の対応を標準化するのではなく専門特化していくことで、日々、技術難度が高くなる開発現場の要望に情シス部門として全力で応えることができます。技術の進歩が著しいゲーム業界において、そのトレンドについていくためにも、「満足度最重要」の考えはゲーム業界に身を置く情シス部門としてはなくてはならない考えではないでしょうか。

最後に

Cygamesでは、「常に『チーム・サイゲームス』の意識を忘れない」という考え方が浸透しているため、バックオフィス部門も「最高」を目指すことができる環境にあります。しかしながら、「現場最速」「満足度最重要」という行動指針は小さいところからでも始められますし、情シス部門に限らず、ゲーム業界において業務基盤の部分に携わっている方にとっては必要となってくる考え方だと思いますので、本発表の内容が何かしら皆様の業務改善のきっかけになれば幸いです。
今後も最高のコンテンツをつくるための最高のサポートを実現すべく、邁進してまいります。

【CEDEC 2019 フォローアップ】スマホゲームリリース時に絶対サーバを落とさないための負荷試験

$
0
0

こんにちは。技術本部所属コンシューマーエンジニアの伊藤です。
先日のCEDEC 2019での『スマホゲームリリース時に絶対サーバを落とさないための負荷試験』の講演に、朝早い時間にもかかわらず多数の方にご参加いただき、誠にありがとうございました。

本講演ではCygamesで行っているリリース前の負荷試験の取り組み方についてご紹介いたしました。

今回のフォローアップ記事では講演内容のポイントのおさらいおよび、いただくことが多かったご質問にお答えしたいと思います。

講演内容のポイント

・快適に遊べることが目標

いつでも安定して素早いレスポンスのサーバを提供することで、ゲームの面白さを損なうことなくユーザーに届けたいと思っています。それを実現する手段の一つが負荷試験です。すべては、Cygamesのビジョンにもある「最高のコンテンツ」につながると考えています。

・必要十分な計測体制

やはりこの格言に尽きると思います。

ルール2: 計測すべし。計測するまでは速度のための調整をしてはならない。コードの一部が残りを圧倒しないのであれば、なおさらである。

Rule 2. Measure. Don’t tune for speed until you’ve measured, and even then don’t unless one part of the code overwhelms the rest.
by Rob Pike

・計測・修正のイテレーションサイクルをできるだけ速く

負荷試験を専門で行うチームがいることも、負荷試験業務の最適化により間接的にイテレーション速度向上に寄与しています。つまるところ、リリースまでに何回イテレーションサイクルを行えたかが安定リリースの鍵となります。

いただいたご質問

Q:本番と同等以上の規模の負荷をかけるのが難しい場合はどのようにすればいいですか?

A:サーバ構成は同じで数分の一サイズに縮小した、相似形の環境を用意して負荷試験する方法があります。しかし、測定結果の精度は劣りますので注意が必要です。また、複数並べて明らかにスケールアウトすることがわかっている設計では、最小単位での性能を計測することでフルサイズの負荷試験の代用と見なせる場合もあります。

※最小単位のサーバクラスタで負荷試験していい場合とそうでない場合の例

※最小単位のサーバクラスタで負荷試験していい場合とそうでない場合の例

Q: リリース後に負荷試験することはありますか?

A:はい。あります。大きい機能の追加/改修時には負荷試験を行う場合があります。ご質問の趣旨とは話がずれますが、他に工夫している点としては、リリース前にも言えることですがメンテナンスを入れずにスケールアウトできるようにする設計であったり、メンテナンスを入れたとしても一部機能/短時間に留める機能を準備しています。

Q: 負荷試験をCIに組み込んだ例が知りたいです

A:node.jsサーバの負荷試験を行った際の例をご紹介いたします。講演でご紹介したmochaを使って正常系の負荷試験シナリオを作ると同時に、バグチケット番号と紐づいたテストケースをソースコード修正と一緒にコミットするようにしました。外形テストと負荷試験シナリオが同じ仕組みなので、高負荷状態や多数のクライアントが接続しているような状況でだけで出るようなバグも再現・修正確認しやすくなります。併せてCIで日常的にこの蓄積したテストシナリオを実行することでデグレードのチェックも行うことができ、非常に開発効率を上げられました。

※バグチケット番号と紐づいた外形テストケースを作成する例

※バグチケット番号と紐づいた外形テストケースを作成する例

Q: 負荷試験の工数があまりかけられません

A:リリース後に負荷で遊べない状態になるよりも、ある種の保険として負荷試験はやっておいたほうが良いです。ゲームを遊べないユーザーのストレスに加えて、緊急メンテ時の開発チームの負担も大きくなり、ひいては「よりゲームを面白くしていくことに時間を割きづらく」なってしまいます。
つまりリリース後の運用が、

  • 「負荷の問題がある」というマイナスの状態から、「負荷に問題がない」ゼロの状態に持っていくか
  • 「負荷に問題がない」というゼロの状態から、「より面白くする」プラスをより大きくしていく

というようにスタート時の状況が大きく違ってきてしまいます。こういった点を工数の検討材料にしてはいかがでしょうか。

Cygamesではユーザーのみなさんが安定して快適に遊べるゲーム環境を提供するために、ご紹介した以外にも様々な工夫を行い日々新たな改善を行っています。ご興味を持っていただいた方は、ぜひほかの講演やこちらもご覧いただけると幸いです。

【CEDEC 2019 フォローアップ】個性的で魅力的なモンスターを量産する為の秘訣と開発手法紹介(モンスターデザイン制作事例)

$
0
0

『プリンセスコネクトRe:Dive』イラストチームの野西です。
2019年9月5日(金)に開催されたCEDEC 2019において、『個性的で魅力的なモンスターを量産する為の秘訣と開発手法紹介』という講演を行いました。
当日はお忙しい中、たくさんの方にお越しいただきありがとうございました。

本講演では、プリンセスコネクトRe:Diveにおけるモンスターを作るうえでの二つの課題を提示しました。それは、「クオリティ」と「量産」の両立です。
この課題に対し、どのような方法でモンスターの制作を進めていったのか。制作手法をデザイン面、アニメーション面それぞれを実例と交えながら紹介していきました。

以下が講演資料になります。

デザインパートで大切にしていること

デザインパートで講演してきたのは以下の点です。
「クオリティ」面で大切にしているのは、世界観を守りつつ、わかりやすいデザインを目指すこと。また、面白いデザインを考えるためにもたくさんのアイディアを出し、その際にユニークな要素も大切にすること。
「量産」面で大切にしていたのは、クオリティを大事にしながらも、時間の削減ができるところでは極力時間短縮すること。そのためにもルールを決めて迷う時間を減らすこと。またすべてを一から作るのではなく、ガワ替えや色替えを行い、デザインのみの変更で時間短縮を狙うこと。
そして不適切な表現は避けること。これを気をつけないと、スケジュールに大きな影響が出てしまいます。
一つ一つは小さいことかもしれませんが、積み重ねが時間短縮、量産へとつながっていきます。
これら二つが、「クオリティ」と「量産」を両立することが大切です。

ある意味、クオリティと量産は対極の位置にあるものです。
これらを同時に行うのは決して簡単なことではないかもしれませんが、ユーザーに最高のコンテンツを楽しんでもらうために、私たちは妥協せずに取り組んでいます。

アニメーションパート
~ビジュアル設計の最適化~

後半のアニメーションパートでは、バトルの絵コンテ制作を軸として、動的ビジュアル設計の手法についてご紹介しました。内容は以下になります。

  • ビジュアル情報の整理方法
  • 字コンテの文字情報を絵コンテにしやすい文字情報に変換する手法をご紹介しました。
    展開を「タメ」「発動」「余韻」の3パターンに要素分解して再構築します。
    情報の整理はビジュアル設計の初期段階に相当し、この段階でだいたいの尺感まで見当を付けます。

  • キャラクターのシルエット設計
  • バトルモーションにおけるシルエットコントロールの重要性と、その手法についてご紹介しました。手法は以下の2点にまとめてご紹介しています。
    ◎個々のバトルモーションの「初動」に変化をつけ、バトルの視認性を上げる手法
    ◎攻撃時のインパクトを高めるための手法

  • 必殺技時の画面レイアウトの設計
  • 絵コンテで、必殺技の画面レイアウトをダイナミックに設計するための手法をご紹介しました。具体的には以下のような内容になります。
    ◎実装面で画面レイアウトのダイナミックな絵作りを支える機能
    ◎上記の機能を最大限に生かした設計を行うためのポイント
    ◎構造の複雑化に耐えうる絵コンテ設の考え方

最後に

私たち『プリンセスコネクトRe:Dive』のチームメンバーは楽しみながらものづくりをしています。
今回講演したモンスター以外の部分、キャラクターはもちろんのこと背景、UI、4コマなどすべてのコンテンツをユーザーに楽しんでもらうために愛とこだわりをもって制作にあたっています。
ご賛同いただける方、ぜひ私たちと一緒に働きませんか?

【CEDEC 2019 フォローアップ】フォトグラメトリーとプロシージャルを用いた最新ハイエンドゲーム3DCG背景制作手法

$
0
0

こんにちは。Cygames デザイナー本部3DCGアーティストチームの吉冨です。2019年9月6日(金)に開催されたCEDEC 2019にて、『フォトグラメトリーとプロシージャルを用いた最新ハイエンドゲーム3DCG背景制作手法 ~ハイエンドゲーム開発の経験がない会社がいかにしてそれらを生み出したか~』という題名で講演を行いました。

聴講いただいた皆様には改めて御礼申し上げます。今回は講演のフォローアップとして、講演資料を公開いたします。また、講演後にいただいた質問への回答を述べさせていただきます。

講演の概要

本セッションでは現実の風景と遜色なく、それ故に高い没入感が得られる写実的な背景制作を課題とし、ハイエンドゲーム制作の経験がない会社がそれらを実現可能にした制作手法とノウハウをご紹介しました。

具体的には、写真から得られたデータをCGに取り込む技術(フォトグラメトリー)と、自動生成の技術(プロシージャル)の融合、また伝統的な背景制作のノウハウの活用です。
何度かの失敗を経験し、そこで得られた気づきと対策をご説明しました。

講演のポイント

本講演で伝えたかったことは3つあります。
1つ目は、ハイエンドグラフィックを目指すからといってテクノロジーに振り回されず、基本に忠実に背景制作に取り組む姿勢が大事だということ。

2つ目は、フォトグラメトリーとプロシージャルを高いレベルで融合させて写真のような見た目を目指しつつ、伝統的な背景制作のノウハウをバランスよく織り交ぜて、両者でシナジー効果を生み出すこと

3つ目は、絶対にお客様にいいものを届けたい! という熱意が、あらゆる課題を突破する原動力となること
です。

この3つのポイントをしっかりと踏まえていただき、皆さんの現場でいいコンテンツを作り日本のゲーム業界を共に盛り上げていきたいです。

講演後の質問と回答

Q:現像段階でディライトすると、PBRのルールに反することになりませんか?

A:ご質問の内容は、Physically Based Rendering(PBR)を徹底させるなら、現像段階で画像を調整する余地はないはずなのにディライトをしても大丈夫なのか、というものです。

答えとしては「PBRのルールには、可能な限り合わせるようにしている」です。

はじめはPBRのルールを100%徹底すべく、岩などをnixのカラーセンサーで計測して正しい数値を検出して現像をしていました。しかし、それでは

  • 工数がかかりすぎる
  • そもそも現実のカラーチェッカーの色と、sRGBカラースペース内のカラーチェッカーの色をすべて正しい数値に完全に一致させて現像することが難しい

との理由から、PBRのルールからやむなく少しだけ外れるということもあります。
ただし、素材を撮影する際に必ず1アセットにつき1枚はグレーカードを撮影して、Adobe Bridgeで露光量を中間グレーのRGB118に合わせるなど、最低限のことは徹底しています。

PBRについて

クオリティーを担保するためにはPBRのルールを遵守する必要があります。ルールを守っていないと、グラフィックの問題が起きたときにマテリアルが悪いのか、それともライティングに問題があるのかが分からなくなるからです。

ですが、上に挙げた運用面の理由から、やむなく外れる場合もあります。その際も、PBRから外れるとわかっていてあえてそうするのか、それともよくわからないままに迷走するのかでは全然意味が違ってきます。

以上のことから、PBRに関してよく理解し、ルールに沿っているかどうかを常にチェックできるスキルが必要になります。

最後に

Cygamesは「チャレンジ」が大好きです。

新しいモノを生み出すときに失敗はつきものです。ですが、私たちは失敗は挑戦することで得られる価値であると考えています。新規タイトル開発を通して、たくさんの価値を生んでお客様に最高のコンテンツを届けたい! という思いで制作しています。

その結果、個人もチームも大きく成長します。新規タイトル開発というクリエイターとして最もエキサイティングな現場に来ませんか?
共に世界へ!


マスターデータにおけるパラメーター検証のためのMasterMemory v2

$
0
0

Cy#の河合です。以前に「MasterMemory – Unityと.NET Coreのための読み取り専用インメモリデータベース」という記事で、弊社で開発しOSSにて公開しているMasterMemoryというライブラリを紹介しました。

[GitHub – Cysharp/MasterMemory]

最初のバージョンでは、コンセプト実証と性能の追求が主軸でしたが、最初の公開から半年を経た今回はVersion 2として、実アプリケーションに適合するにあたっての便利な周辺機能の拡充を図りました。その一つがパラメーター検証のためのバリデーターです。

Cygamesでは以前、「ユーザを飽きさせない高頻度の更新を可能にする開発運用ノウハウ ~ハイスピードな開発、リリースを実現するために~」としてDSLによる検証システムを紹介しました。

DSLベースにすることで、スキーマ定義によるサーバー用(PHP)とクライアント用(C#)コードの生成に加えて、簡潔な記法によるバリデーションを可能にしました。

これに対しMasterMemoryは、C#で完結させることでワークフローを簡潔にするという目的で検証を進めました。
また、C#という汎用言語を使用する利点としては、

  • 完全なシンタックスハイライトが効く
  • すべて型付けされていて入力ミスをリアルタイムで検知できる
  • 入力補完が効く
  • 複雑なフローをコードでシンプルに記述できる

などが挙げられます。

こうした、汎用言語を使うというアプローチは一昔前までは禁忌とされていましたが、最近ではAWS Cloud Development Kitによる、TypeScriptやJavaでAWS CloudFormationのテンプレートを生成する手法であったり、PulumiによるTypeScript、Python、Go、C#でTerraformのテンプレートを生成するといった、特にInfrastructure as Codeの文脈では、最近よく見かけるようになりました。

これは、特にVisual Studio CodeとLanguage Server Protocolの発展によって、エディタがより強力になり、汎用言語のメリットを甘受しやすくなったことと、GitHubのPull Requestなどのレビュー環境や文化が成熟してきたことによる、汎用言語ならではの「なんでもできてしまう」性質を抑えやすくなったことが理由だと考えています。いずれにせよ、今だからこそ、汎用言語によるアプローチを試みる価値があるでしょう。​

MasterMemory v2による検証コードは、上記スライドにおける例を使って書くと、以下のようになります。

// IValidatableを実装すると検証対象になる
[MemoryTable("quest_master"), MessagePackObject(true)]
public class Quest : IValidatable<Quest>
{
    // UniqueKeyの場合はValidate時にデフォルトで重複かの検証がされる
    [PrimaryKey]
    public int QuestId { get; }
    public string Name { get; }
    public int RewardId { get; }
    public int Cost { get; }

    void IValidatable<Quest>.Validate(IValidator<Quest> validator)
    {
        // 外部キー的に参照したいコレクションを取り出せる
        var items = validator.GetReferenceSet<Item>();

        // RewardIdが0以上のとき
        // (0は報酬ナシのための特別なフラグとするため入力を許容する)
        if (this.RewardId > 0)
        {
            // Itemsのマスタに必ず含まれてなければ検証エラー
            items.Exists(x => x.RewardId, x => x.ItemId);
        }

        // コストは10..20でなければ検証エラー
        validator.Validate(x => x.Cost >= 10);
        validator.Validate(x => x.Cost <= 20);

        // 以下で囲った部分は一度しか呼ばれない
        // データセット全体の検証をしたい時に使う
        if (validator.CallOnce())
        {
            var quests = validator.GetTableSet();
            // インデックス生成したもの以外のユニークどうかの検証
            quests.Where(x => x.RewardId != 0)
                  .Unique(x => x.RewardId);
        }
    }
}

[MemoryTable("item_master"), MessagePackObject(true)]
public class Item
{
    [PrimaryKey]
    public int ItemId { get; }
}

void Main()
{
    var db = new MemoryDatabase(bin);
    // 検証を開始し、結果を取得する。
    var validateResult = db.Validate();
    if (validateResult.IsValidationFailed)
    {
        // 検証失敗データを文字列形式でフォーマットして出力
        Console.WriteLine(validateResult.FormatFailedResults());
        // List<(Type, string, object)> で検証データを取得して、
        // 自分でカスタムで出力することも可能
        // MDやHTMLに整形してSlackやレポーターに投げるなど自由に使える
        // validateResult.FailedResults
    }
}


スキーマとなるC#クラスにIValidatable<T>を実装することにより、スキーマの近くに検証コードを書く、という見立てとなっています。基本的にはvalidatorに対してラムダ式で1行で指定するだけで済むので、DSLと比べても、十分に簡潔なものを簡潔に書くことが可能です。

また、C#の「Expression Trees」を活用することにより、検証に失敗した時のメッセージを、特に追加で文字列で説明を記述しなくても、十分理解しやすい内容で出力できます。

Quest - Exists failed: Quest.RewardId -> Item.ItemId,
                       value = 99, PK(QuestId) = 3
Quest - Validate failed: (this.Cost <= 20), Cost = 30, PK(QuestId) = 4
Quest - Validate failed: (this.Cost <= 20), Cost = 40, PK(QuestId) = 9
Quest - Unique failed: .RewardId, value = 100, PK(QuestId) = 10


ExistsValidateといったアサート関数のラムダ式はFuncではなくExpression Treesで表現されていて、検証失敗時には式を解釈して、理解しやすいメッセージに整形しています。

なお、バリデーションにあたってはオンメモリでロード済みのMasterMemoryのデータそのものを使用して検証を行うため、動作も非常に高速に完了します。

メタデータ取得API

繰り返しますが、MasterMemoryではC#コードがスキーマ定義となっていますが、データの入力に関しては、別途CSVから行うかもしれませんし、データベースから取得するかもしれません。CSVでデータ入力を行うなら雛形となるCSVを作りたいし、データーベースも関係するならテーブル生成SQLを作りたいかもしれません。MasterMemoryではメタデータ取得APIとして、こうしたテーブル情報、プロパティ情報、インデックス情報をそのまま取り出すことができます。

// テーブル情報、プロパティ情報、インデックス情報が取れる
var metaDb = MemoryDatabase.GetMetaDatabase();
foreach (var table in metaDb.GetTableInfos())
{
    // CSVのヘッダ生成
    var sb = new StringBuilder();
    foreach (var prop in table.Properties)
    {
        if (sb.Length != 0) sb.Append(",");

        // Original, LowerCamelCase, SnakeCaseに変換した名前を取得
        sb.Append(prop.NameSnakeCase);
    }
    File.WriteAllText(table.TableName + ".csv", sb.ToString(), 
                      new UTF8Encoding(false));
}


つまるところ、スキーマのエクスポーター、あるいはデータのインポーターの開発は別途必要なわけですが、それらの開発に役立つのではないかと思います。そうしたプログラムの作成には、弊社で開発している[ConsoleAppFramework]を併せて使うことによって全体のワークフローをシンプルに作り込んでいくことが可能です。また、UnityEditor上でのエディタ拡張から作ってみるのも良いでしょう。

まとめ

今回のバージョン2で、より実践的に踏み込めたと思っています。標準のインポーターのようなものも用意してみたくはあったのですが、そこは各社でワークフローが全然違うところなので、逆に自由にできたほうが良いかな、という判断もありました。

弊社ではクライアントサイド(Unity)とサーバーサイド(.NET Core)の両方で採用し、実際の開発も進めています。ぜひ、皆さんもお試しいただければ幸いです。

ZString – Unity/.NET CoreにおけるゼロアロケーションのC#文字列生成

$
0
0

Cy#の河合です。今回、文字列生成におけるメモリアロケーションをゼロにする「ZString」というライブラリを公開しました。そこで、この記事ではZStringの紹介の他に、あらためてC#の文字列についてを深く分解して解説し、Stringの複雑さと落とし穴、そしてZStringの必要性について解説します。

[GitHub – Cysharp/ZString]

以下の表は `”x:” + x + ” y:” + y + ” z:” + z` という単純な文字列連結においてのパフォーマンス計測です。
zstring_01_table

それぞれ

  • “x:” + x + ” y:” + y + ” z:” + z
  • ZString.Concat(“x:”, x, ” y:”, y, ” z:”, z)
  • string.Format(“x:{0} y:{1} z:{2}”, x, y, z)
  • ZString.Format(“x:{0} y:{1} z:{2}”, x, y, z)
  • new StringBuilder(), Append(), .ToString()
  • ZString.CreateStringBuilder(), Append(), .ToString()

におけるメモリアロケーション量と速度の図になっています。ZStringはどのケースにおいても、連結後文字列の56B以外のアロケーションがありません。また、APIの使用勝手としてもStringBuilderあるいはString.FormatString.ConcatString.Joinをそのまま置き換えることができます。

String型の構造と生成

C#のString型は内部的にはUTF-16のバイト列となっています。
zstring_02
通常のオブジェクトと同じようにオブジェクトヘッダを持ちヒープ領域に確保され、同様に、原則new stringのみで生成可能です。StringBuilder.ToStringEncoding.GetStringなども、最終的にnew stringを呼んで、新たな文字列を確保しています。

正確には、一部の.NET Framework内部のメソッドは String.FastAllocateString(int length)というinternalメソッドで確保された文字列領域に直接書き込みを行っています。このメソッドは外部に公開されていませんが、.NET Standard 2.1 ではString.Create<TState>(int length, TState state, SpanAction<char, TState>action)メソッドが追加され、これを呼ぶことで、新規の文字列領域に直接書き込みができます。

new stringで生成した文字列は、文字列の値として同一のものも、異なるメモリ領域に確保しています。ただし定数文字列のみ、インターンプールと呼ばれるアプリケーション共有の領域から、一意の参照を取得します。

var x = new string(new[] { 'f', 'o', 'o' });
var y = new string(new[] { 'f', 'o', 'o' });
var z = "foo";
var u = "foo";
var v = String.Intern(x);

// different reference: x != y != z
Console.WriteLine(Object.ReferenceEquals(x, y)); // false
Console.WriteLine(Object.ReferenceEquals(x, z)); // false

// same reference: z == u == v
Console.WriteLine(Object.ReferenceEquals(z, u)); // true
Console.WriteLine(Object.ReferenceEquals(z, v)); // true

// same value
Console.WriteLine(x == y && x == z && x == u && x == v); // true

実行時に生成した文字列を、インターンプールの指す文字列に変更するには String.Internメソッドが使用できます。Internメソッドはインターンプールから取得、存在しなかった場合は登録して、登録した参照を返します。

インターンプールに登録したメモリ領域は削除することができないため、通常うまく活用することは難しいでしょう。ただし弊社で開発しているインメモリデータベースである「MasterMemory」では、展開した文字列はマスタデータとして、アプリケーション実行中ずっと参照し続けるという性質を活かし、全ての文字列をインターン化しています。

internal class InternStringFormatter : 
IMessagePackFormatter<string>
{
    string IMessagePackFormatter<string>.Deserialize(
        ref MessagePackReader reader, ...)
    {
        var str = reader.ReadString();
        if (str == null) return null;
        return string.Intern(str);
    }
    // snip...
}

また、.NET Coreのランタイムでは「String Deduplication」という、GC時に重複した文字列を取り除く(単一の参照に置き換える)機能が提案されていますが、実装の完了にはもう少し時間がかかるでしょう。

+連結とString.Concat

Stringの+連結はC#コンパイラが特殊な処理を行い、String.Concatに変換します。

string.Concat(object arg0, object arg1)
string.Concat(object arg0, object arg1, object arg2)
string.Concat(params object[] values)
string.Concat(string str0, string str1)
string.Concat(string str0, string str1, string str2)
string.Concat(string str0, string str1, string str2, string str3)
string.Concat(params string[] values)

"x:" + x + " y:" + y + " z:" + zは6引数の連結のため、 string.Concat(string[] values)に変換されています(Visual Studio 2019 Version 16.4.2のC#コンパイラの場合。詳しくは後述)。つまり以下のような結果になります。

string.Concat(new string[]{"x:", x.ToString(), "y:", y.ToString(), "z:", z.ToString() } );

C#コンパイラによる+連結の最適化は、現在のものと昔のものとで異なる結果となる場合があります。たとえばVisual Studio 2019のC#コンパイラは (int x) + (string y) + (int z) の結果がString.Concat(x.ToString(), y, z.ToString())となりますが、Visual Studio 2017 のC#コンパイラは String.Concat((object)x, y, (object)z) と、連結対象に非stringの引数が含まれている場合にobjectのオーバーロードを採用するため、構造体のボクシングが発生してしまいます。Unityを使用する場合は、Unityに同梱されているC#コンパイラのバージョンによって結果が異なることに注意が必要です。

Concatのオーバーロードとして、3引数(古いコンパイラでobjectのオーバーロードが使われる場合)、あるいは4引数(新しいコンパイラでstringのオーバーロードが使われる場合)までは可変長引数を通さないため、より高速に処理されます。

StringBuilderとSpanFormatter

StringBuilderの実体は、一時的なバッファとしてchar[]を持ったクラスで、Appendでバッファに書き込み、ToStringで最終的な文字列を生成します。

public class SimpleStringBuilder
{
    char[] buffer;
    int offset;
    int index;

    public void Append(string value)
    {
        value.CopyTo(0, buffer, offset, value.Length);
        index += value.Length;
    }

    public override string ToString()
    {
        return new string(buffer, 0, index);
    }
}

複数のStringを連結する際に+=を使用すべきでないのは、+=のたびに新しい文字列を生成するからです。

string BuildDescription(Enemy enemy, bool addStatus)
{
    var desc = enemy.Name;
    desc += " Current HP:" + enemy.Hp; // allocate new string
    desc += " Current MP:" + enemy.Mp; // allocate new string
    if(addStatus)
    {
        desc += " Status:" + enemy.Status; // allocate new string
    }
    return desc;
}

StringBuilderは、この一時的な新しい文字列の生成を避け、代わりにchar[]へのコピーを行います。

string BuildDescription(Enemy enemy, bool addStatus)
{
    var sb = new StringBuilder();
    sb.Append(enemy.Name);
    sb.Append(" Current HP:");
    sb.Append(enemy.Hp);
    sb.Append(" Current MP:");
    sb.Append(enemy.Mp);
    if(addStatus)
    {
        sb.Append(" Status:");
        sb.Append(enemy.Status);
    }
    return sb.ToString();
}

この際に注意する必要があるのはsb.Append(" Current HP:" + enemy.Hp);などと書いてしまうと、連結した一時的な文字列を作ってしまうので、極力+の利用も避けましょう。

数値型などをAppendする場合は、.NET Standard 2.0(Unityなど)と .NET Standard 2.1(.NET Core 3.0など)では挙動が異なります。

// .NET Standard 2.0
public StringBuilder Append(int value)
{
    return Append(value.ToString(CultureInfo.CurrentCulture));
}

// .NET Standard 2.1
public StringBuilder Append(int value)
{
    return AppendSpanFormattable(value);
}

private StringBuilder AppendSpanFormattable<T>(T value)
    where T : ISpanFormattable
{
    if (value.TryFormat(RemainingCurrentChunk, 
        out int charsWritten, format: default, provider: null))
    {
        m_ChunkLength += charsWritten;
        return this;
    }
    return Append(value.ToString());
}

.NET Standard 2.0では単純にToStringした結果を追加している、つまり文字列化によるアロケーションが発生していますが、.NET Standard 2.1では ISpanFormattable.TryFormatにより文字列を介さず直接バッファに書き込みしています。ISpanFormattable自体はinternalですが、 「ISpanFormattable.references」を確認することにより、どの型がこうした直接書き込みを実装しているかを確認することができます。

ZStringでは、.NET Standard 2.1ではそれぞれのTryFormatを、.NET Standard 2.0では移植したTryFormatメソッドにより、Unity環境下でも数値型を追加する際の文字列アロケーションを避けています。

API自体はStringBuilderとほぼ同一ですが、usingで囲む必要があります。

// using ZString.CreateStringBuilder instead of new StringBuilder
using (var sb = ZString.CreateStringBuilder())
{
    sb.Append(enemy.Name);
    sb.Append(" Current HP:");
    sb.Append(enemy.Hp);
    sb.Append(" Current MP:");
    sb.Append(enemy.Mp);
    if (addStatus)
    {
        sb.Append(" Status:");
        sb.Append(enemy.Status);
    }
    return sb.ToString();
}

CreateStringBuilderの戻り値であるUtf16ValueStringBuilderは、structであるため、StringBuilder自体のヒープへのアロケーションが避けられていることと、内部で書き込みに使用するchar[]バッファをArrayPoolから取得することにより、バッファそれ自体のアロケーションを避けています(ただし、そのためusingで使用後にバッファを返却する必要がある)。

また、ZString.Concatは内部でUtf16ValueStringBuilderを利用していることと、15引数までジェネリックでオーバーロードを用意しているため、Unity環境下においても数値型の文字列変換アロケーションを完全に避けることが可能です。

// many allocation(especially on Unity)
enemy.Name + " Current HP:" + enemy.Hp + " Current Mp:" + enemy.Mp;

// zero allocation
// ZString.Concat<T0~T15>(T0 arg0, T1 arg1, T2 arg2,..,T15 arg15)
ZString.Concat(enemy.Name, " Current HP:", enemy.Hp, " Current Mp:", enemy.Mp);

FormatとReadOnlySpan<char>

String interpolationはString.Formatに変換されるため、それ自体のオーバーヘッドはありませんが、String.Format自体の引数がobjectしか受け入れないため、ボクシングは発生します。

// String interpolationによる文字列変換はC#コンパイラによって下記のコードに置き換えられる
$"{enemy.Name} Current Hp:{enemy.Hp} Current Mp:{enemy.Mp}";

// string.Object(string, object, object, object)
String.Format("{0} Current Hp:{1} Current Mp:{2}", enemy.Name, enemy.Hp, enemy.Mp);

// String.Format自体は3引数までは可変長引数を避けられる
string string.Format(string format, object arg0)
string string.Format(string format, object arg0, object arg1)
string string.Format(string format, object arg0, object arg1, object arg2)
string string.Format(string format, params object[] args)

また、StringBuilder.Appendと同様に、.NET Standard 2.0では更に文字列変換のアロケーションが発生します。

ZString.FormatZString.Concatと同様に15引数までジェネリックでオーバーロードが存在することと、.NET Standard 2.0環境でもTryFormatによる直接変換を実装することでゼロアロケーションを実現しています。

// ZString.Format<string, int, int>(string format, T0 arg0, T1 arg1, T2 arg2);
ZString.Format("{0} Current Hp:{1} Current Mp:{2}", enemy.Name, enemy.Hp, enemy.Mp);

String.Format「複合書式指定文字列(Composite Formatting)」をサポートしています。{ index[,alignment][:formatString]}で表現する複合書式文字列は、日付を任意のフォーマットに整形したり、数値の桁数を固定することに使えます。

var x = 123.454321;
var y = 12.34;

// x:123.45, y:0012.3
string.Format("x:{0:.##}, y:{1:0000.#}", x, y);

ZString.FormatはformatStringはサポートしますが、alignmentのサポートはありません。

最終的にこの複合書式文字列は.##のように抽出されて TryFormat に渡されるのですが、改めて ISpanFormattable の定義を見てみましょう。

internal interface ISpanFormattable
{
    bool TryFormat(Span<char> destination, out int charsWritten,
            ReadOnlySpan<char> format, IFormatProvider? provider);
}

string formatではなくてReadOnlySpan<char> formatなことがポイントです。これは文字列を解析した際に得られる書式文字列が、部分的なスライスだからです。上記例の文字列で考えると [x:][.##][, y:][0000.#] というスライスに分割され、それぞれ [x:]と[, y:]はそのままコピー、[.##]と[0000.#]はフォーマット文字列としてTryFormatに渡されます。このように、フォーマット文字列をReadOnlySpan<char>で表現することにより、文字列のアロケーションを避けています。

冒頭で説明したようにStringの実体はUTF-16のバイト列であるため、ReadOnlySpan/Span<char>で表現することができます。ReadOnlySpan<char>stringと違って、部分的な取得が可能なこと、char[]そのものを使用することができるため、プールからの利用が容易になるのが特徴です。

そのためstringをReadOnlySpan<char>として受け取るAPIを用意したほうが性能を出しやすいですが、ref structのためフィールドに保持したりはできないことと、.NET Standard 2.1ではstring -> ReadOnlySpan<char>の暗黙的変換が用意されているためReadOnlySpan<char>を受け取るAPIだけでも使い勝手を損ねませんが、.NET Standard 2.0では.AsSpan()を明示的に記述しないと渡せないため、ユーザービリティは悪くなります。よく注意した設計が必要になるでしょう。

Stringを通さない直接的な書き込み

ZStringの内部実装はゼロアロケーションですが、最終的にstringを生成するところでアロケーションしています。これは、ほとんどのAPIが文字列を要求するためです。しかし、対象ライブラリがstring以外を受け入れるAPIを持っている場合は、この最後の文字列生成も避けて、完全なゼロアロケーションを達成できます。例えばUnityのTextMeshProは SetCharArray(char[] sourceText, int start, int length)というAPIを持っているため、string生成を避けて直接渡すことができます。

TMP_Text tmp;

// create StringBuilder
using(var sb = ZString.CreateStringBuilder())
{
    sb.Append("foo");
    sb.AppendLine(42);
    sb.AppendFormat("{0} {1:.###}", "bar", 123.456789);

    // direct write(avoid string alloc) to TextMeshPro
    tmp.SetText(sb);

    // SetText(Utf16ValueStringBuilder) is the same as following
    var buffer= sb.AsArraySegment();
    tmp.SetCharArray(buffer.Array, buffer.Offset, buffer.Count);
}

// convinient helper to use ZString.Format
tmp.SetTextFormat("Position: {0}, {1}, {2}", x, y, z);

// other ZString direct write utilities
.AsSpan()
.AsMemory()
.TryCopyTo(Span<char>, out int writtenChars);

.NET CoreにおいてもReadOnlySpan<char>を受け取るAPIが増えてきていますし、こうしたstringを避けるアプローチが増えていけば、アプリケーション全体としてより高性能になっていくはずです。

Utf8StringとReadOnlySpan<byte>

ネットワークやファイル入出力においては、最終的に要求されるデータはString(UTF-16)ではなくてbyte[](UTF-8)の場合も多いでしょう。その場合に、 Encoding.UTF8.GetBytes(stringBuilder.ToString())としていたら、char[]書き込み→string生成→UTF-8エンコーディングと、かなりの無駄があります。直接UTF-8で書き込めばオーバーヘッドはゼロになります。そのためにZStringではCreateUtf8StringBuilderというメソッドを用意しています。

using(var sb = ZString.CreateUtf8StringBuilder())
using(var fs = File.Open("foo.txt", FileMode.OpenOrCreate))
{
    sb.Append("foo");
    sb.AppendLine(42);
    sb.AppendFormat("{0} {1:.###}", "bar", 123.456789);

    // write inner Utf8 buffer to stream
    await sb.WriteToAsync(fs);

    // or get inner buffer
    // .AsSpan(), .AsMemory(), TryCopyTo
}

頻繁な書き込みが必要なネットワークにおくるメトリクスデータ生成や、テンプレートエンジンのバックエンドなどで有効に使えるはずです。

数値型のUTF-8への直接書き込みには System.Buffers.Text.Utf8Formatterを使用しています。これらは Span<byte>に書き込むためのTryFormatメソッドを持っています。

public static bool TryFormat(int value, Span<byte> destination,
    out int bytesWritten, StandardFormat format = default)

つまり、string(Utf16)がReadOnlySpan<char>と表せるように、UTF-8はReadOnlySpan<byte>として表せるということです。

まとめ

C#のString完全に理解した。と言える程度に踏み込んで解説しました。Stringは基本の型ですが、基本の型であるがゆえに、単純なようで多くの特殊な動作が詰まっています。そして、普通に使っているだけでは、それらの落とし穴をすべて避けるのは不可能と言っていいでしょう。ZStringはString/StringBuilderと似たようなAPIを提供することで、ここで述べた複雑な詳細について考えなくても、単純に置き換えるだけでベストなパフォーマンスを叩き出せるように設計されています。

Cygamesでも既にプロジェクトのパフォーマンス最適化のため使用を始めています。ぜひ、皆さんもお試しいただければ幸いです。

【Developpers Summit 2020フォローアップ】グランブルーファンタジーを支えるサーバーサイドの技術

$
0
0

こんにちは。サーバーサイドエンジニアの小松・大橋です。
2020年2月13日・14日に開催された「Developpers Summit 2020(デブサミ2020)」において、『グランブルーファンタジーを支えるサーバーサイドの技術』と題した講演を行いました。ご参加いただいた皆様、当日は素晴らしい時を共有させていただき、ありがとうございました。

なお、この講演はWebメディア「CodeZine」でも取り上げていただきました。ご興味のある方はご参照いただければと思います。

講演資料はこちらになります。

本稿では、講演でお伝えし切れなかったことや、質問を多くいただいた事項をフォローアップしていきます。

中長期的な改善の意義について

講演では、中長期的な改善の意義についてお話ししました。
我々にとって最も大切なのは、お客様に快適にプレイしていただくことです。そして、トラブルを未然に防ぐことも目的の一つであるとお話ししました。
本稿では、さらにもう一つの観点をお伝えしたいと思います。

サーバーリソースの最適化

講演で話し切れなかったもう一つの観点は、サーバーリソースの最適化です。
サーバーリソースを最適に利用できるように進めることは、未来のコンテンツ開発と運営の品質に良い影響を与えます。環境によっては、ビジネスの成否に影響することもあるでしょう。

中長期的な改善は、このサーバーリソースの最適化に繋がります。例えばレスポンス速度は、外部サービスへの問い合わせなどを除くと基本的にCPUリソースの占有度合いに左右されます。このレスポンス速度を本質的に改善していくことは、CPUリソースの最適化に繋がります。

レスポンス速度を改善する一つの選択肢としてスケールアウトが挙げられますが、表面的な解決策で満足せず、「当たり前」ともいえる小さな改善を、中長期的に積み重ねていくことが大切です。

ORM vs 連想配列について

講演では、チューニングの事例としてORMの改善例をご紹介しました。この事例に関しては、講演後の Ask the speakerの時間にも多くの反響をいただきました。特に「ORMと連想配列のどちらを採用すべきか」というテーマについては複数の方から質問を受けました。そこで本稿では、フレームワークのORMを改善するに至った理由をお伝えできればと思います。

プロパティ定義と型の静的定義

講演でもお伝えした通り、従来採用していたORMには『グランブルーファンタジー(以下、グラブル)』にとって不要な複数の機能が存在しました。しかし、ORMの機能すべてが不要なのではなく、ORMの優れた面はグラブルにとっても必要だと考えています。

ORMでは、プロパティと型を静的に定義することで表面的なデータ構造を可視化できます。更新頻度が高く、複数人で継続的なアップデートを続ける開発チームではデータ構造の可視化は大きなメリットです。

一方、ORMの代替として連想配列を採用した場合、ソースコードの保守性・可読性などの複数の観点が弱くなります。グラブルの膨大な更新を支えるうえで、不安材料が残ります。そのために、グラブルに最適なORMを目指して改善を行うことにしました。

PHPのコピーオンライト戦略

PHPの連想配列には、暗黙的コピーが行われるパターンがあります。大規模な開発チームにおいて、暗黙的コピーが行われることで潜在的にチューニングコストが高い(可視化が極めて困難な)問題箇所になる可能性があります。

その点では、ORMクラスのインスタンス(オプジェクト)の扱いは比較的簡単になります。コピーは明示され、エンジニアの意図通りに動作することを担保しやすくなります。PHPにおける連想配列のこの性質も、ORMの改善を決めた理由の一つです。

処理速度の違い

最も大きなポイントですが、連想配列よりも、改善後のORMプロパティ参照が高速であることです。グラブルの性質に合わせて最適化した結果、保守性と可読性を担保しつつ、連想配列よりも高速な参照が行えるようになりました。確かな結果が出たからこそ、グラブルに最適なORMを採用するに至りました。

以上が、我々がORMの改善に踏み切った大きな理由です。これにより、開発者は「ゲームを面白くすること」により集中できるようになり、結果的に生産性および品質の向上につながると考えています。

インフラ構成はどうなっているのか

講演に関する反響の中には、インフラ構成を知りたいというご意見もありました。グラブルのインフラに関しては、「Developpers Summit 2017」における『グランブルーファンタジーを支えるインフラの技術』という講演のフォローアップ記事を公開していますのでご覧ください。

なお、今回の講演タイトル(『グランブルーファンタジーを支えるサーバーサイドの技術』)と、講演中にお伝えした「コア技術は自分たちで実装する」という表現は、この2017年の講演に大きく影響を受けたものであることを付記しておきます。

終わりに

今回の講演や本稿では、『グランブルーファンタジーを支えるサーバーサイドの技術』と題し、サーバーサイドエンジニアの視点での取り組みをご紹介しました。

リリース以来6年間、開発の現場では「お客様に最高のコンテンツを届けたい」という共通の目標に向かい、サーバーサイドのエンジニアのみならず、インフラチームやデバッグチームなど各部署が連携しながら、日々の運用と問題解決・改善に取り組んできました。

今後も同じ気持ちで、チーム一同励んでいきたいと思っておりますので、これからもグラブルをよろしくお願いいたします。

以上、小松・大橋のフォローアップでした。最後まで読んでいただきありがとうございました。

ZLogger – .NET CoreとUnityのためのゼロアロケーション構造化ロガー

$
0
0

Cy#の河合です。今回、.NET CoreアプリケーションとUnityのために新しく「ZLogger」というロガーを公開しました。非常に高速、かつ、以前に紹介したZStringをバックエンドに持つことにより、文字列整形時のアロケーションをなくしています。主に .NET Core 向けに開発したものですが、Unity(IL2CPPやモバイル環境を含む)でも動くように調整してあります。

GitHub – Cysharp/ZLogger

特にコンテナ化において、標準出力は重要です。例えばDatadog LogsStackdriver Loggingは、コンテナの標準出力から直接ログを収集します。Thw Twelve-Factor App – XI. Logsの項でも、標準出力への書き出しが推奨されています。そのような中で、ローカル環境用の装飾過多なログや、低速なConsole.WriteLineで標準出力を扱うのは、あまりにも古い考えだと言えますし、残念ながら今まで .NET Core ではそこを重要視したライブラリは存在しませんでした。

旧来のロギング処理は、多くの無駄が存在しています。

78019524-d4238480-738a-11ea-88ac-00caa8bc5228

標準的なString.Formatは、値を全てobjectで受け取るためにボクシングが発生し、更にUTF16のStringを新しく生成します。このStringをUTF8でエンコードして、最終的なストリームへと書き込む流れになりますが、ZLoggerの場合はZStringを用いてフォーマット文字列を直接バッファ域にUTF8として書き込み、ConsoleStreamにまとめて流し込みます。プリミティブ型のボクシングも発生せず、非同期で一気に書き込むため、呼び出し側、そしてアプリケーション全体に負荷を与えません。

また、標準出力への最適化のほか、ファイルへの書き込みなどのプロバイダーなど通常のロガーとして期待される内容も標準で用意しているため、様々なところで今すぐ使えます。


ロガーのセットアップはGeneric HostによるConfigureLoggingに従います。そのためMicrosoft.Extensions.Loggingによるフィルタリングなども設定できます。

using ZLogger;

Host.CreateDefaultBuilder()
    .ConfigureLogging(logging =>
    {
        // optional(MS.E.Logging):clear default providers.
        logging.ClearProviders();

        // optional(MS.E.Logging): default is Info, you can use this or AddFilter to filtering log.
        logging.SetMinimumLevel(LogLevel.Debug);

        // Add Console Logging.
        logging.AddZLoggerConsole();

        // Add File Logging.
        logging.AddZLoggerFile("fileName.log");

        // Add Rolling File Logging.
        logging.AddZLoggerRollingFile((dt, x) => $"logs/{dt.ToLocalTime():yyyy-MM-dd}_{x:000}.log", x => x.ToLocalTime().Date, 1024);

        // Enable Structured Logging
        logging.AddZLoggerConsole(options =>
        {
            options.EnableStructuredLogging = true;
        });
    })
public class MyClass
{
    readonly ILogger<MyClass> logger;

    // get logger from DI.
    public class MyClass(ILogger<MyClass> logger)
    {
        this.logger = logger;
    }

    public void Foo()
    {
        // log text.
        logger.ZLogDebug("foo{0} bar{1}", 10, 20);

        // log text with structure in Structured Logging.
        logger.ZLogDebugWithPayload(new { Foo = 10, Bar = 20 }, "foo{0} bar{1}", 10, 20);
    }
}

基本的にロガーはDIによって注入されます(LogManager.GetLoggerのような使い方がしたい場合はReadMe#Global LoggerFactoryを参照ください)。LogDebugやLogInformationの代わりに、先頭にZをつけたZLogDebug, ZLogInformationを使うのが基本的な流れです。

標準のLogメソッドは(string format, object[] args)というメソッド定義のため、どうしてもボクシングを避けられません。そのため、独自のジェネリクスで用意した大量のオーバーロードを定義してあります。

// ZLog, ZLogTrace, ZLogDebug, ZLogInformation, ZLogWarning, ZLogError, ZLogCritical and *WithPayload.
public static void ZLogDebug(this ILogger logger, string format)
public static void ZLogDebug(this ILogger logger, EventId eventId, string format)
public static void ZLogDebug(this ILogger logger, Exception? exception, string format)
public static void ZLogDebug(this ILogger logger, EventId eventId, Exception? exception, string format)
public static void ZLogDebug<T1>(this ILogger logger, string format, T1 arg1)
public static void ZLogDebug<T1>(this ILogger logger, EventId eventId, string format, T1 arg1)
public static void ZLogDebug<T1>(this ILogger logger, Exception? exception, string format, T1 arg1)
public static void ZLogDebug<T1>(this ILogger logger, EventId eventId, Exception? exception, string format, T1 arg1)
// T1~T16
public static void ZLogDebug<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16>(this ILogger logger, EventId eventId, Exception? exception, string format, T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7, T8 arg8, T9 arg9, T10 arg10, T11 arg11, T12 arg12, T13 arg13, T14 arg14, T15 arg15, T16 arg16)

標準のLogメソッドと、ZLogメソッドが混在することにより、ミスしてしまう(ZLogメソッドを使うつもりが誤ってLogメソッドを使ってしまう)可能性が非常に高いですが、これは Microsoft.CodeAnalysis.BannedApiAnalyzersを使用することで、コード上で標準Logメソッドを警告/エラー扱いにできます。

このBannedApiAnalyzersを設定することにより、確実にZLoggerを高パフォーマンスで使用できます。

構造化ロギング

ログ管理のクラウドサービス、例えばDatadog LogsやStackdriver Loggingなどは、柔軟なフィルタリングや検索、式を使ったクエリを行えますが、その機能をフルに活かすためにはログが適切にパースされている必要があります。構造化ロギング(Structured Logging, Semantic Logging)は、ログをJSONで送ることにより、特別な後処理をせずログサービスに読み込ませることができます。ZLoggerでは EnableStructuredLogging を有効にすることでテキストメッセージによる出力と、JSON出力を切り替えることができます。また、複数のログプロパイダでそれぞれ切り替えることができるので、コンソール出力はテキストメッセージ、ファイル出力はJSONといった設定も可能です。

logging.AddZLoggerConsole(options =>
{
    options.EnableStructuredLogging = true;
});

// In default, output JSON with log information(categoryName, level, timestamp, exception), message and payload(if exists).

// {"CategoryName":"ConsoleApp.Program","LogLevel":"Information","EventId":0,"EventIdName":null,"Timestamp":"2020-04-07T11:53:22.3867872+00:00","Exception":null,"Message":"Registered User: Id = 10, UserName = Mike","Payload":null}
logger.ZLogInformation("Registered User: Id = {0}, UserName = {1}", id, userName);

// {"CategoryName":"ConsoleApp.Program","LogLevel":"Information","EventId":0,"EventIdName":null,"Timestamp":"2020-04-07T11:53:22.3867872+00:00","Exception":null,"Message":"Registered User: Id = 10, UserName = Mike","Payload":{"Id":10,"Name":"Mike"}}
logger.ZLogInformationWithPayload(new UserRegisteredLog { Id = id, Name = userName }, "Registered User: Id = {0}, UserName = {1}", id, userName);

構造化ロギングにおいても、 System.Text.Json の Utf8JsonWriter によるUTF8への直接の書き込み処理や IBufferWriterを活用した透過的なバッファーの受け渡しによって、JSON処理に関連する一切のアロケーションや、余計なコピーの発生によるパフォーマンス低下を徹底的に防いでいます。

Microsoft.Extensions.Loggingへの直接の実装

現在、ほとんどの .NET Core アプリケーションは .NET Generic Hostの上に構築されているため(コンソールアプリケーションでさえも!例えばCy#の開発しているConsoleAppFrameworkは簡単にCLIアプリケーションを.NET Generic Host上で動かせます)、Microsoft.Extensionsによるコンフィグの読み込み、DI、ロガーが標準で動いています。

多くのロガーはそれぞれ別個のフレームワークを持ち、Microsoft.Extensions.Loggingへのブリッジという形で統合を実現していますが、ログ出力の際に2つのフレームワークを通ることはパイプラインが長くなり、性能劣化に繋がります。

Microsoft.Extensions.Loggingはログレベル、フィルタリング、複数の出力先の登録など、十分にフレームワークとしての機能を持っています。よって、独自のロギングフレームワークを作らず、可能な限り直接 Microsoft.Extensions.Logging の上に実装することにより、オーバーヘッドを極限まで減らしています。

また、Microsoft.Extensions.Loggingの出力用のプロバイダーは現状最小限、あるいはAzure依存のものしか提供されていないため、それだけで実用とするのは難しいところがありました。それが他のロギングフレームワークを必要としていた理由ですが、ZLoggerは性能面以外に、Console, File, RollingFile, Streamといった十分なプロバイダーを用意することで、Microsoft.Extensions.Loggingのみでも実用レベルに押し上げてます。

また、初期化/終了処理などが全て .NET Generic Host に統合されているため、ロガーの設定も非常にシンプルで済んでいます。

async Task Main()
{
    await Host.CreateDefaultBuilder()
        .ConfigureLogging(logging =>
        {
            logging.ClearProviders();
            logging.AddZLoggerConsole();
        })
        .RunAsync();
}

Unity

UnityにおいてはDebug.Log(中身はDebug.unityLogger -> DebugLogHandler -> Internal_Log)しかない、わけではなくて、一応 ILogger, ILogHandler という抽象層が用意されていますが、非常に貧弱かつ、実装がDebugLogHandlerしか存在しない、かつ、Debug.unityLoggerが差し替え不能なので、ほとんどロギングフレームワークとして機能していないでしょう。

ZLoggerをUnityで使うと、標準に比べて「ファイル出力を含む複数のログプロパイダ」「標準的なログレベルとフィルタリング」「ロガー毎のカテゴリ化」などが利点となります。ファイル出力はモバイルアプリケーションではあまり使い道がないですが、VRなどPCアプリケーションでは存在すると嬉しいのではないでしょうか。

また、ロガーによるカテゴリ付与などは、EditorConsoleProなどの、標準よりも強力なログコンソールでのフィルタリングに非常に役立ちます(例えば[UI], [Battle], [Network]などの分類の付与)。

まとめ

Cy#では現在開発中のアプリケーションにおいて、 .NET Core/MagicOnion で実装されたコンテナ上でホスティングされているリアルタイムサーバーのログを Datadog Logs に標準出力経由で、構造化ログとして送っています。

ロギングは昔からパフォーマンス上のネックになりやすい、かつ、現在でもその流量の多さから扱いに関しては慎重に考慮しなければならない領域でした。ZLoggerは余計な設定もなく、標準状態で最大のパフォーマンスを発揮するように設計されています。この現代的観点から再設計された、新しいロギングライブラリ、是非試してみてください。

UniTask v2 – Unityのためのゼロアロケーションasync/awaitと非同期LINQ

$
0
0

Cy#の河合です。去年、UniTask – Unityでasync/awaitを最高のパフォーマンスで実現するライブラリという形で紹介させていただきましたが、今回全てのコードを書き換えた新バージョンをリリースしました。

GitHub – Cysharp/UniTask

UniTask v2では、コードの徹底的な書き換えにより、ほぼ全てがゼロアロケーション化しました(技術的詳細は後ほど述べます)。これによりパフォーマンスの大幅な向上を果たしているほか、新たに非同期シーケンスと、それに対応する非同期LINQが組み込まれました。その他、DOTweenやAddressableなどの外部アセットに対するawait標準対応も組み込まれ、より利便性が高まっています。

v2の前に、まず、async/await はC# 5.0から搭載されている機能で、従来コールバックの連鎖やコルーチンで処理していた非同期コードを、同期コードで書くように、戻り値も例外処理も自然に扱えるようになります。コールバックのみで処理する場合、複雑な処理でネストが多重になること、その際に内側の例外が外側に伝搬されないためエラー処理が難しくなることなどがよく知られています。

FooAsync(x =>
{
    BarAsync(x, y =>
    {
        BazAsync(y, z =>
        {
        });
    });
});

Unityの場合はyield return(ジェネレーター)で実現しているコルーチンで非同期処理を行うことができるため、ある程度はネストを平らにすることができますが、文法上の制約で戻り値を扱うことができないため、合わせてデリゲートを渡したりなどして処理していました。

IEnumerator FooCoroutine(Func<int> resultCallback, Func<Exception> exceptionCallback)
{
    int x = 0;
    Exception error = null;
    yield return DoAsync(v => x = v, ex => error = ex);

    if (error == null)
    {
        resultCallback(x);
    }
    else
    {
        exceptionCallback(error);
    }
}

これでネストの階層をある程度は平らにすることはできましたが

  • 依然として残る煩雑なコールバック処理
  • yield構文の制約でtry-catch-finallyが使えない
  • ラムダ式とコルーチン自体のアロケーション
  • ライフサイクルが紐付いているGameObjectのDestroyに伴うキャンセル処理ができない
  • 複数コルーチンのコントロール(直列/並列処理)が不可能

といった問題がありました。

async/awaitでは

async UniTask<int> FooAsync(int x)
{
    try
    {
        var y = await BarAsync(x);
        var z = await BazAsync(y);
        return x + y + z;
    }
    catch (Exception ex)
    {
    }
    finally
    {
    }
}

といったように、言語レベルのサポートで同期コードとほぼ変わらない記述で非同期コードを書けるようにしているのが強みです。

しかし、Unityのフレームワーク自体があまりasync/awaitをサポートしていないため、そのままでは決して使えるとは言い難い状況でした。UniTaskはC#の言語サポートをベースに以下を提供します。

  • Unityの各AsyncOperationに対するawait対応
  • UnityのPlayerLoopベースのスイッチ処理(Yield, Delay, DelayFrame, etc…)によってコルーチンで可能な全機能をUniTask上で実現
  • MonoBehaviourイベントやuGUIイベントのawait対応

値型ベースの独自の UniTask 型と専用の AsyncMethodBuilder を実装することで .NET 標準の Task と Unityにおいて不要な ExecutionContext/SynchronizationContext を通さずに処理することでUnityに最適化されたパフォーマンスを実現

// Unityの非同期オブジェクトをそのまま待てる
var asset = await Resources.LoadAsync<TextAsset>("foo");

// 100フレーム待つなどフレームベースの待機(コルーチンの代わり)
await UniTask.DelayFrame(100);

// WaitForFixedUpdateの代わり
await UniTask.Yield(PlayerLoopTiming.FixedUpdate);

これらの対応により、Unityにおいても async/await のパワーを100%活かせるような環境になりました。

そして、最初のリリースから2年を経て状況も変わってきました。 .NET Core も3.1、そして .NET 5 が発表されてランタイムも書き換わっていってますし、Unityでも C# 8.0 が見えてきました。そこで、上記の要素は引き継ぎつつ、新たにAPIの全面的な見直しと

  • asyncメソッド全体のゼロアロケーション化による更なるパフォーマンスの向上
  • 非同期LINQ(UniTaskAsyncEnumerable, Channel, AsyncReactiveProperty)
  • PlayerLoopタイミングの増加(新しいLastPostLateUpdateによってWaitForEndOfFrameと同じ効果が見込める)
  • Addressable, DOTweenといった外部アセットのawait対応

という、性能改善と機能追加を実装しました。特にゼロアロケーション化は、async/awaitを多用してもGCを抑えることができるため、大きなパフォーマンス向上が見込めます。また、それに合わせて .NET Core の ValueTask/IValueTaskSource と同様の挙動になるように調整しました(Delayなど全てのFactoryがTaskと同様に呼び出し時に起動、二度await時に例外throwなど)。これによって独自のUniTaskによるパフォーマンス向上のメリットを得つつも、挙動に関しては標準合わせにすることで、学習のギャップを抑えています。

Unity 2020.2.0a12から C# 8.0 がサポートされたことにより、非同期ストリームに関する記法が可能になっています。そこでUniTask v2では非同期ストリームをサポートする UniTaskAsyncEnumerable を追加しました。

// Unity 2020.2.0a12~, C# 8.0
await foreach (var _ in UniTaskAsyncEnumerable.EveryUpdate())
{
    Debug.Log("Update() " + Time.frameCount);
}

// C# 7.3(Unity 2018.3~)
await UniTaskAsyncEnumerable.EveryUpdate().ForEachAsync(_ =>
{
    Debug.Log("Update() " + Time.frameCount);
});

C# 8.0 がサポートされていないUnity 2018, 2019, 2020.1 においても搭載されている非同期LINQを併用することで、ほぼ同じような処理が可能になっています。また、LINQなので、全ての標準LINQクエリ演算子が非同期ストリームに適用可能です。例えば、以下のコードはボタンクリック非同期ストリームに対して2回に1回処理が走るWhereフィルターをかけた例です。

await okButton.OnClickAsAsyncEnumerable().Where((x, i) => i % 2 == 0).ForEachAsync(_ =>
{
});

ボタンクリック以外にも、多くのUnityと融合した豊富な非同期ストリームファクトリーが提供されているので、工夫次第であらゆる処理が書けるようになります。

AsyncStateMachineの原理とゼロアロケーション化

非同期ストリーム対応など多くの新機能がありますが、UniTask v2最大の特徴はパフォーマンスの大幅な向上です。

標準のTask実装による async/await と比較して、どこでアロケーションが発生していて、どのようにそれを抑止したのかを解説します。以下のような比較的単純な非同期メソッドを例に、構造を分解して行きましょう。

public class Loader
{
    object cache;

    public async Task<object> LoadAssetFromCacheAsync(string address)
    {
        if (cache == null)
        {
            cache = await LoadAssetAsync(address);
        }
        return cache;
    }

    Task<object> LoadAssetAsync(string address)
    {
        // do something...
    }
}

もし読み込み済みのキャッシュがあればそれを返し、そうでなければ実際に非同期で読み込み処理をするコードです。この await はコンパイル時に GetAwaiter -> IsCompleted/GetResult/UnsafeOnCompleted に分解されて呼ばれます。

public class Loader
{
    public Task<object> LoadAssetFromCacheAsync(string address)
    {
        if (cache == null)
        {
            var awaiter = LoadAssetAsync(address).GetAwaiter();
            if (awaiter.IsCompleted)
            {
                cache = awaiter.GetResult();
            }
            else
            {
                // regsiter callback, where from moveNext and promise?
                awaiter.UnsafeOnCompleted(moveNext);
                return promise;
            }
        }
        return Task.FromResult(cache);
    }
}

これは最適化の一種で、コールバックが不要な場合に(例えばこのLoadAssetFromCacheAsync自身もキャッシュ済みならば即座に値を返します)、コールバックの生成/登録/呼び出しのコストを避けることができます。

async で宣言されたメソッドはコンパイラによってステートマシンに変換されます。このステートマシンを進める(MoveNext)メソッドがawaitのコールバックに登録される仕組みとなっています。

public class Loader
{
    object cache;

    public Task<object> LoadAssetFromCacheAsync(string address)
    {
        var stateMachine = new __LoadAssetFromCacheAsync
        {
            __this = this,
            address = address,
            builder = AsyncTaskMethodBuilder<object>.Create(),
            state = -1
        };

        var builder = stateMachine.builder;
        builder.Start(ref stateMachine);

        return stateMachine.builder.Task;
    }

    // compiler generated async-statemachine
    // Note: in debug build statemachine as class.
    struct __LoadAssetFromCacheAsync : IAsyncStateMachine
    {
        // local variables to field.
        public Loader __this;
        public string address;

        // internal state
        public AsyncTaskMethodBuilder<object> builder;
        public int state;

        // internal local variables
        TaskAwaiter<object> loadAssetAsyncAwaiter;

        public void MoveNext()
        {
            try
            {
                switch (state)
                {
                    // initial(call from builder.Start)
                    case -1:
                        if (__this.cache != null)
                        {
                            goto RETURN;
                        }
                        else
                        {
                            // await LoadAssetAsync(address)
                            loadAssetAsyncAwaiter = __this.LoadAssetAsync(address).GetAwaiter();
                            if (loadAssetAsyncAwaiter.IsCompleted)
                            {
                                goto case 0;
                            }
                            else
                            {
                                state = 0;
                                builder.AwaitUnsafeOnCompleted(ref loadAssetAsyncAwaiter, ref this);
                                return; // when call MoveNext again, goto case 0:
                            }
                        }
                    case 0:
                        __this.cache = loadAssetAsyncAwaiter.GetResult();
                        goto RETURN;
                    default:
                        break;
                }
            }
            catch (Exception ex)
            {
                state = -2;
                builder.SetException(ex);
                return;
            }

            RETURN:
            state = -2;
            builder.SetResult(__this.cache);
        }

        public void SetStateMachine(IAsyncStateMachine stateMachine)
        {
            builder.SetStateMachine(stateMachine);
        }
    }
}

StateMachineの実装はやや長くなっていますが、基本的にはawaitの書かれた行で分割されてstateが一つ進む、と捉えてください。後述のBuilderとセットになっているため少し読み取りにくいのですが、awaiterはIsCompletedがtrueの場合に即座にGetResultを呼び、falseの場合はUnsafeOnCompletedに自身のMoveNextをセットします。このMoveNextは非同期処理が完了した場合に再度呼び出されて、自身のawaiterのGetResultを呼んで結果を取得するという流れになります。

最後の登場人物が AsyncTaskMethodBuilder で、これはコンパイラ生成ではなく、戻り値であるTaskと1:1で対応するビルダークラスとなっています。元のソースは少し長いので、簡略化したコードを掲示します。

public struct AsyncTaskMethodBuilder<TResult>
{
    MoveNextRunner runner;
    Task<TResult> task;

    public static AsyncTaskMethodBuilder<TResult> Create()
    {
        return default;
    }

    public void Start<TStateMachine>(ref TStateMachine stateMachine)
        where TStateMachine : IAsyncStateMachine
    {
        // when start, call stateMachine's MoveNext directly.
        stateMachine.MoveNext();
    }

    public Task<TResult> Task
    {
        get
        {
            if (task == null)
            {
                // internal task creation(same as TaskCompletionSource but avoid tcs allocation)
                task = new Task<TResult>();
            }

            return task.Task;
        }
    }

    public void AwaitUnsafeOnCompleted<TAwaiter, TStateMachine>(ref TAwaiter awaiter, ref TStateMachine stateMachine)
        where TAwaiter : ICriticalNotifyCompletion
        where TStateMachine : IAsyncStateMachine
    {
        // at first await, copy struct state machine to heap(boxed).
        if (runner == null)
        {
            _ = Task; // create TaskCompletionSource

            // create runner
            runner = new MoveNextRunner((IAsyncStateMachine)stateMachine); // boxed.
        }

        // set cached moveNext delegate(as continuation).
        awaiter.UnsafeOnCompleted(runner.CachedDelegate);
    }

    public void SetResult(TResult result)
    {
        if (task == null)
        {
            _ = Task; // create Task
            task.TrySetResult(result); // same as TaskCompletionSource.TrySetResult.
        }
        else
        {
            task.TrySetResult(result);
        }
    }
}

public class MoveNextRunner
{
    public Action CachedDelegate;

    IAsyncStateMachine stateMachine;

    public MoveNextRunner(IAsyncStateMachine stateMachine)
    {
        this.stateMachine = stateMachine;
        this.CachedDelegate = Run; // Create cached delegate.
    }

    public void Run()
    {
        stateMachine.MoveNext();
    }
}

Builderには初回呼び出し時(Start), 戻り値であるTaskの取得、コールバックの登録時(AwaitUnsafeOnCompleted)、結果設定時(SetResult/SetException)の実際の処理を記述されています。

awaitの連鎖はコールバックの連鎖と似ていますが、手でコールバックの連鎖を書いた場合はラムダ式のアロケーションの発生を避けられませんが、async/awaitはコンパイラの生成した単一のデリゲートによって全ての処理が回るため、アロケーションが少なくすみます。これらの仕組みにより、async/awaitで書いたほうが、手書きよりもむしろ高性能になります。

これでようやく全てのパーツが出揃いました。async/awaitとTaskには、コンパイラ生成を活かした細かい最適化も入っていて基本的にはとても良いのですが、問題点も幾つか残っています。

メモリアロケーションという観点で考えると、ワーストケースで以下の4つのアロケーションが発生します。

  • Taskのアロケーション
  • AsyncStateMachineのBox化
  • AsyncStateMachineを内包したRunnerのアロケーション
  • MoveNextのためのデリゲートのアロケーション

戻り値をTaskで宣言する都合上、たとえ値が即時返しの状態でも、必ずTaskのアロケーションが発生するのが特に頂けません。この問題に対処するために .NET Standard 2.1 ではValueTask型が導入されましたが、コールバックが必要な場合には変わらずTaskのアロケーションが存在することと、AsyncStateMachineのBox化なども存在しています。このアロケーションは、いったん現在の実行状態を解放してStateMachineをヒープ上に置かなければならない都合上、どうしても起こり得るものです。

これらの問題をUniTaskではC# 7.0から搭載されたカスタムAsyncMethodBuilderによって、独自実装に全て置き換えることで解消しました。

// modify Task<T> -> UniTask<T> only.
public async UniTask<object> LoadAssetFromCacheAsync(string address)
{
    if (cache == null)
    {
        cache = await LoadAssetAsync(address);
    }
    return cache;
}

// Compiler generated code is same as standard Task.
public UniTask<object> LoadAssetFromCacheAsync(string address)
{
    var stateMachine = new __LoadAssetFromCacheAsync
    {
        __this = this,
        address = address,
        builder = AsyncUniTaskMethodBuilder<object>.Create(),
        state = -1
    };

    var builder = stateMachine.builder;
    builder.Start(ref stateMachine);

    return stateMachine.builder.Task;
}

// UniTask's AsyncMethodBuilder
public struct AsyncUniTaskMethodBuilder<T>
{
    internal IStateMachineRunnerPromise<T> runnerPromise;
    T result;

    public UniTask<T> Task
    {
        get
        {
            // when registered callback
            if (runnerPromise != null)
            {
                return runnerPromise.Task;
            }
            else
            {
                // sync complete, return struct wrapped result
                return UniTask.FromResult(result);
            }
        }
    }

    public void AwaitUnsafeOnCompleted<TAwaiter, TStateMachine>(ref TAwaiter awaiter, ref TStateMachine stateMachine)
        where TAwaiter : ICriticalNotifyCompletion
        where TStateMachine : IAsyncStateMachine
    {
        if (runnerPromise == null)
        {
            // get Promise/StateMachineRunner from object pool
            AsyncUniTask<TStateMachine, T>.SetStateMachine(ref this, ref stateMachine);
        }

        awaiter.UnsafeOnCompleted(runnerPromise.MoveNext);
    }

    public void SetResult(T result)
    {
        if (runnerPromise == null)
        {
            this.result = result;
        }
        else
        {
            // SetResult signal Task continuation, it will call task.GetResult and finally return to pool self.
            runnerPromise.SetResult(result);

            // AsyncUniTask<TStateMachine, T>.GetResult
            /*
            try
            {
                return core.GetResult(token);
            }
            finally
            {
                TryReturn();
            }
            */
        }
    }
}

値型のUniTaskの導入によって値が即時返しの場合のアロケーションに対処し、専用のAsyncUniTaskMethodBuilderと、強く型付けされた(boxingの発生しない)MoveNextRunnerは戻り値のTaskと一体化することにより更にメモリ消費量を低減したうえに、オブジェクトプールから取得されて、Taskのawait呼び出しが完了した際(GetResult)にプールに戻ります。これによってTaskとステートマシン関連のアロケーションが完全になくなりました。

awaitが完了した際に自動的にプールに戻るため、制限として全てのUniTaskオブジェクトは二度awaitすることができなくなっています。

これは .NET Standard 2.1 で導入される ValueTask/IValueTaskSourceと同様の制約になります。

The following operations should never be performed on a ValueTask instance:

  • Awaiting the instance multiple times.
  • Calling AsTask multiple times.
  • Using .Result or .GetAwaiter().GetResult() when the operation hasn’t yet completed, or using them multiple times.
  • Using more than one of these techniques to consume the instance.

If you do any of the above, the results are undefined.

多少の不自由さはありますが、逆にこの制約のお陰で、アグレッシブなプーリングが可能になりました。

なお、こうしたゼロアロケーションによるasync/awaitは、実装は同じではありませんが、 .NET 5で Async ValueTask Pooling in .NET 5 として導入される予定があります。UniTask v2は遠い先のランタイムの更新を待たずして、今すぐにUnityで実現されます。

Unity Editor上やDevelopment Buildでプロファイラーで監視するとアロケーションが確認できるかもしれません。これは、C#コンパイラの生成するAsyncStateMachineがデバッグビルド時はclassになっているためです。リリースビルド時はstructになるため、アロケーションはなくなります。

プールのサイズはデフォルトでは無制限になっていますが、 TaskPool.SetMaxPoolSize で最大サイズの設定と TaskPool.GetCacheSizeInfo で現在キャッシュ中の数を取得できます。 .NET Core と違いGCによるインパクトの大きいUnityのため、積極的にプールするようにしていますが、アプリケーションによっては調整したほうが良い可能性もあります。

コルーチンとPlayerLoop

Taskと異なるUniTaskの大きな特徴として、(ExecutionContextと)SynchronizationContextを一切使わないことにあります。SynchronizationContextとは、await時に元の同期コンテキスト、Unityの場合だとメインスレッドに自動的に戻してくれる機能です(UnitySynchronizationContextというものが用意されています)。これは一見便利ですが、awaitの度に同期コンテキストのキャプチャの処理が必要というオーバーヘッドがあることと、そもそもUnityの非同期処理(AsyncOperation)はUnityのエンジン(C++)側がメインスレッドに戻してイベントを呼びだす仕組みになっているため、C#側でメインスレッドに戻す機構をあまり必要としない事情もあります。そこで大胆にSynchronizationContextをカットすることで、より軽量化された実行処理を手にいれました。

もう一つ、SynchronizationContextは戻ってくる箇所が一つだけですが、Unityの場合、実行シーケンスの呼び出し箇所を細かく制御するシチュエーションが多くあります。例えばコルーチンでもWaitForEndOfFrameやWaitForFixedUpdateなどで、実行する箇所を調整することがよくあります。

そこで、UniTaskでは単一のSynchronizationContextの代わりに、手動で戻る実行シーケンスの箇所を指定できる形式としました。

// PlayerLoop.Updateで実行する(yield return nullに等しい)
await UniTask.Yield();

// yield return new WaitForEndOfFrameに等しい
await UniTask.Yield(PlayerLoopTiming.LastPostLateUpdate);

// ここより下の処理をThreadPool上で処理
await UniTask.SwitchToThreadPool();

// PreUpdateで指定時間待つ(WaitForSecondsに近いが、呼び出し箇所が指定できる)
await UniTask.Delay(TimeSpan.FromSeconds(1), delayTiming: PlayerLoopTiming.PreUpdate);

// 30フレーム毎にUpdateを呼び出し
await UniTaskAsyncEnumerable.IntervalFrame(30, PlayerLoopTiming.Update).ForEachAsync(_ =>
{
});

現在Unityでは標準でPlayerLoopという仕組みによって全てのイベント関数が駆動しています。以下はその一覧で、UniTaskは先頭と最後にそれぞれ注入し、合計14箇所から選択できるようにしています。

Initialization
---
**UniTaskLoopRunnerYieldInitialization**
**UniTaskLoopRunnerInitialization**
PlayerUpdateTime
DirectorSampleTime
AsyncUploadTimeSlicedUpdate
SynchronizeInputs
SynchronizeState
XREarlyUpdate
**UniTaskLoopRunnerLastYieldInitialization**
**UniTaskLoopRunnerLastInitialization**  

EarlyUpdate
---
**UniTaskLoopRunnerYieldEarlyUpdate**
**UniTaskLoopRunnerEarlyUpdate**
PollPlayerConnection
ProfilerStartFrame
GpuTimestamp
AnalyticsCoreStatsUpdate
UnityWebRequestUpdate
ExecuteMainThreadJobs
ProcessMouseInWindow
ClearIntermediateRenderers
ClearLines
PresentBeforeUpdate
ResetFrameStatsAfterPresent
UpdateAsyncReadbackManager
UpdateStreamingManager
UpdateTextureStreamingManager
UpdatePreloading
RendererNotifyInvisible
PlayerCleanupCachedData
UpdateMainGameViewRect
UpdateCanvasRectTransform
XRUpdate
UpdateInputManager
ProcessRemoteInput
*ScriptRunDelayedStartupFrame*
UpdateKinect
DeliverIosPlatformEvents
TangoUpdate
DispatchEventQueueEvents
PhysicsResetInterpolatedTransformPosition
SpriteAtlasManagerUpdate
PerformanceAnalyticsUpdate
**UniTaskLoopRunnerLastYieldEarlyUpdate**
**UniTaskLoopRunnerLastEarlyUpdate**  

FixedUpdate
---
**UniTaskLoopRunnerYieldFixedUpdate**
**UniTaskLoopRunnerFixedUpdate**
ClearLines
NewInputFixedUpdate
DirectorFixedSampleTime
AudioFixedUpdate
*ScriptRunBehaviourFixedUpdate*
DirectorFixedUpdate
LegacyFixedAnimationUpdate
XRFixedUpdate
PhysicsFixedUpdate
Physics2DFixedUpdate
DirectorFixedUpdatePostPhysics
*ScriptRunDelayedFixedFrameRate*
**UniTaskLoopRunnerLastYieldFixedUpdate**
**UniTaskLoopRunnerLastFixedUpdate**  

PreUpdate
---
**UniTaskLoopRunnerYieldPreUpdate**
**UniTaskLoopRunnerPreUpdate**
PhysicsUpdate
Physics2DUpdate
CheckTexFieldInput
IMGUISendQueuedEvents
NewInputUpdate
SendMouseEvents
AIUpdate
WindUpdate
UpdateVideo
**UniTaskLoopRunnerLastYieldPreUpdate**
**UniTaskLoopRunnerLastPreUpdate**  

Update
---
**UniTaskLoopRunnerYieldUpdate**
**UniTaskLoopRunnerUpdate**
*ScriptRunBehaviourUpdate*
*ScriptRunDelayedDynamicFrameRate*
*ScriptRunDelayedTasks*
DirectorUpdate
**UniTaskLoopRunnerLastYieldUpdate**
**UniTaskLoopRunnerLastUpdate**  

PreLateUpdate
---
**UniTaskLoopRunnerYieldPreLateUpdate**
**UniTaskLoopRunnerPreLateUpdate**
AIUpdatePostScript
DirectorUpdateAnimationBegin
LegacyAnimationUpdate
DirectorUpdateAnimationEnd
DirectorDeferredEvaluate
EndGraphicsJobsAfterScriptUpdate
ParticleSystemBeginUpdateAll
ConstraintManagerUpdate
*ScriptRunBehaviourLateUpdate*
**UniTaskLoopRunnerLastYieldPreLateUpdate**
**UniTaskLoopRunnerLastPreLateUpdate**  

PostLateUpdate
---
**UniTaskLoopRunnerYieldPostLateUpdate**
**UniTaskLoopRunnerPostLateUpdate**
PlayerSendFrameStarted
DirectorLateUpdate
*ScriptRunDelayedDynamicFrameRate*
PhysicsSkinnedClothBeginUpdate
UpdateRectTransform
UpdateCanvasRectTransform
PlayerUpdateCanvases
UpdateAudio
VFXUpdate
ParticleSystemEndUpdateAll
EndGraphicsJobsAfterScriptLateUpdate
UpdateCustomRenderTextures
UpdateAllRenderers
EnlightenRuntimeUpdate
UpdateAllSkinnedMeshes
ProcessWebSendMessages
SortingGroupsUpdate
UpdateVideoTextures
UpdateVideo
DirectorRenderImage
PlayerEmitCanvasGeometry
PhysicsSkinnedClothFinishUpdate
FinishFrameRendering
BatchModeUpdate
PlayerSendFrameComplete
UpdateCaptureScreenshot
PresentAfterDraw
ClearImmediateRenderers
PlayerSendFramePostPresent
UpdateResolution
InputEndFrame
TriggerEndOfFrameCallbacks
GUIClearEvents
ShaderHandleErrors
ResetInputAxis
ThreadedLoadingDebug
ProfilerSynchronizeStats
MemoryFrameMaintenance
ExecuteGameCenterCallbacks
ProfilerEndFrame
**UniTaskLoopRunnerLastYieldPostLateUpdate**
**UniTaskLoopRunnerLastPostLateUpdate**

長いので、Updateだけ取り出してみてみましょう。

Update
---
UniTaskLoopRunnerYieldUpdate
UniTaskLoopRunnerUpdate
ScriptRunBehaviourUpdate
ScriptRunDelayedDynamicFrameRate
ScriptRunDelayedTasks
DirectorUpdate
UniTaskLoopRunnerLastYieldUpdate
UniTaskLoopRunnerLastUpdate

MonoBehaviourのUpdateはScriptRunBehaviourUpdate、コルーチン(yield return null)はScriptRunDelayedDynamicFrameRate、UnitySynchronizationContextはScriptRunDelayedTasksのPlayerLoopで実行されています。こうしてみると、Unityのコルーチンも特別なことはなく、PlayerLoop(ScriptRunDelayedDynamicFrameRate)がIEnumeratorのMoveNextを毎フレーム呼んでいるだけに過ぎず、UniTaskのカスタムのPlayerLoopと大差ありません。

ただしUnityのコルーチンは古くからある仕組みであることと、当時のC# 3.0相当の機能までしか使えなかったこともあり、理想的な処理機構とは言えません。今となっては不要な機能(文字列指定での呼び出しや停止など)のためのエンジン側でのオーバーヘッドや、エンジン-スクリプト間でのオーバーヘッドなど、アロケーションだけではなくパフォーマンスも良好とは言い難く、更にyield returnは戻り値が渡せない、例外処理が使えない、など言語機構としても制約が大きく、また、StartCoroutineを起動したGameObjectのライフサイクルと自動で紐づくのは利点に見えて、実際はDestroyされるとMoveNextを呼ばなくなるだけであるため、finallyなどで記述したいキャンセルに対するクリーンアップ処理が動作しません。

非同期処理に限らないUniTaskによるコルーチンの代替としての利用は、それらの制約がなく、パフォーマンスも良好です。よって、コルーチンをUniTaskに置き換えていくのは現実的な選択だと考えています。

ライフサイクルの管理は、GameObjectとの自動的な紐付きの代わりに、.NET標準のCancellationTokenを使います。MonoBehaviour内では this.GetCancellationTokenOnDestroy() メソッドで、GameObjectの寿命に紐付いたCancellationTokenを取得できるため、それを渡してあげることによりasync/awaitのライフサイクル管理とします。これはUniRxではAddTo(this)に相当するもので、CancellationTokenはCompositeDisposableに相当します。

また、現在そのスクリプトが実行されているPlayerLoopがどこなのかは、Debug.Logでのスタックトレースから確認することが出来ます。

UniTaskのPlayerLoopで実行されている場合は、下から二番目の位置にPlayerLoopが表示されます(この場合はPlayerLoopTiming.PreLateUpdateで実行されている)。

非同期LINQ

Unity 2020.2.0a12から C# 8.0 がサポートされたことにより、非同期ストリームに関する記法が可能になりました。例えば以下のような記述がUpdate()の代わりになります!

// Unity 2020.2.0a12, C# 8.0
await foreach (var _ in UniTaskAsyncEnumerable.EveryUpdate(token))
{
    Debug.Log("Update() " + Time.frameCount);
}

さすがにC# 8.0は早すぎですが、C# 7.3 環境ではForEachAsyncメソッドを使うことで、ほぼ同様の形で動かすことができるので、現実的にはこちらを使っていくことになります。

// C# 7.3(Unity 2018.3~)
await UniTaskAsyncEnumerable.EveryUpdate(token).ForEachAsync(_ =>
{
    Debug.Log("Update() " + Time.frameCount);
});

また、UniTaskAsyncEnumerableには、`IEnumerable` におけるLINQと同じような、あるいは`IObservable`におけるRxと同じような、非同期LINQが実装されています。全ての標準LINQクエリ演算子が非同期ストリームに適用可能です。例えば、以下のコードはボタンクリック非同期ストリームに対して2回に1回処理が走るWhereフィルターをかけた例です。

await okButton.OnClickAsAsyncEnumerable().Where((x, i) => i % 2 == 0).ForEachAsync(_ =>
{
});

UniRx(Reactive Extensions)に近いですが、RxがPush型の非同期ストリームだったのに対し、UniTaskAsyncEnumerableはPull型の非同期ストリームとなります。似ていますが、特性が違うことと、それに伴い細部の挙動が異なることに注意してください。

標準クエリ演算子の他にUnity向けに EveryUpdate, Timer, TimerFrame, Interval, IntervalFrame, EveryValueChanged といったジェネレーターと、 uGUIのコンポーネントには ***AsAsyncEnumerable というイベントの非同期ストリーム変換、そしてMonoBehvaiourにはメッセージイベントを変換する AsyncTriggers が実装されています。また、ReactivePropertyのUniTask版であるAsyncReactiveProperty、Unityのコンポーネント(Text/Selectable/TMP/Text)に非同期ストリームの値をバインディングするBindTo拡張メソッドなど、UniRxに存在した機能も用意されています。

そして .NET Core の System.Threading.Channels をUniTask向けにアレンジしたChannelクラスも用意しました。GoのChannelをasync/await向けにアレンジしたようなものになっていて、Rxで言うところのSubjectのように扱えます。GoのChannelがfor rangeで回せるように、Channel.Reader.ReadAllAsyncIUniTaskAsyncEnumerableを返すので、そのままforeach、或いは非同期LINQに流し込むことが可能です。

応用例としては、UniTask独自の演算子であるPublishと組み合わせると、Pub/Subの実装に変換することなどもできます。

public class AsyncMessageBroker<T> : IDisposable
{
    Channel<T> channel;

    IConnectableUniTaskAsyncEnumerable<T> multicastSource;
    IDisposable connection;

    public AsyncMessageBroker()
    {
        channel = Channel.CreateSingleConsumerUnbounded<T>();
        multicastSource = channel.Reader.ReadAllAsync().Publish();
        connection = multicastSource.Connect();
    }

    public void Publish(T value)
    {
        channel.Writer.TryWrite(value);
    }

    public IUniTaskAsyncEnumerable<T> Subscribe()
    {
        return multicastSource;
    }

    public void Dispose()
    {
        channel.Writer.TryComplete();
        connection.Dispose();
    }
}

UniTaskAsyncEnumerableと非同期LINQには多くの可能性がありますが、しかし上級者向けの機能ではあるので、初めて使う人は、まずは通常のUniTaskのほうを使いこなせるようになってください。手を出すのはその後のほうが良いでしょう。不要な場合のビルドサイズ低減と、規約としての使用制限への対応のため、非同期LINQは `UniTask.Linq` としてUniTask本体とは別アセンブリになっています。

await対応の強化

AsyncOperation, ResourceRequest, AssetBundleRequest, AssetBundleCreateRequest, UnityWebRequestAsyncOperation といったUnityで非同期処理を行う際に出てくる戻り値がawait可能にする拡張がUniTaskには搭載されていますが、今回その拡張を3パターンに整理しました。

* await asyncOperation;
* asyncOperation.WithCancellation(CancellationToken);
* asyncOperation.ToUniTask(IProgress, PlayerLoopTiming, CancellationToken);

そのままawaitする以外に、WithCancellationメソッドを呼ぶことでキャンセル処理をサポートします。また、これの戻り値はUniTaskなので、WhenAllで並列処理に回すことも可能です。ToUniTaskはWithCancellationよりも高機能なオプションで、プログレスコールバック、実行するPlayerLoop、そしてCancellationTokenを渡すことができるメソッドになっています。

また、外部アセットとしてDOTweenとAddressableのサポートを標準で追加しています。例えばDOTweenでは以下のような実装が可能です。

// sequential
await transform.DOMoveX(2, 10);
await transform.DOMoveZ(5, 20);

// parallel
var ct = this.GetCancellationTokenOnDestroy();

await UniTask.WhenAll(
    transform.DOMoveX(10, 3).WithCancellation(ct),
    transform.DOScale(10, 3).WithCancellation(ct));

そして全てのUniTaskはUniTaskTrackerにて使用状況が監視できます。

これによりメモリリークを簡単に防ぐことが可能です。

.NET Core

UniTask v2から新しく .NET Core版の実装をNuGetで提供しています。標準のTask/ValueTaskよりも高性能で動作しますが、使用にあたりExecutionContext/SynchronizationContextを無視することに注意する必要があります。ExecutionContextを無視するため、AysncLocalも動作しません。そのため使う場合は全体では用いず、制約を理解してピンポイントで使うことをお薦めします。

内部ではUniTaskを用いて、かつ外部APIとしてはValueTaskを提供する場合は、以下のような書き方が可能です。

public class ZeroAllocAsyncAwaitInDotNetCore
{
    public ValueTask<int> DoAsync(int x, int y)
    {
        return Core(this, x, y);

        static async UniTask<int> Core(ZeroAllocAsyncAwaitInDotNetCore self, int x, int y)
        {
            // do anything...
            await Task.Delay(TimeSpan.FromSeconds(x + y));
            await UniTask.Yield();

            return 10;
        }
    }
}

.NET Core版提供の主な意図は、Unityとのコード共有時(Cysharpの提供するフレームワークであるMagicOnionなどを用いる)に、インターフェイスとしてUniTaskを使えるようにすることです。 .NET Core版のUniTaskを用いることで、スムーズなコード共有を可能にします。

それ以外の箇所では、 .NET Core においてはValueTaskを用いると良いでしょう。ValueTaskに欠けているUniTask相当のWhenAllなどのユーティリティメソッドは、Cysharp/ValueTaskSupplementとして提供しているので、こちらも合わせてご利用ください。

まとめ

async/awaitが優れた非同期処理の仕組みだということは、主要な言語のほとんどが実装したことからも明らかで(C#, JavaScript, TypeScript, Python, Dart, Kotlin, Rust, etc…)、その中でもC#は最初期に登場(初出はC# 5.0, 2012年)したものですが、その実装は今でも十分通じるものになっています。しかし、.NET Coreでは年月とともに改良が続いていったのも事実で(Task -> ValueTask -> IValueTaskSource -> ManualResetValueTaskSourceCore -> IAsyncEnumerable)、Unityは最初期の実装の状態で留まっているというギャップがありました。

幸い違うのはランタイムだけであることと、C#コンパイラには(7.0以降に)十分な拡張の口が用意されているため、UniTaskではUnityに最適化した完全な独自実装を提供し、性能面でのギャップを埋めるどころか越えることに成功しました。

かなり大掛かりなシステムにはなっていますが、ここまでやる必要あるのか?というと、あります。というのも、言語構文サポートというのは非常に大きく、C#においてはasync/awaitがベストな非同期処理の手法というのは揺るがないでしょう。しかし素のままではUnityではランタイム自体の古さと、若干のゲームエンジンとのミスマッチさを抱えています。UniTaskはそれをC#の徹底的なハックによって克服しましたが、同時にそれは実装することが難しく、UniTask以上の実装/ライブラリが出てくることもないでしょう。

仕組みは大掛かりですが挙動に関しては逆に軽量化されていることと、純粋なC#上で全て処理されているので、ブラックボックスがなくパフォーマンス特性が読みきれるのも利点です。コンパイラ生成結果も、そこまで複雑なものを生成しているわけではないことは、今回の記事で確認できたはずです。UnityのPlayerLoopにうまく委ねることによってスレッドも使わないので、WebGL, WebAssembly出力でも問題なく動作します。

非同期処理をコールバックにするかコルーチンにするか、それともUniRxにするか。答えは、UniTaskを使うべきです。そして、使いこなしてください。そこで物足りない要素があったら、それから初めてUniRxに手を出したり、或いはUniTask.Linqに進めると良いでしょう。

最初のUniTaskのリリースから2年を経て、多くのゲームに採用していただきましたが、まだまだasync/awaitについて理解されていない、誤解している人も少なくないように思えます。今回のUniTask v2によって、更により多くの人にasync/awaitが身近に、そしてUnityにおいての使いやすさを実感していただければと思っています。

【Amazon Redshift事例祭り(活用編) フォローアップ】ソーシャルゲームの膨⼤なゲームログを扱うCygamesのAmazon Redshift 活用事例

$
0
0

こんにちは、データ分析基盤チームの藤田と申します。

2020年8月6日に開催された Redshift事例祭り(活用編) において「ソーシャルゲームの膨⼤なゲームログを扱うCygamesのAmazon Redshift活用事例」と題して、弊社のデータ分析基盤の概要やAmazon Redshiftの新機能の活用事例などをご紹介させていただきました。

配信をご覧いただいた皆様には改めて御礼申し上げます。

本講演では、DataLakeExportを活用した事例やRA3ノードタイプのRedshiftを業務に導入した事例などについて紹介させていただきました。

以下が講演資料となります。

本記事で講演の振り返りをさせていただきます。

Cygamesのデータ分析基盤について

データ分析基盤の役目はゲーム運用に使っているDBのレコードやサーバーのログをDataLakeとして使っているS3にアーカイブし、そのデータを分析用のDWHとして利用しているAmazon Redshift上で参照できるようにすることです。

毎日午前0時過ぎからからロードジョブが実行されます。業務が開始される頃までには前日のレコードがS3やRedshift上にロードされており、分析やBIに活用できる状態になっています。

しかし、ゲームのレコードは複雑なステータス情報を管理する関係でカラムが多くなりがちで、更にゲーム内のイベント等は周回を要する形式のためにレコードも大量に発行される傾向などもあり、Redshiftの容量を圧迫してしまうのが課題でした。

そこで特に容量を圧迫しがちな一部のテーブル(資料のP10のようなテーブル等)についてはRedshift Spectrumを利用してS3内のデータを参照し、Redshiftのストレージにはデータを配置しなくてすむような形をとることである程度容量を節約することができました。

そのためにS3にアーカイブしたデータを一度EC2にダウンロードし、PythonのパッケージでTSV形式からParquet形式に変換してS3のSpectrum用のパスに再配置するという前処理を行なっています。

この前処理は

  • 参照効率の問題からSpectrum用データはTSVよりもParquetで扱いたいこと
  • Spectrumで扱い難いパス構造でデータを配置してしまっていた

上記の二つの課題をクリアするために行なっています。

ただし、力技で変換しているため、一部のテーブルについては処理時間が長く、前処理に数時間以上要していたテーブルもありました。

DataLakeExport

DataLakeExportが新機能としてリリースされたことで、RedshiftのUNLOADクエリを使ってParquet形式によりS3への出力が可能になりました。

これまでEC2上にダウンロードしていたものを、代わりに一時的にRedshiftにロードしてからUNLOADすることで代替してみました。

結果として処理時間に関しては大きく短縮することができました。

数時間近くかかっていたテーブルも含めほとんどのテーブルが数分 ~ 1時間以内にはUNLOADまで完了することができました。

前処理などを必要とせずにSpectrumで参照できるようなDataLake設計がされていることがベストかとは思いますが、EC2やAWS Glueなどによる前処理を行なっている場合はDataLakeExportを導入してみることで費用や処理時間が改善されるケースもあるので是非検討してみることをお勧めいたします。

RA3ノードタイプのインスタンス

Redshiftの新しいノードタイプとして、RA3が発表されました。

全体的にスペックは高いのですが、特にストレージが64TB/node なのでds2タイプからの移行でかなりストレージに余裕が持てると思い、弊社でも移行を検討しました。

移行の際には普段通りの業務に支障が出ないかという事を検証するために実際にRA3のインスタンスを立てて、

  • 分析担当者に業務で扱うクエリをテスト的に実行してもらう
  • 毎日行なっているS3からのCOPY処理が遅くなっていないか

の2点を主に検証しました。

弊社の環境では、ロード処理の時間が大きく伸びてしまったクラスターもあったため移行を見送ったケースもあったのですが、ある程度大きい規模のクラスターについてはロード処理の時間も早くなり、業務にも支障が無さそうということだったためRA3に移行しました。

現在も運用していますが、ストレージで大きく余裕を持つことができ、良い結果となりました。

また、AQUAのGAも今後控えているということで今回移行を見送ったクラスターについてもAQUAの登場によって移行しやすくなることに期待しています。

おわりに

今回の講演ではRedshiftの新機能を活用した事例を紹介しましたが、Cygamesデータ分析基盤チームでは最高の分析基盤を構築するために今後も様々な技術を駆使して常に改善を続けていきます。

データ分析基盤の開発/運用を通じてCygamesの掲げる「最高のコンテンツを作る」というミッションを共に支えていけるエンジニアを募集しております。今回の講演や資料をご覧になり、興味をお持ちいただけましたらこちらの採用ページをご覧ください。

【CEDEC2020 フォローアップ】Python による大規模ゲーム開発環境 ~Cyllista Game Engine 開発事例~

$
0
0

こんにちは、Cyllista Game Engine シニアゲームエンジニアの沖です。
CEDEC 2020 にて、「Python による大規模ゲーム開発環境 ~Cyllista Game Engine 開発事例~」という題目で講演させていただきました。
講演にご参加いただいた皆様には、改めて御礼申し上げます。

当日の資料がこちらになります。

また、講演中に使用したムービーはこちらになります。

いただいたご質問とその回答

本講演は事前収録でしたため、講演中にチャットにてリアルタイムに質疑応答をさせていただきました。
当フォローアップ記事では、講演中にいただいたご質問につきまして、改めて回答していきます。

Q: ジョブ内のログはどのように扱われますか?

A: ログ通信用に Executor Server 内に Pipe 通信サーバーを立ててあり、各 Executor Process から接続することでログ情報を送信しています。

また、Executor Process 内の stdout/stderr を乗っ取ることで、全ての標準出力を送信できるようにしています。

stdout/stderr を乗っ取るシンプルなサンプルコードは下の通りです。

import sys

class OutputWrapper(object):
    def __init__(self):
        self._stdout = sys.stdout

    def __enter__(self):
        sys.stdout = self
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        sys.stdout = self._stdout

    def write(self, text):
        ...

with OutputWrapper():
    # この with 構文内の標準出力が全て OutputWrapper.write() に渡される。
    print(...)

Q: Python ベースのエンジンで扱えるアセット数はどれくらいの規模でしょうか?

A: 具体的な数字を示すことは出来ませんが、AAA タイトルを開発するために必要とされるアセット数を扱えることを要件としています。

そのため、Cyllista Game Engine のアセットシステムのコア部分は C++ (pybind11 利用) で制作し、徹底的な最適化によって  Python 上でも十分な高速動作が実現できています。

Q: アセットのサムネイルはどのように撮影されているのでしょうか?

A: CI 上で定期的にサムネイル撮影用のランタイム (C++) を起動し、サムネイル更新が必要なアセットの撮影を一括で行うようにしています。

サムネイル撮影後は Python 上でアセットシステムを経由してサムネイルファイルを Perforce に submit しています。

Q: 型ヒントは必ず付けなければならないのでしょうか?

A: 作業効率を優先するために強制はしておりません。
ただし、コアシステム寄りのソースコードについては出来る限り型ヒントを付けるようにしています。また、安定版の認定条件に型ヒントは利用していませんが、CLI ツールでいつでも静的解析できるようにしてあり、そこでエラーが見つかった場合は積極的に修正するようにしています。

Q: PEP8 の例外判定はありますか?

A: Cyllista Game Engine では作業効率のために以下の PEP8 警告/エラーについては無視するようにしています。

  • E241 : Multiple spaces after ‘,’
  • E305 : Expected 2 blank lines after end of function or class
  • E501 : Line too long
  • E741 : Do not use variables named ‘I’, ‘O’, or ‘l’
  • F403 : ‘from module import *’ used; unable to detect undefined names
  • F405 : Name may be undefined, or defined from star imports: module
  • W503 : Line break occurred before a binary operator
  • W504 : Line break occurred after a binary operator

Q: クラッシュの監視は具体的にどのように行われているのでしょうか?

A: Python で作成したクラッシュレポートサービスを常駐させています。
クラッシュレポートサービスでは watchdog パッケージを利用して、dmp ファイルの出力先ディレクトリを監視しています。

dmp ファイルの生成が検知されたタイミングで Qt for Python を利用して、クラッシュダンプダイアログを生成しています。

最後に

私たちは「最高のゲームを作るための最高のツール」として、Cyllista Game Engine を開発しております。
Cygames では一緒に Cyllista Game Engine を開発し、さらには Cyllista Game Engine 採用のゲームの第一弾である「Project Awakening」を作るメンバーを募集しています。
詳細につきましては、こちらをご覧ください。

Project Awakening


【CEDEC2020 フォローアップ】大規模プロジェクトで新卒が大活躍するには? 未来の「最高のコンテンツ作り」を牽引する若手エンジニアの育成ノウハウ

$
0
0

こんにちは。株式会社Cygames 技術研修・育成チームの藤井です。
先日のCEDEC2020においてCygamesの若手エンジニア育成のノウハウを紹介いたしました。CEDECが初めてのリモート開催ということもあり、どれほど多くの方々に講演をお聞きいただけたのかは把握しかねておりますが、コメント欄にて多くの質問をしていただけたことは非常に喜ばしく感じております。講演をご覧になってくださった皆様に改めてお礼申し上げます。

講演概要とポイント

講演では、Cygamesが若手エンジニアを育成するための取り組みを紹介いたしました。この記事でも簡単に紹介いたします。育成に関する大きなポイントは3点あります。

  1. 若手エンジニア育成の専門チームがあること
  2. 若手が新卒入社以前からゲーム開発を学ぶこと
  3. 技術力(ハードスキル)と働き方(ソフトスキル)の両軸を学ぶこと

新卒エンジニアが現場で早期に活躍するためには、ハードスキルだけでなくソフトスキルも必要不可欠です。現場配属前の研修のみではこれらのスキルは身につけられないため、入社以前のなるべく早い段階から学ぶ必要があります。Cygamesでは、若手エンジニアがこれらのスキルをより効果的に学ぶために育成専門のチームをつくり、その成長をサポートしています。

ハードスキルとソフトスキルの細かな育成ポイントに関しては、講演スライドを参考ください。

いただいたご質問とその回答

講演で多くの質問をいただきました。こちらの記事で、それらの解答をいたします。

Q: 技術研修・育成チームの指導者は専任でしょうか?また、1人で何人くらいの若手をサポートしていますか?

A: はい、専任です。過去に現場で実際にゲーム開発をしていたスタッフが若手育成専任の指導者として働いております。指導者1名につき10数名程度の若手を育成しております。また、育成のカリキュラムによっては、技術研修・育成チーム外で実際に現場で活躍しているエンジニアに育成のヘルプをしていただく事もあります。若手が最高の環境で学べるように注力しております。

Q: 若手の育成にはどれくらいの期間を割いているのでしょうか?

A: こちらの質問は一概には答えられません。新卒入社後に関しては40営業日ほどの日程で新卒エンジニア研修を実施しております。新卒入社前に関しては、学生のアルバイト開始時期や、アルバイトシフトにどの程度入ることができるのかに大きく左右されます。内定者アルバイトを例に示しますと、期間的には入社前の半年間から1年間はハードスキル・ソフトスキルを学ぶことになります。

Q: 技術研修・育成チームの教育と、現場でのOJTとの線引はどうなっていますか?

A: こちらは明確に線引されています。新卒が実際の現場に配属されるまでが技術研修・育成チームの教育となっています。配属後に関しては、現場の先輩社員からOJTで実務を学びます。
また、配属後はメンター・トレーナー制度を通じて新卒の成長をサポートすることも行っています。具体的には、新卒を直接指導しているトレーナーの方やメンターの方と技術研修・育成チームの指導者が定期的に話し合う場を作り、トレーナーやメンターからの育成相談に乗ったり、育成ノウハウをお伝えしています。

Q: どの程度のハードスキル課題の引き出しを用意しているのか知りたいです。

A: サーバーサイド向けの課題や、クライアントサイド向けの課題、チーム開発課題、その他にも調査課題など、様々なジャンルの課題を用意しております。2020年9月現在では、課題の総数は25個です。1つの課題の中にも複数の小課題を設けており段階的にゲーム開発を学べるようになっています。

Q: ソフトスキルは会社ごとに差があると考えています。学生時代から会社固有のソフトスキルを教えることで学生にプレッシャーはありませんか?

A: 技術研修・育成チームで教えているソフトスキルは、Cygames固有のものではなく、あくまで社会人として一般的に必要なものです。例えば、講演内で紹介した「メッセンジャーや社内SNSだけじゃなく、必ずひと声かける」は確認漏れや認識違いなどのトラブルを防ぎ、皆が気持ちよく働くための、ほんのちょっとしたコミュニケーションスキルです。また、ソフトスキルについて話す際には現場での体験談も織り交ぜ、学生にとって有意義な意見交換をしております。

最後に

このように技術研修・育成チームでは未来の「最高のコンテンツ作り」のために、若手の育成に全力で取り組んでいます。一緒に最高のコンテンツを作りたい方や、ゲーム業界を一緒に盛り上げたい方はぜひCygamesの門を叩いてほしいです。Cygamesでは2023年3月以降に卒業する予定の学生向けにウィンターインターンシップを開催予定です。興味のある学生の方はぜひご応募ください。

【CEDEC2020 フォローアップ】ゲームの世界を完全再現する -グラブルフェスを支える技術とプランニング-

$
0
0

『グランブルーファンタジー』メディアプランナーの堀と間宮と申します。2020年9月4日に開催されたCEDEC2020にて『ゲームの世界を完全再現する ―グラブルフェスを支える技術とプランニング―』という講演を行いました。
ご聴講いただいた皆様には厚く御礼申し上げます。
今回は本公演のフォローアップとして、講演資料とサマリーを公開いたします。

グラブルフェスの根幹思想

初めに、グラブルフェスのビジョン“「グラブル」の世界を完全再現する”のための4つ根幹思想を紹介しました。

  • 現実的に実施可能な最高値を求める
  • 恒常的な施設ではないからこそ細部までこだわる
  • 自己満足で終わらないリアルイベントの作り方をする
  • 会場にいない人にもイベントの楽しさを提供する

トークステージを魅力的にする3つのポイント

Point1: 幅広い層にリーチするための「バランス感覚」

┗会場と配信双方の視聴者が楽しめる企画を用意し、画面割にも気を配る
┗初心者からヘビーユーザーまで全ての層が楽しめるよう、バラエティ企画の内容や景品にもこだわる
┗本放送とは別の裏番組を配信しコンテンツの幅を広げる

Point2: 全スタッフを当事者とした「理解徹底」

┗本番1か月前という早いタイミングで本番同様の環境を作り事前リハーサルを行う
┗これによりスタッフの人的ミスを減らし、事前説明によってキャストの不安も解消する

Point3: 連携が生み出す効果的な「情報発表」

┗ゲーム開発チームや各情報媒体との連携を密に取り、発表内容をよりドラマチックに演出する
┗担当者がオフラインイベント専門ではなく、平常業務でゲーム運営に携わっているからこそ行える施策

3DCGライブでキャラクターを「完全再現」するためのリアル感の追求

Point1: ディラッドスクリーンの使用

┗非常に薄い半透明の「ディラッドスクリーン」を用い、ステージ上にキャラが立っているように演出
┗天吊りしたスクリーンをライブ直前に降ろすことで、生身の人間が立っていた位置にキャラを立たせる

Point2: アドリブMCパート

┗ライブ前後に、舞台裏の声優とモーションアクターが連携した生アフレコのMCを行う
┗その場でお客様との会話を成立させることで、ライブ感を出す

Point3: レンダリング方法の使い分け

┗激しい動きのあるダンスパートはキャラクターが最も美しく見えるようプリレンダリング
┗MCパートは会話の内容に合わせて自然な動きができるようリアルタイムでレンダリング

配信視聴者への対応

┗会場内とは別のARライブ映像を投影
┗客席の空気を共有しながらカメラワークや照明演出にこだわることで映像コンテンツとしての質を向上

グラブルフェスの体験コンテンツについて

講演後半は、グラブルフェスの体験コンテンツについて紹介しました。

 グラブルフェスの体験コンテンツ

「遊べる」体験

┗キャラクターをモチーフにしたミニゲームで遊べるコンテンツ

「会える」体験

┗「VR四騎士」「キャラクタートークスポット(※グラブルフェス2020にてお披露目予定)」など

キャラクターに目の前で会えるコンテンツ

「見られる」体験

┗「グランサイファーライド」「スペシャルショー」などグラブルの物語や世界を見て楽しめるコンテンツ

「味わえる」体験

┗「カフェ ミレニア」など、実際にキャラクターになりきれるコンテンツや、キャラクターをモチーフにしたフードやドリンクを楽しめるコンテンツ

 

講演では、上記のコンテンツの中から「グランサイファーライド」「スペシャルショー」「キャラクタートークスポット」について詳しく紹介しました。

グランサイファーライドについて

グランサイファーライドが目指したのは「空の冒険」の体験でした。

 「空の冒険」の体験を作る4つのポイント

Point1: 「体験」の設計

┗「空を飛んでいるような体感」に加え「冒険」が味わえるシナリオの設計

Point2: 世界観を壊さず拡張する「映像」

┗原作ゲームイラストの印象から離れず3DCG化

Point3: 映像の内容を体感させる「特効」

┗細かなタイミング調整で映像と完全にリンクした体感を作る

Point4: 世界観に没頭してもらう「工夫」

┗舞台装置や運営上必要なスタッフ・整理券等にも世界観やストーリーを反映させる

 スペシャルショーについて

スペシャルショーが目指したのはグラブル5年間のシナリオやイベントシナリオの「思い出の共有」という体験でした。

「思い出の共有」の体験を作る4つのポイント

Point1: 「感情」の設計

┗「引き込まれる」→「楽しい」→「カッコいい」→「悲しい」→「大迫力」と見ている人の感情をひとつにしつつ、後半に向けて徐々に昂るように

Point2: シナリオに合わせた効果的な「表現」

┗紗幕を使った演者へのエフェクト投影で臨場感のある「ファンタジーのバトル」の表現

Point3: 会場にいない方も一緒に「共有」できる工夫

┗配信でも見劣りしない画づくりと計画的な情報出し

キャラクタートークスポットについて

「キャラクタートークスポット」は、キャラクターとお客様がリアルな会話体験が楽しめるコンテンツです。

こちらはグラブルフェス2020にて初お披露目予定ですので、ぜひお楽しみに。

最高の「体験」を作るために

「当たり前のことを当たり前にやる」

┗体験は人間の感覚に深く結びついているため、「当たり前」を少しでも裏切るとそこから大きな違和感が生じてしまう

┗「空飛ぶ艇に乗ったら身体がこの方向から風を受ける」など、ファンタジーの世界でもその状況に応じた「当たり前」を追求することが大事

 

リアルイベントを開催する意義

お客様に「もっと」グラブルを好きになっていただく

  • ステージコンテンツ
  • 会場内外で楽しめる最高のステージライブ
  • 新情報・制作陣の生の声を届ける

プレイへのモチベーションを高めていただく

  • 体験コンテンツ
  • キャラクターや世界観を体験できるコンテンツ

キャラクター・世界観への愛を深めていただく

 

リアルイベントの成果

SNSの盛り上がり

「グラブルをやっていてよかった」「Cygamesのゲームをやっていてよかった」という声も

数字的な効果

  • ゲームの宣伝効果によるユーザー数の増加(新規、復帰)
  • ゲームの話題の盛り上がりによるプレイモチベーションの向上
  • キャラクター・世界観への愛が深まることによるキャラクター取得率の増加

おわりに

昨今は新型コロナウイルス感染症の感染拡大という大きな問題もあり、リアルイベントの開催には厳しい状況が続きます。

しかし、私たちはこれまでもこれからも、お客様に「最高の非日常」を体験いただき「グラブルを、Cygamesのゲームをやっていてよかった。これからも続けたい」と思っていただけるよう、様々な手を尽くしながら最高のグラブルフェスをお届けできればと思います。

最後に、余談にはなりますが、Twitter等でいただけるご意見ご感想はチーム全員で有難く拝見していまして、可能な限り取り入れさせていただいております。

いつも貴重なご意見ありがとうございます。

これからもグラブルフェス、そしてグランブルーファンタジーをよろしくお願いいたします。

【CEDEC2020 フォローアップ】『プリンセスコネクトRe:Dive』が目指した、アニメRPGとしてのゲーム演出制作事例 ~テレビアニメとゲーム演出、二つの制作手法を融合して生まれたカットインアニメーション~

$
0
0

こんにちは、『プリンセスコネクトRe:Dive』インタラクションデザイナーチームの工藤です。
先日開催されたCEDEC2020にて、「『プリンセスコネクトRe:Dive』が目指した、アニメRPGとしてのゲーム演出制作事例 ~テレビアニメとゲーム演出、二つの制作手法を融合して生まれたカットインアニメーション~」と題しまして講演をさせて頂きました。
初めてのオンライン開催に加え、Youtubeでも配信して頂いた事で多くの方にご聴講いただける運びとなりました。この場をお借りして、厚く御礼申し上げます。

本公演では、『プリンセスコネクトRe:Dive』におけるカットインアニメーションを制作する中で、アニメ演出をゲームに入れ込む際の課題と解決法をはじめ、カットインアニメーションが持つ役割、制作におけるノウハウ、新しい表現への試みを紹介しました。
以下が講演資料になります。

アニメ演出をゲームに入れ込むときの課題と解決法

アニメ演出をゲームの素材として入れ込むにあたり、大きく二つの課題がありました。

  1. アニメとゲームの制作体制の違いによる課題
  2. 演出のテンポ感の違いによって生じる違和感の課題

この二つを解決する手段として採用されたのが「パーツアニメーション技法」です。
パーツアニメーションでは、少ない原画枚数でアニメーションを制作することができるため、外部に依頼していた作画工程を社内制作にシフトする事ができました。また、この手法はSDキャラクターの演出でも取られており、同じ手法で制作することによりバトルシーンで演出のつながりに違和感を無くす事ができます。

カットインアニメーションの役割

『プリンセスコネクトRe:Dive』においてカットインアニメーションの役割といえる大切なポイントが3つあります。

  1. キャラクターの魅力を最大限に見せる
  2. バトルシーンを“2秒”で盛り上げる
  3. アニメとゲームをつなぐ

この3つの役割を果たすため、制作の各工程において工夫を凝らしています。中でも最も大事なキャラクターの魅力の見せ方においては、主にプランニングと作画に力を入れ、キャラクター一人一人の個性が際立つ演出、そしてユーザーの心に残る魅力あふれる作画にこだわっています。

アニメーション制作においては技術的な面で工夫を凝らすことで、よりTVアニメらしい表現を目指しており、例えば3Dライクなパーティクルは控えめにして手描きにこだわったエフェクト作りや、メッシュ機能を使った柔らかい表現でパーツアニメの持つ機械的な動きをなるべく緩和しています。

カットインアニメーションとSD演出が一体となった演出になるために、指針となるコンテの存在とチームの連携がとても大切です。

プリンセスフォームの制作

カットインアニメーションの新しい表現としてプリンセスフォームのカットインアニメーションがあります。従来のカットインアニメーションよりも豪華に、そしてよりアニメらしくというコンセプトの元、各演出制作チームが密に連携して一つの演出を制作しております。

従来とは違う新しい試みとして、SDキャラクターとカットインアニメーションが何度も切り替わる事や、従来よりも演出のスケールが大きい事、そしてストーリーアニメの演出と全く同じものにすることで、ゲームとアニメの繋がりを深めるといった試みがされています。

今まではカットインアニメーションのみが動画だったところを、SD演出も含めてすべて動画化することでエフェクト演出にも制限がなくなるなど新しい挑戦を多数行っております。

頂いた質問とその回答

講演当日は、多くの質問をいただきありがとうございました。その中で一部ではありますが、いくつかをピックアップしてこちらの記事でも回答をさせて頂きます。

Q. 「パーツアニメ」と「TVアニメ風」のカットインアニメ、コスト(人月)はどれくらい違うのでしょうか?

A. キャラクターによって実制作にかかる日数が違うので単純比較はできませんが、パーツアニメーションの方が仕様変更などで作業の後戻りが発生した際の修正コストが少なく、ゲーム開発の現場では適している手法になります。

Q. パーツアニメーションを制作する人員は前作のFlashアニメーターが多くかかわっているのでしょうか

A. 前作より規模は拡大しておりますが、元々Flashアニメーターだった方も現在多く活躍しております。

Q. カットインパーツアニメは、リアルタイム再生ではなくプリレンダムービーなんでしょうか?

A. その通りです。プリレンダムービーにすることでエフェクトやパーツの表示制限がなくなり、豪華なエフェクト演出や複雑なアニメーションでも負荷なく再生することができます。また、よりアニメらしい表現を加えるため作業工程の最後に撮影作業があります。これを加えるためにも、現状プリレンダムービーで制作することが最良と判断しております。

Q. UB時のセリフをカットアニメシーンで口パクさせていないのは何か理由などあるのでしょうか?

A. プリンセスコネクトRe:Diveにはバトルの倍速演出が存在しており、等速と倍速ではセリフが微妙に違うものがあります。どちらの速度でも使われているムービーは同じなので、あえて口パクをつけない演出を制作しています。

最後に

私たちカットインアニメーション制作チームは、キャラクターの魅力をユーザーに届ける事を第一に考え、クオリティに妥協しない事を心がけております。
その上で、新しい表現にチャレンジをしたり、どんな見せ方をすればもっとキャラクターを輝かせることができるかと日々考え、全てのキャラクターに全力を注いで制作しております。
今後とも、最高のキャラクター演出を皆様にお届けできるよう精進して参りますので、何卒ご期待ください!

Viewing all 78 articles
Browse latest View live