diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..133519f --- /dev/null +++ b/.gitignore @@ -0,0 +1,12 @@ +node_modules/ +Gemfile.lock +.sass-cache/ + +articles/*.pdf +articles/*.epub +articles/*.html +# articles/*.md +articles/*.xml +articles/*.txt + +.DS_Store diff --git a/Documentation/bible_logo.png b/Documentation/bible_logo.png new file mode 100644 index 0000000..07be2d4 Binary files /dev/null and b/Documentation/bible_logo.png differ diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000..b350d83 --- /dev/null +++ b/Gemfile @@ -0,0 +1,7 @@ +# A sample Gemfile +source "https://rubygems.org" + +gem 'review', '5.3.0' +gem 'pandoc2review' +gem 'rake' +# gem 'review-peg', '0.2.2' diff --git a/Gruntfile.js b/Gruntfile.js new file mode 100644 index 0000000..202fa80 --- /dev/null +++ b/Gruntfile.js @@ -0,0 +1,179 @@ +"use strict"; + +let fs = require("fs"); +let yaml = require("js-yaml"); + +const articles = "articles"; +const bookConfig = yaml.safeLoad(fs.readFileSync(`${articles}/config.yml`, "utf8")); + +const reviewPrefix = process.env["REVIEW_PREFIX"] || "bundle exec "; +const reviewPostfix = process.env["REVIEW_POSTFIX"] || ""; // REVIEW_POSTFIX="-peg" npm run pdf とかするとPEGでビルドできるよ +const reviewConfig = process.env["REVIEW_CONFIG_FILE"] || "config.yml"; // REVIEW_CONFIG_FILE="config-ebook.yml" npm run pdf のようにすると別のconfigでビルドできるよ +const reviewPreproc = `${reviewPrefix}review-preproc${reviewPostfix}`; +const reviewCompile = `${reviewPrefix}review-compile${reviewPostfix}`; +const reviewPdfMaker = `${reviewPrefix}rake pdf ${reviewPostfix}`; +const reviewEpubMaker = `${reviewPrefix}rake epub ${reviewPostfix}`; +const reviewWebMaker = `${reviewPrefix}rake web ${reviewPostfix}`; +const reviewTextMaker = `${reviewPrefix}rake text ${reviewPostfix}`; +const reviewIDGXMLMaker = `${reviewPrefix}rake idgxml ${reviewPostfix}`; +const reviewVivliostyle = `${reviewPrefix}rake vivliostyle ${reviewPostfix}`; + +module.exports = grunt => { + grunt.initConfig({ + clean: { + review: { + src: [ + `${articles}/${bookConfig.bookname}-*/`, // pdf, epub temp dir + `${articles}/*.pdf`, + `${articles}/*.epub`, + `${articles}/*.html`, + `${articles}/*.md`, + `${articles}/*.xml`, + `${articles}/*.txt`, + `${articles}/webroot` + ] + } + }, + shell: { + preprocess: { + options: { + execOptions: { + cwd: articles, + } + }, + command: `${reviewPreproc} -r --tabwidth=2 **/*.re` + }, + compile2text: { + options: { + execOptions: { + cwd: articles, + } + }, + command: `${reviewTextMaker}` + }, + compile2markdown: { + options: { + execOptions: { + cwd: articles, + } + }, + command: `${reviewCompile} --target=markdown` + }, + compile2html: { + options: { + execOptions: { + cwd: articles, + } + }, + command: `${reviewCompile} --target=html --stylesheet=style.css --chapterlink` + }, + compile2latex: { + options: { + execOptions: { + cwd: articles, + } + }, + command: `${reviewCompile} --target=latex --footnotetext` + }, + compile2idgxml: { + options: { + execOptions: { + cwd: articles, + } + }, + command: `${reviewCompile} --target=idgxml` + }, + compile2pdf: { + options: { + execOptions: { + cwd: articles, + } + }, + command: `${reviewPdfMaker}` + }, + compile2epub: { + options: { + execOptions: { + cwd: articles, + } + }, + command: `${reviewEpubMaker}` + }, + compile2web: { + options: { + execOptions: { + cwd: articles, + } + }, + command: `${reviewWebMaker}` + }, + compile2idgxmlmaker: { + options: { + execOptions: { + cwd: articles, + } + }, + command: `${reviewIDGXMLMaker}` + }, + compile2vivliostyle: { + options: { + execOptions: { + cwd: articles, + } + }, + command: `${reviewVivliostyle}` + } + } + }); + + function generateTask(target) { + return ["clean", "shell:preprocess", `shell:compile2${target}`]; + } + + grunt.registerTask( + "default", + "原稿をコンパイルしてPDFファイルにする", + "pdf"); + + grunt.registerTask( + "text", + "原稿をコンパイルしてTextファイルにする", + generateTask("text")); + + grunt.registerTask( + "markdown", + "原稿をコンパイルしてMarkdownファイルにする", + generateTask("markdown")); + + grunt.registerTask( + "html", + "原稿をコンパイルしてHTMLファイルにする", + generateTask("html")); + + grunt.registerTask( + "idgxmlmaker", + "原稿をコンパイルしてInDesign用XMLファイルにする", + generateTask("idgxmlmaker")); + + grunt.registerTask( + "pdf", + "原稿をコンパイルしてLaTeXでpdfファイルにする", + generateTask("pdf")); + + grunt.registerTask( + "epub", + "原稿をコンパイルしてepubファイルにする", + generateTask("epub")); + + grunt.registerTask( + "web", + "原稿をコンパイルしてWebページファイルにする", + generateTask("web")); + + grunt.registerTask( + "vivliostyle", + "原稿をコンパイルしてVivliostyle-CLIでpdfファイルにする", + generateTask("vivliostyle")); + + require('load-grunt-tasks')(grunt); +}; diff --git a/README.md b/README.md index 374d788..2cb5e9a 100644 --- a/README.md +++ b/README.md @@ -1 +1,17 @@ -# UnityPerformanceTuningBible \ No newline at end of file +

+ UnityPerformanceTuningBible +

+ +# Unity Performance Tuning Bible +本書はUnityのパフォーマンスチューニングに関するノウハウを体系的にまとめた書籍です。 +本リポジトリから電子書籍をPDFとしてダウンロードすることができます。 + +ご意見、ご指摘は [Issues](https://github.com/CyberAgentGameEntertainment/UnityPerformanceTuningBible/issues) や [Pull Requests](https://github.com/CyberAgentGameEntertainment/UnityPerformanceTuningBible/pulls) からお願いいたします。 + +## 電子書籍のダウンロード方法 +1. [Releasesの最新版](https://github.com/CyberAgentGameEntertainment/UnityPerformanceTuningBible/releases/latest)へ遷移します。 + +2. AssetsからPDFをダウンロードします。 + +## Copyright +(C) 2022 CyberAgent, Inc. diff --git a/README_CONTRIBUTOR.md b/README_CONTRIBUTOR.md new file mode 100644 index 0000000..60916ff --- /dev/null +++ b/README_CONTRIBUTOR.md @@ -0,0 +1,250 @@ +

Unity Performance Tuning Bible for Contributor

+ +執筆者向けのドキュメントです。 +本プロジェクトはRe:VIEW 5.1と5.3が混在しています。 +これは5.3でビルドを行った場合、一部レイアウトが崩れるためです。 +今後、5.3に統一していく予定となります。 + +# セットアップ + +## ローカルでビルド(PDF出力)する環境の構築 +まずローカルで出力を確認できる環境を作ります。 +Dockerを使ったセットアップが簡単なため推奨します。 + +``` +$ docker pull vvakame/review:5.1 +``` + +Macの場合、Ruby・TeXLive・Re:VIEWをそれぞれインストールすれば自力で環境構築できますが、 +いろいろと面倒なので特別な理由がなければDockerを使うのがいいと思います。 + +## ビルド方法 + +#### ローカルでビルドする +上述のDockerイメージをpullして環境構築が完了している状態を想定しています。 +Dockerを使ってローカルでビルドするには以下のシェルを実行します。 + +書籍用 +``` +$ ./build-in-docker.sh +``` + +電子書籍用 +``` +$ ./build-in-docker-epub.sh +``` + +# 記事を編集する + +#### 編集するファイル +articles/text内の「.reファイル」を編集します。 + +#### 記事を新たに追加する +articles/textに新規で「.reファイル」を作成します。 + +つぎに章扉の画像をimages/chapter_titleに追加します。 +章扉の画像名は「chapter_章番号.png」となります。 + +#### Re:VIEWの書き方 +Re:VIEWの一般的な記法は以下を参照してください。 +https://github.com/kmuto/review/blob/master/doc/format.ja.md +よく使う記法や注意事項については[記法](#記法)にまとめます。 + +# Unityバージョンについて +使用するUnityバージョンは、当時最新だったLTSバージョンの2020.3.24f1を利用します。 +他Unityバージョンの機能について言及する際は、バージョンを明記してから執筆を行いましょう。 + +# 画像ファイルの解像度 +一般的にA4サイズの書籍の印刷の場合、DPIの基準は以下のようになります。 +* フルカラー:350dpi = 2894×4093 +* モノクロ:600dpi = 4961×7016 +書籍には余白があるため、使用可能な横幅の最大サイズは約1/2ぐらいでしょう。 +それでもモノクロの場合は2500近く必要になります。 +スクリーンショットは画面解像度に依存するため現実的ではありません。 + +そのため、本書籍では解像度のレギュレーションは細かく決めませんが、以下のことを意識してください。 +* 画面の解像度を最大まで上げる(Macの場合、設定→ディスプレイから変更する。4Kディスプレイがあるなら4K推奨) +* フルカラー基準で考慮し、横幅最大の画像で1400ぐらいを目安とする。(基準を満たさないとしても、なるべく大きく撮る) + +# テキストの校正 +Visual Studio Codeのプラグインとして提供されている「テキスト校正くん」を必ず使いましょう。 +https://marketplace.visualstudio.com/items?itemName=ICS.japanese-proofreading + +# 記法 +記法について、執筆者ごとにブレないようにまとめます。 +「キャプション」という馴染みのない言葉が出てきますが、「見出し」や「タイトル名」のことを指しています。 + +## 見出し +見出しには5つのレベルが存在します。 +記法は次のようになります。 +```review +={ラベル} 章のキャプション +=={ラベル} 節のキャプション +==={ラベル} 項のキャプション +===={ラベル} 目のキャプション +====={ラベル} 段のキャプション +======{ラベル} 小段のキャプション +``` +見出しはなるべく第3レベルの「項」までに抑えましょう。 + +第4レベルは利用しても良いですが、本文書体と見分けがつきにくいため、なるべく使用は控えてください。 +記述量が少ない場合などの区切りとして使うのは問題ありません。 +使う場合は、本文書体と区別を明確にするために採番することを推奨します。 + +第5レベル以降は禁止とします。 + +#### 参照方法 +・章を参照する場合 +`@{reファイル名}`:「第3章」のように章番号に置換される。 +`@{reファイル名}`:「第3章 最適化」のように章番号と章タイトルに置換される。 +`@{reファイル名}`:「最適化」のように章タイトルに置換される。 + +・節や項を参照する場合 +`@<hd>{ラベル または 見出し}`:「2.1 最適化手法」のように見出し名に置換される。 +また、他ファイルの節や項を参照する場合は、次のようにファイル名を使用します。 +`@<hd>{ファイル名|ラベル名}` + +## 囲み枠 +### インフォ +本文から少し逸れた話など、雑談のような情報を書く際に使います。 +数行の短い文章量の場合に使うと良いでしょう。 +記法は次のようになります。 +```review +//info[キャプション]{ + +インフォの内容 +インフォの内容 + +//} +``` +キャプションは省略可能です。 + + +### コラム +インフォと同様の用途ですが、文章量が多い際はコラムの方を使用します。 +記法は次のようになります。 +```review +===[column]{ラベル} キャプション + +コラムの内容 +コラムの内容 + +===[/column] +``` +ラベルは省略可能です。 + +#### 参照方法 +`@<column>{ラベル または 見出し}>`:「GC.Allocの小話」のようにコラム名に置換される。 + + +## 図 +記法は次のようになります。 +```review +//image[識別子][キャプション][scale=1.0] +``` +scaleは省略可能です。 + +識別子は画像ファイル名と同じ名前にする必要があります。 +識別子のファイル参照先は、`images/reファイル名/識別子の画像ファイル名`になります。 + +#### 参照方法 +`@<img>{識別子}>`:「図1.1」のように図番号に置換される。 + +また、他ファイルの図を参照する場合は、次のようにファイル名を使用します。 +`@<img>{ファイル名|識別子}` + +## ソースコード +特別な理由がなければ、連番、行番号付のlistnumを使用します。 +記法は次のようになります。 +```review +//listnum[識別子][キャプション][csharp]{ +public class Sample { + public void SampleMethod() { + // サンプルメソッド + } +} +//} +``` +コードフォーマットはRiderのデフォルト設定を採用します。 +不安な場合は、コアテク標準のRider設定をインポートしてください。 + +#### 参照方法 +`@<list>{識別子}`:「リスト2.1」のようにリスト番号に置換される + +また、他ファイルのリストを参照する場合は、次のようにファイル名を使用します。 +`@<list>{ファイル名|識別子}` + +## ソースコードの本文引用 +本文中にソースコードを記述したい場合に使用します。 +改行が出来ないので1行に収まる引用にしか使えません。 +記法は次のようになります。 +```review +@<code>{var sample = gameobject.name;} +``` + +クラス名、メソッド名、プロパティ名などの本文引用は、この記法を原則、利用しましょう。 + +## テーブル +特別な理由がなければ、採番あり、キャプションありのテーブルを使用します。 +記法は次のようになります。 +```review +//table[識別子][キャプション]{ +A B +-------------------- +HogeHoge FugaFuga +FugaFuga . +//} +``` +ヘッダーと内容を分ける罫線は「-」を12個以上連続する必要があります。 +また、表の各列は「タブ」によって区切りと判定されます。 +エディターの設定で、タブがスペースに自動変換されると適切に表示されないので注意してください。 + +空白のセルには「.」を記入します。 +先頭文字に「.」を使用する場合は「..」と書きましょう。 + +#### 参照方法 +`@<table>{識別子}`:「表1.1」のようにテーブル番号に置換される + +また、他ファイルのテーブルを参照する場合は、次のようにファイル名を使用します。 +`@<table>{ファイル名|識別子}` + +## 箇条書き +記法は次のようになります。 +```review + * 第1の項目 + ** 第1の項目のネスト + * 第2の項目 + ** 第2の項目のネスト +``` +分かりにくいですが、行頭に1つ以上の空白が必要になるので注意してください。 +通常の段落との結合を防ぐために、前後に空行を入れましょう。 + +## 番号付き箇条書き +記法は次のようになります。 +```review + 1. 1つ目の内容 + 2. 2つ目の内容 + 3. 3つ目の内容 +``` +分かりにくいですが、行頭に1つ以上の空白が必要になるので注意してください。 +通常の段落との結合を防ぐために、前後に空行を入れましょう。 + +## 用語リスト +記法は次のようになります。 +```review + : 用語1 + 用語の説明 + 用語の説明 + : 用語2 + 用語の説明 +``` +分かりにくいですが、行頭に1つ以上の空白が必要になるので注意してください。 +通常の段落との結合を防ぐために、前後に空行を入れましょう。 + +## その他 +`@<em>{太字}`:強調の太字はこれに統一します。 +`@<kw>{キーワード}`:キーワード。はじめて出てくる専門用語などを強調するために使います。 +`@<href>{https://www.google.com/}`:URLがそのまま記載されます。 +`@<href>{https://www.google.com/, Google}`:Googleという文字へ置換されたリンクになります。 +`@<br>{}`:基本的に使用しません。テーブルのセル内、箇条書き内だけに限定して使いましょう。 +`//blankline`:基本的に使用しません。前後のページでキャプションだけ分離した場合などに使用するイメージです。 diff --git a/Third Party Notices.md b/Third Party Notices.md new file mode 100644 index 0000000..81345ef --- /dev/null +++ b/Third Party Notices.md @@ -0,0 +1,10 @@ +This package contains third-party software components governed by the licenses indicated below: +--------- + +Component Name: Morisawa BIZ UDMincho (https://github.com/googlefonts/morisawa-biz-ud-mincho) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. This license is available with a FAQ at https://scripts.sil.org/OFL + +Component Name: Morisawa BIZ UDGothic (https://github.com/googlefonts/morisawa-biz-ud-gothic) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. This license is available with a FAQ at https://scripts.sil.org/OFL diff --git a/articles/Rakefile b/articles/Rakefile new file mode 100644 index 0000000..07b7219 --- /dev/null +++ b/articles/Rakefile @@ -0,0 +1,3 @@ +Dir.glob('lib/tasks/*.rake').sort.each do |file| + load(file) +end diff --git a/articles/backcover.tex b/articles/backcover.tex new file mode 100644 index 0000000..71a9a7f --- /dev/null +++ b/articles/backcover.tex @@ -0,0 +1,4 @@ +% バックカバー画像 +\begin{titlepage} + \includefullpagegraphics[height=\paperheight,keepaspectratio]{/book/articles/images/cover/tuning_bible_cover_back.png} +\end{titlepage}\clearpage diff --git a/articles/catalog.yml b/articles/catalog.yml new file mode 100644 index 0000000..daabd5c --- /dev/null +++ b/articles/catalog.yml @@ -0,0 +1,22 @@ +PREDEF: + - preface.re + +CHAPS: + - tuning_start.re + - basic.re + - profile_tool.re + - tuning_practice_asset.re + - tuning_practice_assetbundle.re + - tuning_practice_physics.re + - tuning_practice_graphics.re + - tuning_practice_ui.re + - tuning_practice_script_unity.re + - tuning_practice_script_csharp.re + - tuning_practice_player_settings.re + - tuning_practice_third_party.re + +APPENDIX: + +POSTDEF: + - end_of_summary.re + - contributors.re diff --git a/articles/config-epub.yml b/articles/config-epub.yml new file mode 100644 index 0000000..c1be984 --- /dev/null +++ b/articles/config-epub.yml @@ -0,0 +1,480 @@ +# review-epubmaker向けの設定ファイルの例。 +# yamlファイルをRe:VIEWファイルのある場所に置き、 +# 「review-epubmaker yamlファイル」を実行すると、<bookname>.epubファイルが +# 生成されます。 +# このファイルはUTF-8エンコーディングで記述してください。 + +# この設定ファイルでサポートするRe:VIEWのバージョン番号。 +# major versionが違うときにはエラーを出す。 +review_version: 5.1 + +# ほかの設定ファイルの継承を指定できる。同じパラメータに異なる値がある場合は、 +# 呼び出し元の値が優先される。 +# A.yml、B.ymlのパラメータを継承する例。A.ymlとB.ymlに同じパラメータがある +# 場合、B.ymlの値が優先される。さらに今このファイルに同じパラメータがあるなら、 +# その値がB.ymlよりも優先される。 +# 同様にA.yml、B.yml内でさらにinherit:パラメータを使うこともできる。 +# inherit: ["A.yml", "B.yml"] + +# ブック名(ファイル名になるもの。ASCII範囲の文字を使用) +bookname: Unityパフォーマンスチューニングバイブル +# 記述言語。省略した場合はja +language: ja + +# 書名 +booktitle: {name: "Unity パフォーマンスチューニング バイブル", file-as: "TuningBible"} + +# 著者名。「, 」で区切って複数指定できる +aut: [{name: "株式会社 サイバーエージェント SGEコア技術本部"}] + +# 以下はオプション +# 以下はオプション(autと同じように配列書式で複数指定可能)。 +# 読みの指定はaut:の例を参照。 +# a-が付いているものはcreator側、 +# 付いていないものはcontributor側(二次協力者)に入る +# a-adp, adp: 異なるメディア向けに作り直した者 +# a-ann, ann: 注釈記述者 +# a-arr, arr: アレンジした者 +# a-art, art: グラフィックデザインおよび芸術家 +# a-asn, asn: 関連・かつての所有者・関係者 +# a-aqt, aqt: 大きく引用された人物 +# a-aft, aft: 後書き・奥付の責任者 +# a-aui, aui: 序論・序文・前書きの責任者 +# a-ant, ant: 目録責任者 +# a-bkp, bkp: メディア制作責任者 +# a-clb, clb: 限定参加または補足者 +# a-cmm, cmm: 解釈・分析・考察者 +# a-csl, csl: 監修者 +# a-dsr, dsr: デザイナ +# a-edt, edt: 編集者 +# a-ill, ill: イラストレータ +# a-lyr, lyr: 歌詞作成者 +# a-mdc, mdc: メタデータセットの一次的責任者 +# a-mus, mus: 音楽家 +# a-nrt, nrt: 語り手 +# a-oth, oth: その他 +# a-pht, pht: 撮影責任者 +# a-pbl, pbl: 出版社(発行所) +# a-prt, prt: 印刷所 +# prt: TechBooster +# a-red, red: 項目の枠組起草者 +# a-rev, rev: 評論者 +# a-spn, spn: 援助者 +# a-ths, ths: 監督者 +# a-trc, trc: 筆記・タイプ作業者 +# a-trl, trl: 翻訳者 + +pbl: 株式会社 サイバーエージェント +dsr: 株式会社 サイバーエージェント SGE統括本部 PRデザイン室 + +# 刊行日(省略した場合は実行時の日付) +date: 2022-07-08 +# 発行年月。YYYY-MM-DD形式による配列指定。省略した場合はdateを使用する +# 複数指定する場合は次のように記述する +# [["初版第1刷の日付", "初版第2刷の日付"], ["第2版第1刷の日付"]] +# 日付の後ろを空白文字で区切り、任意の文字列を置くことも可能。 +history: [["2022年7月8日 初版第1刷 発行"]] +# 権利表記(配列で複数指定可) +# rights: (C) 2016 Re:VIEW Developers +rights: ["(C) 2022 CyberAgent, Inc."] +# description: 説明 +# subject: 短い説明用タグ(配列で複数指定可) +# type: 書籍のカテゴリーなど(配列で複数指定可) +# format: メディアタイプおよび特徴(配列で複数指定可) +# source: 出版物生成の重要なリソース情報(配列で複数指定可) +# relation: 補助的リソース(配列で複数指定可) +# coverage: 内容の範囲や領域(配列で複数指定可) + +# デバッグフラグ。nullでないときには一時ファイルをカレントディレクトリに作成し、削除もしない +debug: null + +# 固有IDに使用するドメイン。省略した場合は時刻に基づくランダムUUIDが入る +urnid: urn:uuid:https://github.com/CyberAgentGameEntertainment/UnityPerformanceTuningBible/ +# +# ISBN。省略した場合はurnidが入る +# isbn: null +# +# @<chap>, @<chapref>, @<title>, @<hd>命令をハイパーリンクにする(nullでハイパーリンクにしない) +# chapterlink: true + +# HTMLファイルの拡張子(省略した場合はhtml) +htmlext: html +# +# CSSファイル(配列で複数指定可、yamlファイルおよびRe:VIEWファイルを置いたディレクトリにあること) +stylesheet: ["style.css"] + +# ePUBのバージョン (2か3) +epubversion: 3 +# +# HTMLのバージョン (4か5。epubversionを3にしたときには5にする) +htmlversion: 5 + +# 目次として抽出する見出しレベル +toclevel: 3 + +# 採番の設定。採番させたくない見出しには「==[nonum]」のようにnonum指定をする +# +# 本文でセクション番号を表示する見出しレベル +secnolevel: 3 + +# 本文中に目次ページを作成するか。省略した場合はnull (作成しない) +toc: true + +# EPUB2標準の目次(NCX)以外に物理目次ファイルを作成するか。省略した場合はnull (作成しない) +# ePUB3においてはこの設定によらず必ず作成される +# mytoc: true + +# 表紙にするファイル。ファイル名を指定すると表紙として入る (PDFMaker向けにはLaTeXソース断片、EPUBMaker向けにはHTMLファイル) +# cover: null +# +# 表紙に配置し、書籍の影絵にも利用する画像ファイル。省略した場合はnull (画像を使わない)。画像ディレクトリ内に置いてもディレクトリ名は不要(例: cover.jpg) +# PDFMaker 固有の表紙設定は pdfmaker セクション内で上書き可能 +coverimage: cover/tuning_bible_cover_color.png +# +# 表紙の後に大扉ページを作成するか。省略した場合はtrue (作成する) +titlepage: true +# +# 自動生成される大扉ページを上書きするファイル。ファイル名を指定すると大扉として入る (PDFMaker向けにはLaTeXソース断片、EPUBMaker向けにはHTMLファイル) +titlefile: title-epub.tex +# +# 原書大扉ページにするファイル。ファイル名を指定すると原書大扉として入る (PDFMaker向けにはLaTeXソース断片、EPUBMaker向けにはHTMLファイル) +# originaltitlefile: null +# +# 権利表記ページファイル。ファイル名を指定すると権利表記として入る (PDFMaker向けにはLaTeXソース断片、EPUBMaker向けにはHTMLファイル) +# creditfile: null + +# 奥付を作成するか。デフォルトでは作成されない。trueを指定するとデフォルトの奥付、ファイル名を指定するとそれがcolophon.htmlとしてコピーされる +# デフォルトの奥付における各項目の名前(「著 者」など)を変えたいときにはlocale.ymlで文字列を設定する(詳細はdoc/format.ja.mdを参照) +colophon: true +# デフォルトの奥付における、各項目の記載順序 +# colophon_order: ["aut", "csl", "trl", "dsr", "ill", "cov", "edt", "pbl", "contact", "prt"] + +# 裏表紙データファイル (PDFMaker向けにはLaTeXソース断片、EPUBMaker向けにはHTMLファイル) +backcover: backcover.tex + +# プロフィールページファイル (PDFMaker向けにはLaTeXソース断片、EPUBMaker向けにはHTMLファイル)。ファイル名を指定すると著者紹介として入る +# profile: null +# プロフィールページの目次上の見出し +# profiletitle: 著者紹介 + +# 広告ファイル。ファイル名を指定すると広告として入る (PDFMaker向けにはLaTeXソース断片、EPUBMaker向けにはHTMLファイル) +# advfile: null + +# 取り込む画像が格納されているディレクトリ。省略した場合は以下 +# imagedir: images + +# 取り込むフォントが格納されているディレクトリ。省略した場合は以下 +# fontdir: fonts + +# imagedir内から取り込まれる対象となるファイル拡張子。省略した場合は以下 +# image_ext: ["png", "gif", "jpg", "jpeg", "svg", "ttf", "woff", "otf"] + +# fontdir内から取り込まれる対象となるファイル拡張子。省略した場合は以下 +# font_ext: ["ttf", "woff", "otf"] + +# ソースコードハイライトを利用する (rouge,pygmentsには外部gemが必要) +# highlight: +# html: "rouge" +# latex: "listings" + +# カタログファイル名を指定する +# catalogfile: catalog.yml + +# reファイルを格納するディレクトリ。省略した場合は以下 (. はカレントディレクトリを示す) +contentdir: text +# pandoc2reviewを利用してMarkdownコンテンツを取り込むには、以下を有効にする +# contentdir: _refiles + +# @<w>命令で使用する単語ファイルのパス。["common.csv", "mybook.csv"]のように配列指定も可 +# words_file: words.csv + +# //table命令における列の区切り文字。tabs (1文字以上のタブ文字区切り。デフォルト), singletab (1文字のタブ文字区切り), spaces (1文字以上のスペースまたはタブ文字の区切り), verticalbar ("0個以上の空白 | 0個以上の空白"の区切り) +# table_row_separator: tabs + +# 複数行から段落を結合する際、前後のUnicode文字種に基づき必要に応じて空白文字を挿入するか +# 省略した場合はnull (挿入しない)。別途unicode-eaw gemファイルが必要 +# join_lines_by_lang: null + +# 図・表・コードリスト・数式のキャプション位置。 +# 値はtop(上)またはbottom(下)でデフォルトは以下のとおり +# caption_position: +# image: bottom +# table: top +# list: top +# equation: top + +# review-toc向けのヒント情報 +# (文字幅を考慮した行数計測には、別途unicode-eaw gemファイルが必要) +# ページあたりの行数文字数を用紙サイズで指定する(A5 or B5) +# page_metric: A5 +# +# あるいは、配列で指定することもできる +# 各数字の意味は、順にリストの行数、リストの1行字数、テキストの行数、テキストの1行字数 +# page_metric: [40,34,29,34] + +# @<m>, //texequation に記述したTeX数式の表現方法 (PDFMaker (LaTeX) 以外) +# null: TeX式をそのまま文字列として出力 (デフォルト) +# mathml: MathML変換。別途math_ml gemファイルが必要。EPUBMaker/WebMakerのみ効果 +# imgmath: 画像化。オプションはimgmath_optionsで設定する +# mathjax: MathJax変換。EPUBMaker/WebMakerのみ効果。なお、MathJaxに必要なデータはインターネットから取得される。EPUBで利用できるかはEPUBリーダ依存 +# math_format: null + +# math_formatがimgmathの場合の設定 +# 以下のパラメータを有効にするときには、 +# imgmath_options: +# パラメータ: 値 +# パラメータ: 値 +# ... +# という構成にする必要がある(インデントさせる) +# imgmath_options: + # 使用する画像拡張子。通常は「png」か「svg」(svgの場合は、pdfcrop_pixelize_cmdの-pngも-svgにする) + # format: png + # 変換手法。pdfcrop または dvipng + # converter: pdfcrop + # プリアンブルの内容を上書きするファイルを指定する(デフォルトはupLaTeX+jsarticle.clsを前提とした、lib/review/makerhelper.rbのdefault_imgmath_preambleメソッドの内容) + # preamble_file: null + # 基準のフォントサイズ + # fontsize: 10 + # 基準の行間 + # lineheight: 12 + # converterにpdfcropを指定したときのpdfcropコマンドのコマンドライン。プレースホルダは + # %i: 入力ファイル、%o: 出力ファイル + # pdfcrop_cmd: "pdfcrop --hires %i %o" + # PDFから画像化するコマンドのコマンドライン。プレースホルダは + # %i: 入力ファイル、%o: 出力ファイル、%O: 出力ファイルから拡張子を除いたもの + # %p: 対象ページ番号、%t: フォーマット + # pdfcrop_pixelize_cmd: "pdftocairo -%t -r 90 -f %p -l %p -singlefile %i %O" + # pdfcrop_pixelize_cmdが複数ページの処理に対応していない場合に単ページ化するか + # extract_singlepage: null + # extract_singlepageがtrueの場合に単ページ化するコマンドのコマンドライン + # pdfextract_cmd: "pdfjam -q --outfile %o %i %p" + # converterにdvipngを指定したときのdvipngコマンドのコマンドライン + # dvipng_cmd: "dvipng -T tight -z 9 -p %p -l %p -o %o %i" + # + # PDFで保存したいときにはたとえば以下のようにする + # format: pdf + # extract_singlepage: true + # pdfextract_cmd: "pdftk A=%i cat A%p output %o" + # pdfcrop_pixelize_cmd: "mv %i %o" + +# EPUBにおけるページ送りの送り方向、page-progression-directionの値("ltr"|"rtl"|"default") +direction: "ltr" + +# EPUBのOPFへの固有の追加ルール +# <package>要素に追加する名前空間 +# opf_prefix: {ebpaj: "http://www.ebpaj.jp/"} +# 追加する<meta>要素のプロパティとその値 +# opf_meta: {"ebpaj:guide-version": "1.1.3"} + +# 以下のパラメータを有効にするときには、 +# epubmaker: +# パラメータ: 値 +# パラメータ: 値 +# ... +# という構成にする必要がある(インデントさせる) + +epubmaker: + # HTMLファイルの拡張子 + htmlext: xhtml + stylesheet: ["style.css", "epub_style.css"] + # + # 目次を要素の階層表現にしない。省略した場合(null)は階層化する。 + # 特に部扉が入るなどの理由で、構成によっては階層化目次でepubcheckに + # パスしない目次ができるが、そのようなときにはこれをtrueにする + # flattoc: null + # + # 目次のインデントレベルをスペース文字で表現する(flattocがtrueのときのみ) + # flattocindent: true + # + # NCX目次の見出しレベルごとの飾り(配列で設定)。EPUB3ではNCXは作られない + # ncxindent: + #- + #- - + # フックは、各段階で介入したいときのプログラムを指定する。自動で適切な引数が渡される + # プログラムには実行権限が必要 + # ファイル変換処理の前に実行するプログラム。スタイルシートのコンパイルをしたいときなどに利用する。 + # 渡される引数1=作業用展開ディレクトリ + # hook_beforeprocess: null + # + # 前付の作成後に実行するプログラム。作業用展開ディレクトリにある目次ファイル(toc-html.txt)を操作したいときなどに利用する。 + # 渡される引数1=作業用展開ディレクトリ + # hook_afterfrontmatter: null + # + # 本文の変換後に実行するプログラム。作業用展開ディレクトリにある目次ファイル(toc-html.txt)を操作したいときなどに利用する。 + # 渡される引数1=作業用展開ディレクトリ + # hook_afterbody: null + # + # 後付の作成後に実行するプログラム。作業用展開ディレクトリにある目次ファイル(toc-html.txt)を操作したいときなどに利用する。 + # 渡される引数1=作業用展開ディレクトリ + # hook_afterbackmatter: null + # + # 画像およびフォントをコピーした後に実行するプログラム。別の画像やフォントを追加したいときなどに利用する。 + # 渡される引数1=作業用展開ディレクトリ + # hook_aftercopyimage: null + # + # ePUB zipアーカイブ直前に実行するプログラム。メタ情報などを加工したいときなどに利用する。 + # 渡される引数1=ePUB準備ディレクトリ + # hook_prepack: null + # + # 変換したHTMLファイルおよびCSSを解析して厳密に使用している画像ファイルだけを取り込むか。デフォルトはnull(imagesディレクトリすべてを取り込む) + # なお、フォント、カバー、広告についてはこの設定によらずディレクトリ内のものがすべて取り込まれる + # verify_target_images: null + # + # verify_target_imagesがtrueの状態において、解析で発見されなくても強制的に取り込むファイルの相対パスの配列 + # force_include_images: [] + # + # 画像ファイルの縦x横の最大ピクセル数許容値 + # image_maxpixels: 4000000 + # + # Re:VIEWファイル名を使わず、前付にpre01,pre02...、本文にchap01,chap02l...、後付にpost01,post02...という名前付けルールにするか + # rename_for_legacy: null + # + # ePUBアーカイブの非圧縮実行 + # zip_stage1: "zip -0Xq" + # + # ePUBアーカイブの圧縮実行 + # zip_stage2: "zip -Xr9Dq" + # + # ePUBアーカイブに追加するパス(デフォルトはmimetype、META-INF、OEBPS) + # zip_addpath: null + # + # EPUBで表紙をコンテンツに含めるか。デフォルトでは作成されない。yesにするとiBooks等でも最初に表紙が表示されるようになる + # cover_linear: null + # + # @<href>タグでの外部リンクを禁止し、地の文にする(falseで禁止する) + # externallink: true + # + # 脚注に「戻る」リンクを追加する(trueで追加)。脚注の記号および戻るリンクの記号はlocale.ymlで変更可能 + # back_footnote: null + # epubmaker:階層を使うものはここまで + +# LaTeX用のスタイルファイル(styディレクトリ以下に置くこと) +texstyle: ["reviewmacro"] +# +# LaTeX用のdocumentclassを指定する +# オプションについてはsty/README.mdを参照 +# デフォルトは印刷用。電子配布版を作るには media=ebook のほうを使う +# serial_pagination=trueはアラビア数字通し +# hiddenfolio=nikko-pcは日光企画向けのノドへの隠しノンブル配置 +# + +# B5の設定(10pt 40文字×35行) - 電子版 +texdocumentclass: ["review-jsbook", "media=ebook,paper=b5,serial_pagination=false,openany,fontsize=10pt,baselineskip=15.4pt,line_length=40zw,number_of_lines=35,head_space=41mm,headsep=10mm,headheight=5mm,footskip=16mm"] + +# B5の設定(10pt 40文字×35行) - 紙版 +#texdocumentclass: ["review-jsbook", "media=print,paper=b5,serial_pagination=true,hiddenfolio=nikko-pc,openany,fontsize=10pt,baselineskip=15.4pt,line_length=40zw,number_of_lines=35,head_space=30mm,headsep=10mm,headheight=5mm,footskip=10mm"] +# B5の設定(10pt 40文字×35行) - 電子版 +# texdocumentclass: ["review-jsbook", "media=ebook,paper=b5,serial_pagination=true,openany,fontsize=10pt,baselineskip=15.4pt,line_length=40zw,number_of_lines=35,head_space=30mm,headsep=10mm,headheight=5mm,footskip=10mm"] +# A5の設定(9pt 38文字×37行) - 紙版 +# texdocumentclass: ["review-jsbook", "media=print,paper=a5,serial_pagination=true,hiddenfolio=nikko-pc,openany,fontsize=9pt,baselineskip=13pt,line_length=38zw,number_of_lines=37,head_space=15mm,headsep=3mm,headheight=5mm,footskip=10mm"] +# A5の設定(9pt 38文字×37行) - 電子版 +# texdocumentclass: ["review-jsbook", "media=ebook,paper=a5,serial_pagination=true,openany,fontsize=9pt,baselineskip=13pt,line_length=38zw,number_of_lines=37,head_space=15mm,headsep=3mm,headheight=5mm,footskip=10mm"] +# +# LaTeX用のコマンドを指定する +# texcommand: "uplatex" +# +# LaTeXのコマンドに渡すオプションを指定する +# texoptions: "-interaction=nonstopmode -file-line-error -halt-on-error" +# +# LaTeX用のdvi変換コマンドを指定する(dvipdfmx) +# dvicommand: "dvipdfmx" +# +# LaTeX用のdvi変換コマンドのオプションを指定する。変換が遅い場合は`-d 5 -z 3`等にする +# dvioptions: "-d 5 -z 9" + +# 以下のパラメータを有効にするときには、 +# pdfmaker: +# パラメータ: 値 +# パラメータ: 値 +# ... +# という構成にする必要がある(インデントさせる) +# +pdfmaker: + # + # TeX版で利用する表紙画像。 + # 仕上がりサイズ+塗り足し3mmありのPDFまたはIllustratorファイル(PDF互換オプション付き)を推奨。 + # 拡縮はされず「そのまま」貼り付けられる + # 原寸画像を用意できないときには、「,cover_fit_page=true」をtexdocumentclassパラメータの2つめの値に追加すると、仕上がりサイズにリサイズして貼り付ける + # coverimage: cover-a5.ai + # + # TeXコンパイル前に実行するプログラム。変換後のTeXソースを調整したいときに使用する。 + # 渡される引数1=作業用展開ディレクトリ、引数2=呼び出しを実行したディレクトリ + # hook_beforetexcompile: null + # + # 索引処理前に実行するプログラム。idxファイルを加工したいときなどに使用する。 + # 渡される引数1=作業用展開ディレクトリ、引数2=呼び出しを実行したディレクトリ + # hook_beforemakeindex: null + # + # 索引処理後に実行するプログラム。indファイルを加工したいときなどに使用する。 + # 渡される引数1=作業用展開ディレクトリ、引数2=呼び出しを実行したディレクトリ + # hook_aftermakeindex: null + # + # ひととおりのコンパイル後に実行するプログラム。目次を加工して再度コンパイルしたいときなどに使用する。 + # 渡される引数1=作業用展開ディレクトリ、引数2=呼び出しを実行したディレクトリ + # hook_aftertexcompile: null + # + # PDF(__REVIEW_BOOK__.pdf)作成後に実行するプログラム。PDFに加工を施したいときに使用する。 + # 渡される引数1=作業用展開ディレクトリ、引数2=呼び出しを実行したディレクトリ + # hook_afterdvipdf: null + # + # 画像のscale=X.Xという指定を画像拡大縮小率からページ最大幅の相対倍率に変換する + # image_scale2width: true + # + # 画像のデフォルトのサイズを、版面横幅合わせではなく、原寸をそのまま利用する + # use_original_image_size: null + # + # PDFやIllustratorファイル(.ai)の画像のBoudingBoxの抽出に指定のボックスを採用する + # cropbox(デフォルト), mediabox, artbox, trimbox, bleedboxから選択する。 + # Illustrator CC以降のIllustratorファイルに対してはmediaboxを指定する必要がある + # bbox: mediabox + # + # 索引を作成するか。trueにすると索引作成が有効になる + # makeindex: null + # 索引作成コマンド + # makeindex_command: mendex + # 索引作成コマンドのオプション + # makeindex_options: "-f -r -I utf8" + # 索引作成コマンドのスタイルファイル + # makeindex_sty: null + # 索引作成コマンドの辞書ファイル + # makeindex_dic: null + # MeCabによる索引読み探索を使うか + # makeindex_mecab: true + # MeCabの読みの取得オプション + # makeindex_mecab_opts: "-Oyomi" + # 奥付を作成するか。trueを指定するとデフォルトの奥付、ファイル名を指定するとそれがcolophon.htmlとしてコピーされる + colophon: true + # 表紙挿入時に表紙のページ番号名を「cover」とし、偶数ページ扱いにして大扉前に白ページが入るのを防ぐ。デフォルトはtrue + # use_cover_nombre: true + # + # 囲み表現の切り替え設定 + # column, note, memo, tip, info, warning, important, caution, noticeを設定可 + # styleはreview-tcbox.styまたは独自に作成したスタイルで定義済みの囲みスタイル名 + # optionsはキャプションなし囲みに対するtcolorboxの追加・上書きオプション + # options_with_captionはキャプション付き囲みのtcolorboxの追加・上書きオプション(省略した場合はoptionsと同じ) + # + # boxsetting: + # note: + # style: squarebox + # options: "colback=black!5!white" + # options_with_caption: "colbacktitle=black!25!white" + # + # pdfmaker:階層を使うものはここまで + +# 以下のパラメータを有効にするときには、 +# webmaker: +# パラメータ: 値 +# パラメータ: 値 +# ... +# という構成にする必要がある(インデントさせる) +# +webmaker: + stylesheet: ["style.css", "style-web.css"] + +# TechBooster Re:VIEW-Template独自設定 +techbooster: + # 表紙および裏表紙を実寸ではなく仕上がりサイズにリサイズするか + # texdocumentclassでcover_fit_page=trueを付けるのと同じ + cover_fit_page: true + # 裏表紙画像指定。画像はimages/から探索される + # backcoverimage: backcover.jpg diff --git a/articles/config.yml b/articles/config.yml new file mode 100644 index 0000000..8b3fbce --- /dev/null +++ b/articles/config.yml @@ -0,0 +1,19 @@ +# 書籍版用の上書き設定 + +# 継承元設定 +inherit: ["config-epub.yml"] + +# 書籍版を別のファイル名にしたい場合 +# bookname: ReVIEW-Template-ebook + +# 書籍版のみ印刷所を追記 +prt: 株式会社 日光企画 + +# 自動生成される大扉ページを上書きするファイル。ファイル名を指定すると大扉として入る (PDFMaker向けにはLaTeXソース断片、EPUBMaker向けにはHTMLファイル) +titlefile: title.tex + +# 裏表紙データファイル (PDFMaker向けにはLaTeXソース断片、EPUBMaker向けにはHTMLファイル) +backcover: null + +# B5の設定(10pt 40文字×35行) +texdocumentclass: ["review-jsbook", "media=print,paper=b5,serial_pagination=false,fontsize=10pt,baselineskip=15.4pt,line_length=40zw,number_of_lines=35,head_space=41mm,headsep=10mm,headheight=5mm,footskip=16mm"] diff --git a/articles/doc/catalog.ja.md b/articles/doc/catalog.ja.md new file mode 100644 index 0000000..57b8aec --- /dev/null +++ b/articles/doc/catalog.ja.md @@ -0,0 +1,45 @@ +# Re:VIEW カタログファイル ガイド + +Re:VIEW のカタログファイル catalog.yml について説明します。 + +このドキュメントは、Re:VIEW 2.0 に基づいています。 + +## カタログファイルとは + +カタログファイルは、Re:VIEW フォーマットで記述された各ファイルを1冊の本(たとえば PDF や EPUB)にまとめる際に、どのようにそれらのファイルを構造化するかを指定するファイルです。現在はカタログファイルと言えば catalog.yml のことを指します。 + +## catalog.yml を用いた場合の設定方法 + +catalog.yml 内で、`PREDEF`(前付け)、`CHAPS`(本編)、`APPENDIX`(付録、連番あり)、`POSTDEF`(後付け、連番なし)を記述します。CHAPS のみ必須です。 + +```yaml +PREDEF: + - intro.re + +CHAPS: + - ch01.re + - ch02.re + +APPENDIX: + - appendix.re + +POSTDEF: + - postscript.re +``` + +本編に対して、「部」構成を加えたい場合、`CHAPS` を段階的にして記述します。部の指定については、タイトル名でもファイル名でもどちらでも使えます。 + +```yaml +CHAPS: + - ch01.re + - 第1部: + - ch02.re + - ch03.re + - pt02.re: + - ch04.re +``` + +## 古いバージョンについて +1.2 以前の Re:VIEW ではカタログファイルとして PREDEF, CHAPS, POSTDEF, PART という独立した4つのファイルを使用していました。古いカタログファイルを変換するツールとして、`review-catalog-converter` を提供しています。 + +このコマンドにドキュメントのパスを指定して実行後、生成された catalog.yml の内容が正しいか確認してください。 diff --git a/articles/doc/catalog.md b/articles/doc/catalog.md new file mode 100644 index 0000000..86e6801 --- /dev/null +++ b/articles/doc/catalog.md @@ -0,0 +1,48 @@ +# Re:VIEW catalog.yml Guide + +This article describes Re:VIEW catalog file catalog.yml. + +## What's catalog.yml + +Catalog file shows the structure of files to generate books (such as PDF or EPUB) in Re:VIEW format. +Now we use catalog.yml as catalog file. + +## How to write catalog.yml + +In catalog.yml, you can write `PREDEF`(frontmatter), `CHAPS`(bodymatter), `APPENDIX`(appendix) and `POSTDEF`(backmater). `CHAPS` is required. + +```yaml + PREDEF: + - intro.re + + CHAPS: + - ch01.re + - ch02.re + + APPENDIX: + - appendix.re + + POSTDEF: + - postscript.re +``` + +You can add parts in body to use `CHAPS` in a hierarchy. You can use both title name and file name to specify parts. + +```yaml + CHAPS: + - ch01.re + - TITLE_OF_PART1: + - ch02.re + - ch03.re + - pt02.re: + - ch04.re +``` + +(For old version user: there is no `PART`. You write them in `CHAPS`.) + +## About earlier version + +In version 1.x, Re:VIEW use 4 files PREDEF, CHAPS, POSTDEF, PART as catalog files. + +You can convert there files with `review-catalog-converter`. +When using it, you should compare with these files and the generated file `catalog.yml`. diff --git a/articles/doc/customize_epub.ja.md b/articles/doc/customize_epub.ja.md new file mode 100644 index 0000000..7eafa67 --- /dev/null +++ b/articles/doc/customize_epub.ja.md @@ -0,0 +1,65 @@ +# EPUB ローカルルールへの対応方法 +Re:VIEW の review-epubmaker が生成する EPUB ファイルは IDPF 標準に従っており、EpubCheck を通過する正規のものです。 + +しかし、ストアによってはこれに固有のローカルルールを設けていることがあり、それに合わせるためには別途 EPUB ファイルに手を入れる必要があります。幸い、ほとんどのルールは EPUB 内のメタ情報ファイルである OPF ファイルにいくつかの情報を加えることで対処できます。 + +Re:VIEW の設定ファイルは config.yml を使うものとします。 + +## 電書協ガイドライン +* http://ebpaj.jp/counsel/guide + +電書協ガイドラインの必須属性を満たすには、次の設定を config.yml に加えます。 + +```yaml +opf_prefix: {ebpaj: "http://www.ebpaj.jp/"} +opf_meta: {"ebpaj:guide-version": "1.1.3"} +``` + +これは次のように展開されます。 + +```xml +<package …… prefix="ebpaj: http://www.ebpaj.jp/"> + …… + <meta property="ebpaj:guide-version">1.1.3</meta> +``` + +ただし、Re:VIEW の生成する EPUB は、ファイルやフォルダの構成、スタイルシートの使い方などにおいて電書協ガイドラインには準拠していません。 + +## iBooks ストア +デフォルトでは、iBooks で EPUB を見開きで開くと、左右ページの間に影が入ります。 +これを消すには、次のように指定します。 + +```yaml +opf_prefix: {ibooks: "http://vocabulary.itunes.apple.com/rdf/ibooks/vocabulary-extensions-1.0/"} +opf_meta: {"ibooks:binding": "false"} +``` + +すでにほかの定義があるときには、たとえば次のように追加してください。 + +```yaml +opf_prefix: {ebpaj: "http://www.ebpaj.jp/", ibooks: "http://vocabulary.itunes.apple.com/rdf/ibooks/vocabulary-extensions-1.0/"} +opf_meta: {"ebpaj:guide-version": "1.1.3", "ibooks:binding": "false"} +``` + +## Amazon Kindle + +EPUB を作成したあと、mobi ファイルにする必要があります。これには Amazon が無料で配布している KindleGen を使用します。 + +- https://www.amazon.com/gp/feature.html?ie=UTF8&docId=1000765211 + +OS に合わせたインストーラでインストールした後、`kindlegen EPUBファイル` で mobi ファイルに変換できます。 + +Kindle Previewer にも内包されています。 + +- https://kdp.amazon.co.jp/ja_JP/help/topic/G202131170 + +注意点として、KindleGen は論理目次だけだとエラーを報告します。物理目次ページを付けるために、次のように config.yml に設定します。 + +```yaml +epubmaker: + toc: true +``` + +CSS によっては、Kindle では表現できないことについての警告が表示されることがあります。「Amazon Kindle パブリッシング・ガイドライン」では、使用可能な文字・外部ハイパーリンクの制約・色の使い方・画像サイズなどが詳細に説明されています。 + +- http://kindlegen.s3.amazonaws.com/AmazonKindlePublishingGuidelines_JP.pdf diff --git a/articles/doc/customize_epub.md b/articles/doc/customize_epub.md new file mode 100644 index 0000000..4684a62 --- /dev/null +++ b/articles/doc/customize_epub.md @@ -0,0 +1,70 @@ +# Supporting local rules of EPUB files + +EPUB files that generated by Re:VIEW (review-epubmaker) should be valid in eubcheck in IDPF. + +But some e-book stores have their own rules, so they reject EPUB files by Re:VIEW. To pass their rules, you can customize OPF file with config.yml. + +## EBPAJ EPUB 3 File Creation Guide + +* http://ebpaj.jp/counsel/guide + +EBPAJ, the Electronic Book Publishers Association of Japan, releases the guide for publishers to create EPUB files that make nothing of trouble in major EPUB readers. + +To pass their guide, you can add some settings into config.yml: + +```yaml +opf_prefix: {ebpaj: "http://www.ebpaj.jp/"} +opf_meta: {"ebpaj:guide-version": "1.1.3"} +``` + +With this settings, Re:VIEW generates OPF files with epbaj attributes: + +```xml +<package ... prefix="ebpaj: http://www.ebpaj.jp/"> + ... + <meta property="ebpaj:guide-version">1.1.3</meta> +``` + +But EPUB files that Re:VIEW generates are not the same of name and structure to EBPAJ guide. + + +## iBookStore + +Without special setting, iBooks has a margin between right page and left page in double-page spread. + +To remove it, you can add some settings in config.yml. + +```yaml +opf_prefix: {ibooks: "http://vocabulary.itunes.apple.com/rdf/ibooks/vocabulary-extensions-1.0/"} +opf_meta: {"ibooks:binding": "false"} +``` + +If you have already some settings, merge them: + +```yaml +opf_prefix: {ebpaj: "http://www.ebpaj.jp/", ibooks: "http://vocabulary.itunes.apple.com/rdf/ibooks/vocabulary-extensions-1.0/"} +opf_meta: {"ebpaj:guide-version": "1.1.3", "ibooks:binding": "false"} +``` + +## Amazon Kindle + +For Kindle, you need to convert EPUB to mobi format using KindleGen, which Amazon distributes. + +- https://www.amazon.com/gp/feature.html?ie=UTF8&docId=1000765211 + +After installation, you can convert EPUB with `kindlegen EPUB file`. + +KindleGen is also included in Kindle Previewer. + +- https://kdp.amazon.co.jp/ja_JP/help/topic/G202131170 + +Note: if there is only a "logical" table of contents, KindleGen reports a strange error. To include "physical" table of contents, set config.yml as follows. + +```yaml +epubmaker: + toc: true +``` + +You may be warned about some CSS can't be handled in Kindle. "Amazon Kindle Publishing Guidelines" describes in detail the usable characters, restrictions on hyperlinks to the outside, usage of colors, image size, and so on. + +- http://kindlegen.s3.amazonaws.com/AmazonKindlePublishingGuidelines_JP.pdf diff --git a/articles/doc/format.ja.md b/articles/doc/format.ja.md new file mode 100644 index 0000000..510d259 --- /dev/null +++ b/articles/doc/format.ja.md @@ -0,0 +1,1162 @@ +# Re:VIEW フォーマットガイド + +Re:VIEW フォーマットの文法について解説します。Re:VIEW フォーマットはアスキー社(現カドカワ)の EWB を基本としながら、一部に RD や各種 Wiki の文法を取り入れて簡素化しています。 + +このドキュメントは、Re:VIEW 5.1 に基づいています。 + +## 段落 + +段落(本文)の間は1行空けて表現します。空けずに次の行を記述した場合は、1つの段落として扱われます。 + +例: + +``` +だんらくだんらく〜〜〜 +この行も同じ段落 + +次の段落〜〜〜 +``` + +* 2行以上空けても、1行空きと同じ意味になります。 +* 空行せずに改行して段落の記述を続ける際、英文の単語間スペースについては考慮されないことに注意してください。Re:VIEW は各行を単純に連結するだけであり、TeX のように前後の単語を判断してスペースを入れるようなことはしません。 + +## 章・節・項・目・段(見出し) + +章・節・項・目といった見出しは「`=`」「`==`」「`===`」「`====`」「`=====`」で表します。7 レベル以上は使えません。`=`のあとにはスペースを入れます。 + +例: + +```review += 章のキャプション + +== 節のキャプション + +=== 項のキャプション + +==== 目のキャプション + +===== 段のキャプション + +====== 小段のキャプション +``` + +見出しは行の先頭から始める必要があります。行頭に空白を入れると、ただの本文と見なされます。 + +また、見出しの前に文があると、段落としてつながってしまうことがあります。このようなときには空行を見出しの前に入れてください。 + +## コラムなど + +節や項の見出しに `[column]` を追加すると以降がコラムとして扱われ、見出しはコラムのキャプションになります。 + +例: + +```review +===[column] コンパイラコンパイラ +``` + +このとき、「=」と「[column]」は間を空けず、必ず続けて書かなければなりません。 + +コラムは、そのコラムのキャプションの見出しと同等か上位のレベル(コラムキャプションが `===` であれば、`=`〜`===`)の見出しが登場するかファイル末尾に達した時点で終了します。これを待たずにコラムを終了する場合は、「`===[/column]`」と記述します(`=`の数はコラムキャプションと対応)。 + +例: + +```review +===[column] コンパイラコンパイラ + +コラムの内容 + +===[/column] +``` + +より下位の見出しを使うことでコラム内に副見出しを入れることができますが、それが必要になるほど長いコラムはそもそも文章構造に問題がある可能性があります。 + +このほか、次のような見出しオプションがあります。 + +* `[nonum]` : これを指定している章・節・項・段には連番を振りません。見出しは目次に含まれます。 +* `[nodisp]` : これを指定している章・節・項・段の見出しは変換結果には表示されず、連番も振りません。ただし、見出しは目次に含まれます。 +* `[notoc]` : これを指定している章・節・項・段には連番を振りません。見出しは目次に含まれません。 + +## 箇条書き + +箇条書き(HTML で言う ul、ビュレット箇条書きともいう)は「` *`」で表現します。ネストは「` **`」のように深さに応じて数を増やします。 + +例: + +``` + * 第1の項目 + ** 第1の項目のネスト + * 第2の項目 + ** 第2の項目のネスト + * 第3の項目 +``` + +箇条書きには行頭に1つ以上の空白が必要です。行頭に空白を入れず「*」と書くと、ただのテキストと見なされるので注意してください。 + +通常の段落との誤った結合を防ぐため、箇条書きの前後には空行を入れることをお勧めします(他の箇条書きも同様)。 + +## 番号付き箇条書き + +番号付きの箇条書き(HTML で言う ol)は「` 1. 〜`」「` 2. 〜`」「` 3. 〜`」のように示します。ネストはサポートしていません。 + +例: + +``` + 1. 第1の条件 + 2. 第2の条件 + 3. 第3の条件 +``` + +番号付き箇条書きも、ただの箇条書きと同様、行頭に1つ以上の空白が必要です。 + +## 用語リスト + +用語リスト(HTML で言う dl)は空白→「:」→空白、で始まる行を使って示します。 + +例: + +```review + : Alpha + DEC の作っていた RISC CPU。 + 浮動小数点数演算が速い。 + : POWER + IBM とモトローラが共同製作した RISC CPU。 + 派生として POWER PC がある。 + : SPARC + Sun が作っている RISC CPU。 + CPU 数を増やすのが得意。 +``` + +「`:`」それ自体はテキストではないので注意してください。その後に続く文字列が用語名(HTML での dt 要素)になります。 + +そして、その行以降、空白で始まる行が用語内容(HTML では dd 要素)になります。次の用語名か空行になるまで、1つの段落として結合されます。 + +## ブロック命令とインライン命令 + +見出しと箇条書きは特別な記法でしたが、それ以外の Re:VIEW の命令はほぼ一貫した記法を採用しています。 + +ブロック命令は1〜複数行の段落に対して何らかのアクション(たいていは装飾)を行います。ブロック命令の記法は次のとおりです。 + +``` +//命令[オプション1][オプション2]…{ +対象の内容。本文と同じような段落の場合は空行区切り + … +//} +``` + +オプションを取らなければ単に `//命令{` という開始行になります。いずれにせよ、開始と終了は明確です。オプション内で「]」という文字が必要であれば、`\]` でリテラルを表現できます。 + +亜種として、一切段落を取らないブロック命令もごく少数あります。 + +``` +//命令[オプション1][オプション2]… +``` + +インライン命令は段落、見出し、ブロック内容、一部のブロックのオプション内で利用でき、文字列内の一部に対してアクション(装飾)を行います。 + +``` +@<命令>{対象の内容} +``` + +内容に「}」という文字が必要であれば、`\}` でリテラルを表現できます。なお、内容の末尾を「\」としたい場合は、`\\` と記述する必要があります(たとえば `@<tt>{\\}`)。 + +記法および処理の都合で、次のような制約があります。 + +* ブロック命令内には別のブロック命令をネストできません。 +* ブロック命令内には見出しや箇条書きを格納できません。 +* インライン命令には別のインライン命令をネストできません。 + +### インライン命令のフェンス記法 + +インライン命令において `}` や 末尾 `\` を多用したい場合、それぞれ `\}` や `\\` のようにエスケープするのはわずらわしいことがあります。そのようなときには、インライン命令の囲みの `{ }` の代わりに `$ $` あるいは `| |` を使って内容を囲むことで、エスケープ表記せずに記述できます。 + +``` +@<命令>$対象の内容$ +@<命令>|対象の内容| +``` + +例: + +```review +@<m>$\Delta = \frac{\partial^2}{\partial x_1^2}+\frac{\partial^2}{\partial x_2^2} + \cdots + \frac{\partial^2}{\partial x_n^2}$ +@<tt>|if (exp) then { ... } else { ... }| +@<b>|\| +``` + +あくまでも代替であり、推奨する記法ではありません。濫用は避けてください。 + +## ソースコードなどのリスト + +ソースコードなどのリストには`//list`を使います。連番を付けたくない場合は先頭に `em`(embedded の略)、行番号を付ける場合は末尾に `num` を付加します。まとめると、以下の4種類になります。 + +* `//list[識別子][キャプション][言語指定]{ 〜 //}` + * 通常のリスト。言語指定は省略できます。 +* `//listnum[識別子][キャプション][言語指定]{ 〜 //}` + * 通常のリストに行番号をつけたもの。言語指定は省略できます。 +* `//emlist[キャプション][言語指定]{ 〜 //}` + * 連番がないリスト。キャプションと言語指定は省略できます。 +* `//emlistnum[キャプション][言語指定]{ 〜 //}` + * 連番がないリストに行番号を付けたもの。キャプションと言語指定は省略できます。 + +例: + +```review +//list[main][main()][c]{ ←「main」が識別子で「main()」がキャプション +int +main(int argc, char **argv) +{ + puts("OK"); + return 0; +} +//} +``` + +例: + +```review +//listnum[hello][ハローワールド][ruby]{ +puts "hello world!" +//} +``` + +例: + +```review +//emlist[][c]{ +printf("hello"); +//} +``` + +例: + +```review +//emlistnum[][ruby]{ +puts "hello world!" +//} +``` + +言語指定は、ハイライトを有効にしたときに利用されます。 + +コードブロック内でもインライン命令は有効です。 + +また本文中で「リスト X.X を見てください」のようにリストを指定する場合は、インライン命令 `@<list>` を使います。`//list` あるいは `//listnum` で指定した識別子を指定し、たとえば「`@<list>{main}`」と表記します。 + +他章のリストを参照するには、後述の「章ID」を指定し、たとえば `@<list>{advanced|main}`(`advanced.re` ファイルの章にあるリスト `main` を参照する)と記述します。 + +### 行番号の指定 + +行番号を指定した番号から始めるには、`//firstlinenum`を使います。 + +例: + +```review +//firstlinenum[100] +//listnum[hello][ハローワールド][ruby]{ +puts "hello world!" +//} +``` + +### ソースコード専用の引用 + +ソースコードを引用するには次のように記述します。 + +例: + +```review +//source[/hello/world.rb]{ +puts "hello world!" # キャプションあり +//} + +//source{ +puts "hello world!" # キャプションなし +//} + +//source[/hello/world.rb][ruby]{ +puts "hello world!" # キャプションあり、ハイライトあり +//} + +//source[][ruby]{ +puts "hello world!" # キャプションなし、ハイライトあり +//} +``` + +ソースコードの引用は、`//emlist` とほぼ同じです。HTML の CSS などでは区別した表現ができます。 + +## 本文中でのソースコード引用 + +本文中でソースコードを引用して記述するには、`code` を使います。 + +例: + +```review +@<code>{p = obj.ref_cnt} +``` + +## コマンドラインのキャプチャ + +コマンドラインの操作を示すときは `//cmd{ 〜 //}` を使います。インライン命令を使って入力箇所を強調するのもよいでしょう。 + +例: + +``` +//cmd{ +$ @<b>{ls /} +//} +``` + +## 図 + +図は `//image{ 〜 //}` で指定します。後述するように、識別子に基づいて画像ファイルが探索されます。 + +ブロック内の内容は単に無視されますが、アスキーアートや、図中の訳語などを入れておくといった使い道があります。 + +例: + +``` +//image[unixhistory][UNIX系OSの簡単な系譜]{ + System V 系列 + +----------- SVr4 --> 各種商用UNIX(Solaris, AIX, HP-UX, ...) +V1 --> V6 --| + +--------- 4.4BSD --> FreeBSD, NetBSD, OpenBSD, ... + BSD 系列 + + --------------> Linux +//} +``` + +3番目の引数として、画像の倍率・大きさを指定することができます。今のところ「scale=X」で倍率(X 倍)を指定でき、HTML、TeX ともに紙面(画面)幅に対しての倍率となります(0.5 なら半分の幅になります)。3番目の引数をたとえば HTML と TeX で分けたい場合は、`html::style="transform: scale(0.5);",latex::scale=0.5` のように `::` でビルダを明示し、`,` でオプションを区切って指定できます。 + +※TeX において原寸からの倍率にしたいときには、`config.yml` に `image_scale2width: false` を指定してください。 + +また、本文中で「図 X.X を見てください」のように図を指定する場合は、インライン命令 `@<img>` を使います。`//image` で指定した識別子を用いて「`@<img>{unixhistory}`」のように記述します(`//image` と `@<img>` でつづりが違うので注意してください)。 + +他章の図を参照するには、リストと同様に「章ID」を指定します。たとえば `@<img>{advanced|unixhistory}`(`advanced.re` ファイルの章にある図 `unixhistory` を参照する)と記述します。 + +### 画像ファイルの探索 + +図として貼り込む画像ファイルは、次の順序で探索され、最初に発見されたものが利用されます。 + +``` +1. <imgdir>/<builder>/<chapid>/<id>.<ext> +2. <imgdir>/<builder>/<chapid>-<id>.<ext> +3. <imgdir>/<builder>/<id>.<ext> +4. <imgdir>/<chapid>/<id>.<ext> +5. <imgdir>/<chapid>-<id>.<ext> +6. <imgdir>/<id>.<ext> +``` + +* `<imgdir>` はデフォルトでは images ディレクトリです。 +* `<builder>` は利用しているビルダ名(ターゲット名)で、たとえば `--target=html` としているのであれば、images/html ディレクトリとなります。各 Maker におけるビルダ名は epubmaker および webmaker の場合は `html`、pdfmaker の場合は `latex`、textmaker の場合は `top` です。 +* `<chapid>` は章 ID です。たとえば ch01.re という名前であれば「ch01」です。 +* `<id>` は //image[〜] の最初に入れた「〜」のことです(つまり、ID に日本語や空白交じりの文字を使ってしまうと、後で画像ファイル名の名前付けに苦労することになります!)。 +* `<ext>` は Re:VIEW が自動で判別する拡張子です。ビルダによってサポートおよび優先する拡張子は異なります。 + +各ビルダでは、以下の拡張子から最初に発見した画像ファイルが使われます。 + +* HTMLBuilder (EPUBMaker、WEBMaker)、MARKDOWNBuilder: .png、.jpg、.jpeg、.gif、.svg +* LATEXBuilder (PDFMaker): .ai、.eps、.pdf、.tif、.tiff、.png、.bmp、.jpg、.jpeg、.gif +* それ以外のビルダ・Maker: .ai、.psd、.eps、.pdf、.tif、.tiff、.png、.bmp、.jpg、.jpeg、.gif、.svg + +### インラインの画像挿入 + +段落途中などに画像を貼り込むには、インライン命令の `@<icon>{識別子}` を使います。ファイルの探索ルールは同じです。 + +## 番号が振られていない図 + +`//indepimage[ファイル名][キャプション]` で番号が振られていない画像ファイルを生成します。キャプションは省略できます。 + +例: + +``` +//indepimage[unixhistory2] +``` + +同様のことは、`//numberlessimage`でも使えます。 + +例: + +``` +//numberlessimage[door_image_path][扉絵] +``` + +## グラフ表現ツールを使った図 + +`//graph[ファイル名][コマンド名][キャプション]` で各種グラフ表現ツールを使った画像ファイルの生成ができます。キャプションは省略できます。 + +例: gnuplotの使用 + +``` +//graph[sin_x][gnuplot][Gnuplotの使用]{ +plot sin(x) +//} +``` + +コマンド名には、「`graphviz`」「`gnuplot`」「`blockdiag`」「`aafigure`」「`plantuml`」のいずれかを指定できます。ツールはそれぞれ別途インストールし、インストール先のフォルダ名を指定することなく実行できる (パスを通す) 必要があります。 + +* Graphviz ( https://www.graphviz.org/ ) : `dot` コマンドへのパスを OS に設定すること +* Gnuplot ( http://www.gnuplot.info/ ) : `gnuplot` コマンドへのパスを OS に設定すること +* Blockdiag ( http://blockdiag.com/ ) : `blockdiag` コマンドへのパスを OS に設定すること。PDF を生成する場合は ReportLab もインストールすること +* aafigure ( https://launchpad.net/aafigure ) : `aafigure` コマンドへのパスを OS に設定すること +* PlantUML ( http://plantuml.com/ ) : `java` コマンドへのパスを OS に設定し、`plantuml.jar` が作業フォルダにあること + +## 表 + +表は `//table[識別子][キャプション]{ 〜 //}` という記法です。ヘッダと内容を分ける罫線は「`------------`」(12個以上の連続する `-` または `=`)を使います。 + +表の各列のセル間は「1つ」のタブで区切ります。空白のセルには「`.`」と書きます。セルの先頭の「`.`」は削除されるので、先頭文字が「`.`」の場合は「`.`」をもう1つ余計に付けてください。たとえば「`.`」という内容のセルは「`..`」と書きます。 + +例: + +``` +//table[envvars][重要な環境変数]{ +名前 意味 +------------------------------------------------------------- +PATH コマンドの存在するディレクトリ +TERM 使っている端末の種類。linux・kterm・vt100など +LANG ユーザのデフォルトロケール。日本語ならja_JP.eucJPやja_JP.utf8 +LOGNAME ユーザのログイン名 +TEMP 一時ファイルを置くディレクトリ。/tmpなど +PAGER manなどで起動するテキスト閲覧プログラム。lessなど +EDITOR デフォルトエディタ。viやemacsなど +MANPATH manのソースを置いているディレクトリ +DISPLAY X Window Systemのデフォルトディスプレイ +//} +``` + +本文中で「表 X.X を見てください」のように表を指定する場合はインライン命令 `@<table>` を使います。たとえば `@<table>{envvars}` となります。 + +表のセル内でもインライン命令は有効です。 + +「採番なし、キャプションなし」の表は、`//table` ブロック命令に引数を付けずに記述します。 + +``` +//table{ +〜 +//} +``` + +「採番なし、キャプションあり」の表を作りたいときには、`//emtable` ブロック命令を利用します。 + +``` +//emtable[キャプション]{ +〜 +//} +``` + +### 表の列幅 + +LaTeX および IDGXML のビルダを利用する場合、表の各列の幅を `//tsize` ブロック命令で指定できます。 + +``` +//tsize[|ビルダ|1列目の幅,2列目の幅,……] +``` + +* 列の幅は mm 単位で指定します。 +* IDGXML の場合、3列のうち1列目だけ指定したとすると、省略した残りの2列目・3列目は紙面版面の幅の残りを等分した幅になります。1列目と3列目だけを指定する、といった指定方法はできません。 +* LaTeX の場合、すべての列について漏れなく幅を指定する必要があります。 +* LaTeX の場合、「`//tsize[|latex||p{20mm}cr|]`」のように LaTeX の table マクロの列情報パラメータを直接指定することもできます。 +* その他のビルダ (HTML など) においては、この命令は単に無視されます。 + +### 複雑な表 + +現時点では表のセルの結合や、中央寄せ・右寄せなどの表現はできません。 + +複雑な表については、画像を貼り込む `imgtable` ブロック命令を代わりに使用する方法もあります。`imgtable` の表は通常の表と同じく採番され、インライン命令 `@<table>` で参照できます。 + +例: + +``` +//imgtable[complexmatrix][複雑な表]{ +complexmatrixという識別子に基づく画像ファイルが貼り込まれる。 +探索ルールはimageと同じ +//} +``` + +## 引用 + +引用は「`//quote{ 〜 //}`」を使って記述します。 + +例: + +``` +//quote{ +百聞は一見に如かず。 +//} +``` + +複数の段落を入れる場合は、空行で区切ります。 + +## 囲み記事 + +技術書でよくある、コラムにするほどではないけれども本文から独立したちょっとした記事を入れるために、以下の命令があります。 + +* `//note[キャプション]{ 〜 //}` : ノート +* `//memo[キャプション]{ 〜 //}` : メモ +* `//tip[キャプション]{ 〜 //}` : Tips +* `//info[キャプション]{ 〜 //}` : 情報 +* `//warning[キャプション]{ 〜 //}` : 注意 +* `//important[キャプション]{ 〜 //}` : 重要 +* `//caution[キャプション]{ 〜 //}` : 警告 +* `//notice[キャプション]{ 〜 //}` : 注意 + +いずれも `[キャプション]` は省略できます。 + +内容には、空行で区切って複数の段落を記述可能です。 + +Re:VIEW 5.0 以降では、囲み記事に箇条書きや図表・リストを含めることもできます。 + +``` +//note{ + +箇条書きを含むノートです。 + + 1. 箇条書き1 + 2. 箇条書き2 + +//} +``` + +## 脚注 + +脚注は「`//footnote`」を使って記述します。 + +例: + +``` +パッケージは本書のサポートサイトから入手できます@<fn>{site}。 +各自ダウンロードしてインストールしておいてください。 +//footnote[site][本書のサポートサイト: http://i.loveruby.net/ja/stdcompiler ] +``` + +本文中のインライン命令「`@<fn>{site}`」は脚注番号に置換され、「本書のサポートサイト……」という文は実際の脚注に変換されます。 + +注意: TeX PDF において、コラムの中で脚注を利用する場合、`//footnote` 行はコラムの終わり(`==[/column]` など)の後ろに記述することをお勧めします。Re:VIEW の標準提供のコラム表現では問題ありませんが、サードパーティのコラムの実装によってはおかしな採番表現になることがあります。 + +### footnotetext オプション +TeX PDF において、コラム以外の `//note` などの囲み記事の中で「`@<fn>{~}`」を使うには、`footnotetext` オプションを使う必要があります。 + +`footnotetext` オプションを使うには、`config.yml` ファイルに`footnotetext: true` を追加します。 + +ただし、通常の脚注(footnote)ではなく、footnotemark と footnotetext を使うため、本文と脚注が別ページに分かれる可能性があるなど、いろいろな制約があります。また、採番が別々になるため、footnote と footnotemark/footnotetext を両立させることはできません。 + +## 参考文献の定義 + +参考文献は同一ディレクトリ内の `bib.re` ファイルに定義します。 + +``` +//bibpaper[cite][キャプション]{…コメント…} +``` + +コメントは省略できます。 + +``` +//bibpaper[cite][キャプション] +``` + +例: + +``` +//bibpaper[lins][Lins, 1991]{ +Refael D. Lins. A shared memory architecture for parallel study of +algorithums for cyclic reference_counting. Technical Report 92, +Computing Laboratory, The University of Kent at Canterbury , August +1991 +//} +``` + +本文中で参考文献を参照したい場合は、インライン命令 `@<bib>` を使い、次のようにします。 + +例: + +``` +…という研究が知られています(@<bib>{lins})。 +``` + +## リード文 + +リード文は `//lead{ 〜 //}` で指定します。歴史的経緯により、`//read{ 〜 //}` も使用可能です。 + +例: + +``` +//lead{ +本章ではまずこの本の概要について話し、 +次にLinuxでプログラムを作る方法を説明していきます。 +//} +``` + +空行区切りで複数の段落を記述することもできます。 + +## TeX 式 + +LaTeX の式を挿入するには、`//texequation{ 〜 //}` を使います。 + +例: + +``` +//texequation{ +\sum_{i=1}^nf_n(x) +//} +``` + +「式1.1」のように連番を付けたいときには、識別子とキャプションを指定します。 + +``` +//texequation[emc][質量とエネルギーの等価性]{ +\sum_{i=1}^nf_n(x) +//} +``` + +参照するにはインライン命令 `@<eq>` を使います(たとえば `@<eq>{emc}`)。 + +インライン命令では `@<m>{〜}` を使います。インライン命令の式中に「}」を含む場合、`\}` とエスケープする必要があることに注意してください(`{` はエスケープ不要)。「インライン命令のフェンス記法」も参照してください。 + +LaTeX の数式が正常に整形されるかどうかは処理系に依存します。LaTeX を利用する PDFMaker では問題なく利用できます。 + +EPUBMaker および WEBMaker では、MathML に変換する方法、MathJax に変換する方法、画像化する方法から選べます。 + +### MathML の場合 +MathML ライブラリをインストールしておきます(`gem install math_ml`)。 + +さらに config.yml に以下のように指定します。 + +``` +math_format: mathml +``` + +なお、MathML で正常に表現されるかどうかは、ビューアやブラウザに依存します。 + +### MathJax の場合 +config.yml に以下のように指定します。 + +``` +math_format: mathjax +``` + +MathJax の JavaScript モジュールはインターネットから読み込まれます。現時点で EPUB の仕様では外部からの読み込みを禁止しているため、MathJax を有効にすると EPUB ファイルの検証を通りません。また、ほぼすべての EPUB リーダーで MathJax は動作しません。CSS 組版との組み合わせでは利用できる可能性があります。 + +### 画像化の場合 + +LaTeX を内部で呼び出し、外部ツールを使って画像化する方法です。画像化された数式は、`images/_review_math` フォルダに配置されます。 + +TeXLive などの LaTeX 環境が必要です。必要に応じて config.yml の `texcommand`、`texoptions`、`dvicommand`、`dvioptions` のパラメータを調整します。 + +さらに、画像化するための外部ツールも用意します。現在、以下の2つのやり方をサポートしています。 + +- `pdfcrop`:TeXLive に収録されている `pdfcrop` コマンドを使用して数式部分を切り出し、さらに PDF から画像化します。デフォルトでは画像化には Poppler ライブラリに収録されている `pdftocairo` コマンドを使用します(コマンドラインで利用可能であれば、別のツールに変更することもできます)。 +- `dvipng`:[dvipng](https://ctan.org/pkg/dvipng) を使用します。OS のパッケージまたは `tlmgr install dvipng` でインストールできます。数式中に日本語は使えません。 + +config.yml で以下のように設定すると、 + +``` +math_format: imgmath +``` + +デフォルト値として以下が使われます。 + +``` +imgmath_options: + # 使用する画像拡張子。通常は「png」か「svg」(svgの場合は、pdfcrop_pixelize_cmdの-pngも-svgにする) + format: png + # 変換手法。pdfcrop または dvipng + converter: pdfcrop + # プリアンブルの内容を上書きするファイルを指定する(デフォルトはupLaTeX+jsarticle.clsを前提とした、lib/review/makerhelper.rbのdefault_imgmath_preambleメソッドの内容) + preamble_file: null + # 基準のフォントサイズ + fontsize: 10 + # 基準の行間 + lineheight: 12 + # pdfcropコマンドのコマンドライン。プレースホルダは + # %i: 入力ファイル、%o: 出力ファイル + pdfcrop_cmd: "pdfcrop --hires %i %o" + # PDFから画像化するコマンドのコマンドライン。プレースホルダは + # %i: 入力ファイル、%o: 出力ファイル、%O: 出力ファイルから拡張子を除いたもの + # %p: 対象ページ番号、%t: フォーマット + pdfcrop_pixelize_cmd: "pdftocairo -%t -r 90 -f %p -l %p -singlefile %i %O" + # pdfcrop_pixelize_cmdが複数ページの処理に対応していない場合に単ページ化するか + extract_singlepage: null + # 単ページ化するコマンドのコマンドライン + pdfextract_cmd: "pdfjam -q --outfile %o %i %p" + # dvipngコマンドのコマンドライン + dvipng_cmd: "dvipng -T tight -z 9 -p %p -l %p -o %o %i" +``` + +たとえば SVG を利用するには、次のようにします。 + +``` +math_format: imgmath +imgmath_options: + format: svg + pdfcrop_pixelize_cmd: "pdftocairo -svg -r 90 -f %p -l %p -singlefile %i %o" +``` + +デフォルトでは、pdfcrop_pixelize_cmd に指定するコマンドは、1ページあたり1数式からなる複数ページの PDF のファイル名を `%i` プレースホルダで受け取り、`%p` プレースホルダのページ数に基づいて `%o`(拡張子あり)または `%O`(拡張子なし)の画像ファイルに書き出す、という仕組みになっています。 + +単一のページの処理を前提とする `sips` コマンドや `magick` コマンドを使う場合、入力 PDF から指定のページを抽出するように `extract_singlepage: true` として挙動を変更します。単一ページの抽出はデフォルトで TeXLive の `pdfjam` コマンドが使われます。 + +``` +math_format: imgmath +imgmath_options: + extract_singlepage: true + # pdfjamの代わりに外部ツールのpdftkを使う場合(Windowsなど) + pdfextract_cmd: "pdftk A=%i cat A%p output %o" + # ImageMagickを利用する例 + pdfcrop_pixelize_cmd: "magick -density 200x200 %i %o" + # sipsを利用する例 + pdfcrop_pixelize_cmd: "sips -s format png --out %o %i" +``` + +textmaker 向けに PDF 形式の数式ファイルを作成したいときには、たとえば以下のように設定します(ページの抽出には pdftk を利用)。 + +``` +math_format: imgmath +imgmath_options: + format: pdf + extract_singlepage: true + pdfextract_cmd: "pdftk A=%i cat A%p output %o" + pdfcrop_pixelize_cmd: "mv %i %o" +``` + +Re:VIEW 2 以前の dvipng の設定に合わせるには、次のようにします。 + +``` +math_format: imgmath +imgmath_options: + converter: dvipng + fontsize: 12 + lineheight: 14.3 +``` + +## 字下げの制御 + +段落の行頭字下げを制御するタグとして、`//noindent` があります。HTML では `noindent` が `class` 属性に設定されます。 + +## 空行 + +1行ぶんの空行を明示して入れるには、`//blankline` を使います。 + +例: + +``` +この下に1行の空行が入る + +//blankline + +この下に2行の空行が入る + +//blankline +//blankline +``` + +## 見出し参照 +章に対する参照は、次の3つのインライン命令を利用できます。章 ID は、各章のファイル名から拡張子を除いたものです。たとえば `advanced.re` であれば `advanced` が章 ID です。 + +* `@<chap>{章ID}` : 「第17章」のような、章番号を含むテキストに置換されます。 +* `@<title>{章ID}` : その章の章題に置換されます。 +* `@<chapref>{章ID}` : 『第17章「さらに進んだ話題」』のように、章番号とタイトルを含むテキストに置換されます。 + +節や項といったより下位の見出しを参照するには、`@<hd>` インライン命令を利用します。見出しの階層を「`|`」で区切って指定します。 + +例: + +``` +@<hd>{はじめに|まずは} +``` + +他の章を参照したい場合は、先頭に章 ID を指定してください。 + +例: + +``` +@<hd>{preface|はじめに|まずは} +``` + +参照先にラベルが設定されている場合は、見出しの代わりに、ラベルで参照します。 + +``` +=={hajimeni} はじめに +… +=== まずは +… +@<hd>{hajimeni|まずは} +``` + +### コラム見出し参照 + +コラムの見出しの参照は、インライン命令 `@<column>` を使います。 + +例: + +``` +@<column>{Re:VIEWの用途いろいろ} +``` + +ラベルでの参照も可能です。 + +``` +==[column]{review-application} Re:VIEWの応用 +… +@<column>{review-application} +``` + +## リンク + +Web ハイパーリンクを記述するには、リンクに `@<href>`、アンカーに `//label` を使います。リンクの書式は `@<href>{URL, 文字表現}` で、「`, 文字表現`」を省略すると URL がそのまま使われます。URL 中に `,` を使いたいときには、`\,` とエスケープしてください。 + +例: + +``` +@<href>{http://github.com/, GitHub} +@<href>{http://www.google.com/} +@<href>{#point1, ドキュメント内ポイント} +@<href>{chap1.html#point1, ドキュメント内ポイント} +//label[point1] +``` + +## 単語ファイルの展開 + +キーと値のペアを持つ単語ファイルを用意しておき、キーが指定されたら対応するその値を展開するようにできます。`@<w>{キー}` あるいは `@<wb>{キー}` インライン命令を使います。後者は太字にします。 + +現在、単語ファイルは CSV(コンマ区切り)形式で拡張子 .csv のものに限定されます。1列目にキー、2列目に値を指定して列挙します。 + +``` +"LGPL","Lesser General Public License" +"i18n","""i""nternationalizatio""n""" +``` + +単語ファイルのファイルパスは、`config.yml` に `words_file: ファイルパス` で指定します。`word_file: ["common.csv", "mybook.csv"]` のように複数のファイルも指定可能です(同一のキーがあるときには後に指定したファイルの値が優先されます)。 + +例: + +```review +@<w>{LGPL}, @<wb>{i18n} +``` + +テキストビルダを使用している場合: + +``` +Lesser General Public License, ★"i"nternationalizatio"n"☆ +``` + +展開されるときには、ビルダごとに決められたエスケープが行われます。インライン命令を含めるといったことはできません。 + +## コメント + +最終結果に出力されないコメントを記述するには、「`#@#`」を使います。行末までがコメントとして無視されます。 + +例: + +``` +#@# FIXME: あとで調べておくこと +``` + +最終結果に出力するコメントを記述したい場合は、`//comment` または `@<comment>` を使ったうえで、review-compile コマンドに `--draft` オプションを付けて実行します。 + +例: + +``` +@<comment>{あとで書く} +``` + +## 生データ行 + +Re:VIEW のタグ範囲を超えて何か特別な行を挿入したい場合、`//raw` ブロック命令や`//embed`ブロック命令、または `@<raw>` インライン命令や `@<embed>` インライン命令を使います。 + +### `//raw`行 + +例: + +``` +//raw[|html|<div class="special">\nここは特別な行です。\n</div>] +``` + +ブロック命令は1つだけオプションをとり、「|ビルダ名|そのまま出力させる内容」という書式です。`\n`は改行文字に変換されます。 + +ビルダ名には「`html`」「`latex`」「`idgxml`」「`markdown`」「`top`」のいずれかが入ります。ビルダ名は「,」で区切って複数指定することも可能です。該当のビルダを使用しているときのみ、内容が出力されます。 + +例: + +``` +(HTMLビルダの場合:) +<div class="special"> +ここは特別な行です。 +</div> + +(ほかのビルダの場合は単に無視されて何も出力されない) +``` + +インライン命令は、`@<raw>{|ビルダ名|〜}` という書式で、記述はブロック命令に同じです。 + +### `//embed`ブロック + +例: + +``` +//embed{ +<div class="special"> +ここは特別なブロックとして扱われます。 +</div> +//} + +//embed[html,markdown]{ +<div class="special"> +ここはHTMLとMarkdownでは特別なブロックとして扱われます。 +</div> +//} +``` + +`//embed`ブロック命令はブロック内の文字列をそのまま文書中に埋め込みます。エスケープされる文字はありません。 + +オプションとして、ビルダ名を指定できます。ビルダ名には「`html`」「`latex`」「`idgxml`」「`markdown`」「`top`」のいずれかが入ります。ビルダ名は「,」で区切って複数指定することも可能です。該当のビルダを使用しているときのみ、内容が出力されます。異なるビルダを使用している場合は無視されます。 + +オプションを省略した場合、すべてのビルダで文字列が埋め込まれます。 + +例: + +HTMLビルダを使用している場合: + +```html +<div class="special"> +ここは特別なブロックとして扱われます。 +</div> + +<div class="special"> +ここはHTMLとMarkdownでは特別なブロックとして扱われます。 +</div> +``` + +LaTeXビルダを使用している場合: + +```tex +<div class="special"> +ここは特別なブロックとして扱われます +</div> + +``` + +`//raw`、`//embed`、`@<raw>` および `@<embed>` は、HTML、XML や TeX の文書構造を容易に壊す可能性があります。使用には十分に注意してください。 + +### 入れ子の箇条書き + +Re:VIEW の箇条書きは `*` 型の箇条書きを除き、基本的に入れ子を表現できません。いずれの箇条書きも、別の箇条書き、あるいは図表・リストを箇条書きの途中に配置することを許容していません。 + +この対策として、Re:VIEW 4.2 では試験的に `//beginchild`、`//endchild` というブロック命令を追加しています。箇条書きの途中に何かを含めたいときには、それを `//beginchild` 〜 `//endchild` で囲んで配置します。多重に入れ子にすることも可能です。 + +``` + * UL1 + +//beginchild +#@# ここからUL1の子 + + 1. UL1-OL1 + +//beginchild +#@# ここからUL1-OL1の子 + +UL1-OL1-PARAGRAPH + + * UL1-OL1-UL1 + * UL1-OL1-UL2 + +//endchild +#@# ここまでUL1-OL1の子 + + 2. UL1-OL2 + + : UL1-DL1 + UL1-DD1 + : UL1-DL2 + UL1-DD2 + +//endchild +#@# ここまでUL1の子 + + * UL2 +``` + +これをたとえば HTML に変換すると、次のようになります。 + +``` +<ul> +<li>UL1 +<ol> +<li>UL1-OL1 +<p>UL1-OL1-PARAGRAPH</p> +<ul> +<li>UL1-OL1-UL1</li> +<li>UL1-OL1-UL2</li> +</ul> +</li> + +<li>UL1-OL2</li> +</ol> +<dl> +<dt>UL1-DL1</dt> +<dd>UL1-DD1</dd> +<dt>UL1-DL2</dt> +<dd>UL1-DD2</dd> +</dl> +</li> + +<li>UL2</li> +</ul> +``` + +(試験実装のため、命令名や挙動は今後のバージョンで変更になる可能性があります。) + +## インライン命令 +主なインライン命令を次に示します。 + +### 書体 +書体については、適用するスタイルシートなどによって異なることがあります。 + +* `@<kw>{〜}`, `@<kw>{キーワード, 補足}` : キーワード。通常は太字になることを想定しています。2つめの記法では、たとえば `@<kw>{信任状, credential}` と表記したら「信任状(credential)」のようになります。 +* `@<bou>{〜}` : 傍点が付きます。 +* `@<ami>{〜}` : 文字に対して網がかかります。 +* `@<u>{〜}` : 下線を引きます。 +* `@<b>{〜}` : 太字にします。 +* `@<i>{〜}` : イタリックにします。和文の場合、処理系によってはイタリックがかからないこともあります。 +* `@<strong>{〜}` : 強調(太字)にします。 +* `@<em>{〜}` : 強調にします。 +* `@<tt>{〜}` : 等幅にします。 +* `@<tti>{〜}` : 等幅+イタリックにします。 +* `@<ttb>{〜}` : 等幅+太字にします。 +* `@<code>{〜}` : 等幅にします(コードの引用という性質)。 +* `@<tcy>{〜}` : 縦書きの文書において文字を縦中横にします。 +* `@<ins>{〜}` : 挿入箇所を明示します(デフォルトでは下線が引かれます)。 +* `@<del>{〜}` : 削除箇所を明示します(デフォルトでは打ち消し線が引かれます)。 + +### 参照 +* `@<chap>{章ファイル名}` : 「第17章」のような、章番号を含むテキストに置換されます。 +* `@<title>{章ファイル名}` : その章の章題に置換されます。 +* `@<chapref>{章ファイル名}` : 『第17章「さらに進んだ話題」』のように、章番号とタイトルを含むテキストに置換されます。 +* `@<list>{識別子}` : リストを参照します。 +* `@<img>{識別子}` : 図を参照します。 +* `@<table>{識別子}` : 表を参照します。 +* `@<eq>{識別子}` : 式を参照します。 +* `@<hd>{ラベルまたは見出し}` : 節や項を参照します。 +* `@<column>{ラベルまたは見出し}` : コラムを参照します。 + +### その他 +* `@<ruby>{親文字, ルビ}` : ルビを振ります。たとえば `@<ruby>{愕然, がくぜん}` のように表記します。 +* `@<br>{}` : 段落途中で改行します。濫用は避けたいところですが、表のセル内や箇条書き内などで必要になることもあります。 +* `@<uchar>{番号}` : Unicode文字を出力します。引数は16進数で指定します。 +* `@<href>{URL}`, `@<href>{URL, 文字表現}` : ハイパーリンクを作成します(後述)。 +* `@<icon>{識別子}` : インラインの画像を出力します。 +* `@<m>{数式}` : インラインの数式を出力します。 +* `@<w>{キー}` : キー文字列に対応する、単語ファイル内の値文字列を展開します。 +* `@<wb>{キー}` : キー文字列に対応する、単語ファイル内の値文字列を展開し、太字にします。 +* `@<raw>{|ビルダ|〜}` : 生の文字列を出力します。`\}`は`}`に、`\\`は`\`に、`\n`は改行に置き換えられます。 +* `@<embed>{|ビルダ|〜}` : 生の文字列を埋め込みます。`\}`は`}`に、`\\`は`\`に置き換えられます(`\n`はそのままです)。 +* `@<idx>{文字列}` : 文字列を出力するとともに、索引として登録します。索引の使い方については、makeindex.ja.md を参照してください。 +* `@<hidx>{文字列}` : 索引として登録します (idx と異なり、紙面内に出力はしません)。`親索引文字列<<>>子索引文字列` のように親子関係にある索引も定義できます。 +* `@<balloon>{〜}` : コードブロック (emlist など) 内などでのいわゆる吹き出しを作成します。たとえば「`@<balloon>{ABC}`」とすると、「`←ABC`」となります。デフォルトの挙動および表現は簡素なので、より装飾されたものにするにはスタイルシートを改変するか、`review-ext.rb` を使って挙動を書き換える必要があります。 + +## 著者用タグ(プリプロセッサ命令) + +これまでに説明したタグはすべて最終段階まで残り、見た目に影響を与えます。それに対して以下のタグは著者が使うための専用タグであり、変換結果からは除去されます。 + +* `#@#` : コメント。この行には何を書いても無視されます。 +* `#@warn(〜)` : 警告メッセージ。プリプロセス時にメッセージが出力されます。 +* `#@require`, `#@provide` : キーワードの依存関係を宣言します。 +* `#@mapfile(ファイル名) 〜 #@end` : ファイルの内容をその場に展開します。 +* `#@maprange(ファイル名, 範囲名) 〜 #@end` : ファイル内の範囲をその場に展開します。 +* `#@mapoutput(コマンド) 〜 #@end` : コマンドを実行して、その出力結果を展開します。 + +コメントを除き、プリプロセッサ `review-preproc` コマンドとの併用を前提とします。 + +## 国際化(i18n) + +Re:VIEW が出力する文字列(「第◯章」「図」「表」など)を、指定した言語に合わせて出力することができます。デフォルトは日本語です。 + +ファイルが置かれているディレクトリに `locale.yml` というファイルを用意して、以下のように記述します(日本語の場合)。 + +例: + +``` +locale: ja +``` + +既存の表記を書き換えたい場合は、該当する項目を上書きします。既存の設定ファイルは Re:VIEW の `lib/review/i18n.yml` にあります。 + +例: + +``` +locale: ja +list: 実行例 +``` + +### Re:VIEW カスタムフォーマット + +`locale.yml` ファイルでは、章番号などに以下の Re:VIEW カスタムフォーマットを使用可能です。 + +* `%pA` : アルファベット(大文字)A, B, C, ... +* `%pa` : アルファベット(小文字)a, b, c, ... +* `%pAW` : アルファベット(大文字・いわゆる全角)A, B, C, ... +* `%paW` : アルファベット(小文字・いわゆる全角)a, b, c, ... +* `%pR` : ローマ数字(大文字)I, II, III, ... +* `%pr` : ローマ数字(小文字)i, ii, iii, ... +* `%pRW` : ローマ数字(大文字・単一文字表記)Ⅰ, Ⅱ, Ⅲ, ... +* `%pJ` : 漢数字 一, 二, 三, ... +* `%pdW` : アラビア数字(0〜9まではいわゆる全角、10以降半角)1, 2, ... 10, ... +* `%pDW` : アラビア数字(すべて全角)1, 2, ... 10, ... + +例: + +``` +locale: ja + part: 第%pRW部 + appendix: 付録%pA +``` + +## その他の文法 + +拡張文法は `review-ext.rb` というファイルで指定できます。 + +たとえば、 + +```ruby +# review-ext.rb +ReVIEW::Compiler.defblock :foo, 0..1 +class ReVIEW::HTMLBuilder + def foo(lines, caption = nil) + puts lines.join(",") + end +end +``` + +のような内容のファイルを用意すると、以下のような文法を追加できます。 + +``` +//foo{ +A +B +C +//} +``` + +``` +# 出力結果 +A,B,C +``` + +詳しいことについては、ここでは触れません。 + +## HTML および LaTeX のレイアウト機能 + +ファイルが置かれているディレクトリに layouts/layout.html.erb を置くと、デフォルトの HTML テンプレートの代わりにその HTML が使われます(erb 記法で記述します)。 + +例: + +``` +<html> + <head> + <title><%= @config["booktitle"] %> + + + <%= @body %> +
+ + +``` + +同様に、layouts/layout.tex.erb で、デフォルトの LaTeX テンプレートを置き換えることができます。 diff --git a/articles/doc/format.md b/articles/doc/format.md new file mode 100644 index 0000000..e994806 --- /dev/null +++ b/articles/doc/format.md @@ -0,0 +1,1216 @@ +# Re:VIEW Format Guide + +The document is a brief guide for Re:VIEW markup syntax. + +Re:VIEW is based on EWB of ASCII (now KADOKAWA), influenced RD and other Wiki system's syntax. + +This document explains about the format of Re:VIEW 5.1. + +## Paragraph + +Paragraphs are separated by an empty line. + +Usage: + +``` +This is a paragraph, paragraph, +and paragraph. + +Next paragraph here is ... +``` + +Two empty lines or more are same as one empty line. + +## Chapter, Section, Subsection (headings) + +Chapters, sections, subsections, subsubsections use `=`, `==`, `===`, `====`, `=====`, and `======`. +You should add one or more spaces after `=`. + +Usage: + +```review += 1st level (chapter) + +== 2nd level (section) + +=== 3rd level (subsection) + +==== 4th level + +===== 5th level + +====== 6th level +``` + +Headings should not have any spaces before title; if line head has space, it is as paragraph. + +You should add emply lines between Paragraphs and Headings. + +## Column + +`[column]` in a heading are column's caption. + +Usage: + +```review +===[column] Compiler-compiler +``` + +`=` and `[column]` should be closed to. Any spaces are not permitted. + +Columns are closed with next headings. + +``` +== head 01 + +===[column] a column + +== head 02 and the end of 'a column' +``` + +If you want to close column without headings, you can use `===[/column]` + +Usage: + +```review +===[column] Compiler-compiler + +Compiler-compiler is ... + +===[/column] + +blah, blah, blah (this is paragraphs outside of the column) +``` + +There are some more options of headings. + +* `[nonum]` : no numbering, but add it into TOC (Table of Contents). +* `[nodisp]` : not display in document, only in TOC. +* `[notoc]` : no numbering, not in TOC. + +## Itemize + +Itemize (ul in HTML) uses ` *` (one space char and asterisk). + +Nested itemize is like ` **`, ` ***`. + +Usage: + +``` + * 1st item + ** nested 1st item + * 2nd item + ** nested 2nd item + * 3rd item +``` + +In itemize, you must write one more space character at line head. +When you use `*` without spaces in line head, it's just paragraph. + +You should add emply lines between Paragraphs and Itemize (same as Ordered and Non-Orderd). + +## Ordered Itemize + +Ordered itemize (ol in HTML) uses ` 1. ...`, ` 2. ...`, ` 3. ...`. +They aren't nested. + +Usage: + +``` + 1. 1st condition + 2. 2nd condition + 3. 3rd condition +``` + +The value of Number is ignored. + +``` + 1. 1st condition + 1. 2nd condition + 1. 3rd condition +``` + +You must write one more space character at line head like itemize. + +## Definition List + +Definition list (dl in HTML) uses ` : ` and indented lines. + +Usage: + +```review + : Alpha + RISC CPU made by DEC. + : POWER + RSIC CPU made by IBM and Motolora. + POWER PC is delivered from this. + : SPARC + RISC CPU made by SUN. +``` + +`:` in line head is not used as a text. +The text after `:` is as the term (dt in HTML). + +In definition list, `:` at line head allow space characters. +After dt line, space-indented lines are descriptions(dd in HTML). + +You can use inline markup in texts of lists. + +## Block Commands and Inline Commands + +With the exception of headings and lists, Re:VIEW supports consistent syntax. + +Block commands are used for multiple lines to add some actions (ex. decoration). + +The syntax of block commands is below: + +``` +//command[option1][option2]...{ +(content lines, sometimes separated by empty lines) + ... +//} +``` + +If there is no options, the begining line is just `//command{`. When you want to use a character `]`, you must use escaping `\]`. + +Some block commands has no content. + +``` +//command[option1][option2]... +``` + +Inline commands are used in block, paragraphes, headings, block contents and block options. + +``` +@{content} +``` + +When you want to use a character `}` in inline content, you must use escaping `\}`. If the content ends with `\`, it must be written `\\`. (ex. `@{\\}`) + +There are some limitations in blocks and inlines. + +* Block commands do not support nestins. You cannot write blocks in another block. +* You cannot write headings and itemize in block contents. +* Inline commands also do not support nestins. You cannot write inlines in another inline. + +### Fence notation for inline commands +You may be tired of escaping when you use a large number of inline commands including `{` and `\`. By surrounding the contents with `$ $` or `| |` instead of `{ }`, you can write without escaping. + +``` +@$content$ +@|content| +``` + +Example: + +```review +@$\Delta = \frac{\partial^2}{\partial x_1^2}+\frac{\partial^2}{\partial x_2^2} + \cdots + \frac{\partial^2}{\partial x_n^2}$ +@|if (exp) then { ... } else { ... }| +@|\| +``` + +Since this notation is substitute, please avoid abuse. + +## Code List + +Code list like source codes is `//list`. If you don't need numbers, you can use ``em`` prefix (as embedded). If you need line numbers, you can use ``num`` postfix. So you can use four types of lists. + +* ``//list[ID][caption][language]{ ... //}`` + * normal list. language is optional. +* ``//listnum[ID][caption][language]{ ... //}`` + * normal list with line numbers. language is optional. +* ``//emlist[caption][language]{ ... //}`` + * list without caption counters. caption and language are optional. +* ``//emlistnum[caption][language]{ ... //}`` + * list with line numbers without caption counters. caption and language are optional. + +Usage: + +```review +//list[main][main()][c]{ ←ID is `main`, caption is `main()` +int +main(int argc, char **argv) +{ + puts("OK"); + return 0; +} +//} +``` + +Usage: + +```review +//listnum[hello][hello world][ruby]{ +puts "hello world!" +//} +``` + +Usage: + +```review +//emlist[][c]{ +printf("hello"); +//} +``` + +Usage: + +```review +//emlistnum[][ruby]{ +puts "hello world!" +//} +``` + +The Language option is used for highlightings. + +You can use inline markup in blocks. + +When you refer a list like `see list X`, you can use an ID in `//list` +such as `@{main}`. + +When you refer a list in the other chapter, you can use an ID with chapter ID, such like `@{advanced|main}`, to refer a list `main` in `advanced.re`. + +### define line number of first line in code block + +If you want to start with specified number as line number, you can use `firstlinenum` command. + +Usage: + +```review +//firstlinenum[100] +//listnum[hello][helloworld][ruby]{ +puts "hello world!" +//} +``` + +### Quoting Source Code + +`//source` is for quoting source code. filename is mandatory. + +Usage: + +```review +//source[/hello/world.rb]{ +puts "hello world!" +//} +``` + +`//source` and `//emlist` with caption is not so different. +You can use them with different style with CSS (in HTML) and style file (in LaTeX). + +`//source` can be referred same as the list. + +Usage: + +``` +When you ..., see @{hello}. +``` + +## Inline Source Code + +You can use `@{...}` in inline context. + +Usage: + +```review +@{p = obj.ref_cnt} +``` + +## Shell Session + +When you want to show command line operation, you can use `//cmd{ ... //}`. +You can use inline commands in this command. + +Usage: + +``` +//cmd{ +$ @{ls /} +//} +``` + +You can use inline markup in `//cmd` blocks. + +## Figure + +You can use `//image{ ... //}` for figures. +You can write comments or Ascii art in the block as an alternative description. +When publishing, it's simply ignored. + +Usage: + +``` +//image[unixhistory][a brief history of UNIX-like OS]{ + System V + +----------- SVr4 --> Commercial UNIX(Solaris, AIX, HP-UX, ...) +V1 --> V6 --| + +--------- 4.4BSD --> FreeBSD, NetBSD, OpenBSD, ... + BSD + + --------------> Linux +//} +``` + +The third option is used to define the scale of images. `scale=X` is scaling for page width (`scale=0.5` makes image width to be half of page width). +If you'd like to use different values for each builders, such as HTML and TeX, you can specify the target builders using `::`. Example: `html::style="transform: scale(0.5);",latex::scale=0.5` + +When you want to refer images such as "see figure 1.", you can use +inline reference markup like `@{unixhistory}`. + +When you refer a image in the other chapter, you can use the same way as a list reference. To refer a image `unixhistory` in `advanced.re`, use `@{advanced|unixhistory}`. + +When you want to use images in paragraph or other inline context, you can use `@`. + +### Finding image pathes + +The order of finding image is as follows. The first matched one is used. + +``` +1. ///. +2. //-. +3. //. +4. //. +5. /-. +6. /. +``` + +* ```` is `images` as default. +* ```` is a builder (target) name to use. When you use review-comile commmand with ``--target=html``, `/` is `images/html`. The builder name for epubmaker and webmaker is `html`, for pdfmaker it is `latex`, and for textmaker it is `top`. +* ```` is basename of *.re file. If the filename is `ch01.re`, chapid is `ch01`. +* ```` is the ID of the first argument of `//image`. You should use only printable ASCII characters as ID. +* ```` is file extensions of Re:VIEW. They are different by the builder you use. + +For each builder, image files are searched in order of the following extensions, and the first hit file is adopted. + +* HTMLBuilder (EPUBMaker, WEBMaker), MARKDOWNBuilder: .png, .jpg, .jpeg, .gif, .svg +* LATEXBuilder (PDFMaker): .ai, .eps, .pdf, .tif, .tiff, .png, .bmp, .jpg, .jpeg, .gif +* Other builders/makers: .ai, .psd, .eps, .pdf, .tif, .tiff, .png, .bmp, .jpg, .jpeg, .gif, .svg + +### Inline Images + +When you want to use images in paragraph, you can use the inline command `@{ID}`. The order of finding images are same as `//image`. + +## Images without caption counter + +`//indepimage[filename][caption]` makes images without caption counter. +caption is optional. + +Usage: + +``` +//indepimage[unixhistory2] +``` + +Note that there are similar markup `//numberlessimage`, but it is deprecated. + + +## Figures with graph tools + +Re:VIEW generates image files using graph tool with command `//graph[filename][commandname][caption]`. The caption is optional. + +Usage: using with Gnuplot + +``` +//graph[sin_x][gnuplot]{ +plot sin(x) +//} +``` + +You can use `graphviz`, `gnuplot`, `blockdiag`, `aafigure`, and `plantuml` as the command name. +Before using these tools, you should installed them and configured path appropriately. + +* Graphviz ( https://www.graphviz.org/ ) : set path to `dot` command +* Gnuplot ( http://www.gnuplot.info/ ) : set path to `gnuplot` command +* Blockdiag ( http://blockdiag.com/ ) : set path to `blockdiag` command. Install ReportLab also to make a PDF +* aafigure ( https://launchpad.net/aafigure ) : set path to `aafigure` command +* PlantUML ( http://plantuml.com/ ) : set path to `java` command. place `plantuml.jar` on working folder + +## Tables + +The markup of table is `//table[ID][caption]{ ... //}` +You can separate header and content with `------------`. + +The columns are splitted by TAB character. Write `.` to blank cells. When the first character in the cell is `.`, the character is removed. If you want to write `.` at the first, you should write `..`. + +When you want to use an empty column, you write `.`. + +Usage: + +``` +//table[envvars][Important environment varialbes]{ +Name Comment +------------------------------------------------------------- +PATH Directories where commands exist +TERM Terminal. ex: linux, kterm, vt100 +LANG default local of users. ja_JP.eucJP and ja_JP.utf8 are popular in Japan +LOGNAME login name of the user +TEMP temporary directory. ex: /tmp +PAGER text viewer on man command. ex: less, more +EDITOR default editor. ex: vi, emacs +MANPATH Directories where sources of man exist +DISPLAY default display of X Window System +//} +``` + +When you want to write "see table X", you can write `@{envvars}`. + +You can use inline markup in the tables. + +`//table` without arguments creates a table without numbering and captioning. + +``` +//table{ +... +//} +``` + +To create a table without numbering but with captioning, use `//emtable`. + +``` +//emtable[caption]{ +... +//} +``` + +### Column width of table +When using LaTeX or IDGXML builder, you can specify each column width of the table with `//tsize` block command. + +``` +//tsize[|builder|width-of-column1,width-of-column2,...] +``` + +* The collumn width is specified in mm. +* For IDGXML, if only 1st of the three columns is specified, the remaining 2nd and 3rd columns will be the width of the remainder of the live area width equally divided. It is not possible to specify that only the 1st and 3rd columns are specified. +* For LaTeX, you have to specify all column widths. +* For LaTeX, you can also directly specify the column parameter of LaTeX table macro like `//tsize[|latex||p{20mm}cr|]`. +* In other builders such as HTML, this command is simply ignored. + +### Complex Table + +If you want to use complex tables, you can use `imgtable` block command with an image of the table. `imgtable` supports numbering and `@
`. + +Usage: + +``` +//imgtable[complexmatrix][very complex table]{ +to use image "complexmatrix". +The rule of finding images is same as image command. +//} +``` + +## Quoting Text + +You can use `//quote{ ... //}` as quotations. + +Usage: + +``` +//quote{ +Seeing is believing. +//} +``` + +You can use inline markup in quotations. + +## Short column + +Some block commands are used for short column. + +* `//note[caption]{ ... //}` +* `//memo[caption]{ ... //}` +* `//tip[caption]{ ... //}` +* `//info[caption]{ ... //}` +* `//warning[caption]{ ... //}` +* `//important[caption]{ ... //}` +* `//caution[caption]{ ... //}` +* `//notice[caption]{ ... //}` + +`[caption]` is optional. + +The content is like paragraph; separated by empty lines. + +From Re:VIEW 5.0, it is also possible to include itemize, figures and tables in short columns. + +``` +//note{ + +With ordered itemize. + + 1. item1 + 2. item2 + +//} +``` + +## Footnotes + +You can use `//footnote` to write footnotes. + +Usage: + +``` +You can get the packages from support site for the book.@{site} +You should get and install it before reading the book. +//footnote[site][support site of the book: http://i.loveruby.net/ja/stdcompiler ] +``` + +`@{site}` in source are replaced by footnote marks, and the phrase "support site of .." +is in footnotes. + +Note that in LATEXBuilder, it is highly recommended to place `//footnote` after the end line of column (`==[/column]`) to avoid problems when using third party's style file. + +### `footnotetext` option +Note that in LATEXBuilder, you should use `footnotetext` option to use `@{...}` in `//note` or other short column blocks. + +By adding `footnotetext:true` in config.yml, you can use footnote in tables and short notes. + +Note that there are some constraints that (because of normal footnote ) + +And you cannot use footnote and footnotemark/footnotetext at the same time. + +Note that with this option, Re:VIEW use footnotemark and footnotetext instead of normal footnote. +There are some constraints to use this option. +You cannot use footnote and footnotemark/footnotetext at the same time. + +## Bibliography + +When you want to use a bibliography, you should write them in the file `bib.re`. + +``` +//bibpaper[cite][caption]{..comment..} +``` + +The comment is optional. + +``` +//bibpaper[cite][caption] +``` + +Usage: + +``` +//bibpaper[lins][Lins, 1991]{ +Refael D. Lins. A shared memory architecture for parallel study of +algorithums for cyclic reference_counting. Technical Report 92, +Computing Laboratory, The University of Kent at Canterbury , August +1991 +//} +``` + +When you want to refer some references, You should write as: + +Usage: + +``` +… is the well-known project.(@{lins}) +``` + +## Lead Sentences + +lead sentences are `//lead{ ... //}`. +You can write as `//read{ ... //}`. + +Usage: + +``` +//lead{ +In the chapter, I introduce brief summary of the book, +and I show the way how to write a program in Linux. +//} +``` + +## TeX Equations + +You can use `//texequation{ ... //}` to insert mathematical equations of LaTeX. + +Usage: + +``` +//texequation{ +\sum_{i=1}^nf_n(x) +//} +``` + +If you'd like to assign a number like 'Equation 1.1`, specify the identifier and caption. + +``` +//texequation[emc][The Equivalence of Mass and Energy]{ +\sum_{i=1}^nf_n(x) +//} +``` + +To reference this, use the inline command `@`. + +There is `@{ ... }` for inline (see "Fence notation for inline commands" section also). + +Whether LaTeX formula is correctly displayed or not depends on the processing system. PDFMaker uses LaTeX internally, so there is no problem. + +In EPUBMaker and WEBMaker, you can choose between MathML conversion, MathJax conversion, and imaging. + +### MathML case +Install MathML library (`gem install math_ml`). + +Specify in config.yml as follows: + +``` +math_format: mathml +``` + +Whether it is displayed properly in MathML depends on your viewer or browser. + +### MathJax case +Specify in config.yml as follows: + +``` +math_format: mathjax +``` + +MathJax JavaScript module is loaded from the Internet. Because the EPUB specification prohibits loading files from external, enabling this feature will cause the EPUB file to fail validation. Also MathJax will not work in almost all EPUB readers, but may be available with CSS formatting processor. + +### imaging case + +This way calls LaTeX internally and images it with an external tool. Image files will be placed in `images/_review_math` folder. + +You need TeXLive or other LaTeX environment. Modify the parameters of `texcommand`,` texoptions`, `dvicommand`,` dvioptions` in config.yml as necessary. + +In addition, external tools for image conversion are also needed. Currently, it supports the following two methods. + +- `pdfcrop`: cut out the formula using `pdfcrop` command (included in TeXLive) and image it. By default, `pdftocairo` command is used (included in Poppler library). You can change it to another tool if available on the command line. +- `dvipng`: it uses [dvipng](https://ctan.org/pkg/dvipng) to cut out and to image. You can install with OS package or `tlmgr install dvipng`. + +By setting in config.yml, + +``` +math_format: imgmath +``` + +it is set as follows: + +``` +imgmath_options: + # format. png|svg + format: png + # conversion method. pdfcrop|dvipng + converter: pdfcrop + # custom preamble file (default: for upLaTeX+jsarticle.cls, see lib/review/makerhelper.rb#default_imgmath_preamble) + preamble_file: null + # default font size + fontsize: 10 + # default line height + lineheight: 12 + # pdfcrop command. + # %i: filename for input %o: filename for output + pdfcrop_cmd: "pdfcrop --hires %i %o" + # imaging command. + # %i: filename for input %o: filename for output %O: filename for output without the extension + # %p: page number, %t: format + pdfcrop_pixelize_cmd: "pdftocairo -%t -r 90 -f %p -l %p -singlefile %i %O" + # whether to generate a single PDF page for pdfcrop_pixelize_cmd. + extract_singlepage: null + # command line to generate a single PDF page file. + pdfextract_cmd: "pdfjam -q --outfile %o %i %p" + # dvipng command. + dvipng_cmd: "dvipng -T tight -z 9 -p %p -l %p -o %o %i" +``` + +For example, to make SVG: + +``` +math_format: imgmath +imgmath_options: + format: svg + pdfcrop_pixelize_cmd: "pdftocairo -svg -r 90 -f %p -l %p -singlefile %i %o" +``` + +By default, the command specified in `pdfcrop_pixelize_cmd` takes the filename of multi-page PDF consisting of one formula per page. + +If you want to use the `sips` command or the` magick` command, they can only process a single page, so you need to set `extract_singlepage: true` to extract the specified page from the input PDF. `pdfjam` command (in TeXLive) is used to extract pages. + +``` +math_format: imgmath +imgmath_options: + extract_singlepage: true + # use pdftk instead of default pdfjam (for Windows) + pdfextract_cmd: "pdftk A=%i cat A%p output %o" + # use ImageMagick + pdfcrop_pixelize_cmd: "magick -density 200x200 %i %o" + # use sips + pdfcrop_pixelize_cmd: "sips -s format png --out %o %i" +``` + +To create PDF math images: + +``` +math_format: imgmath +imgmath_options: + format: pdf + extract_singlepage: true + pdfextract_cmd: "pdftk A=%i cat A%p output %o" + pdfcrop_pixelize_cmd: "mv %i %o" +``` + +To set the same setting as Re:VIEW 2: + +``` +math_format: imgmath +imgmath_options: + converter: dvipng + fontsize: 12 + lineheight: 14.3 +``` + +## Spacing + +`//noindent` is a tag for spacing. + +* `//noindent` : ingore indentation immediately following line. (in HTML, add `noindent` class) + +## Blank line + +`//blankline` put an empty line. + +Usage: + +``` +Insert one blank line below. + +//blankline + +Insert two blank line below. + +//blankline +//blankline +``` + +## Referring headings + +There are 3 inline commands to refer a chapter. These references use Chapter ID. The Chapter ID is filename of chapter without extentions. For example, Chapter ID of `advanced.re` is `advance`. + +* `@{ChapterID}` : chapter number (ex. `Chapter 17`). +* `@{ChapterID}` : chapter title +* `@<chapref>{ChapterID}` : chapter number and chapter title (ex. `Chapter 17. other topics`). + +`@<hd>` generate referred section title and section number. +You can use deeper section with separator `|`. + +Usage: + +``` +@<hd>{intro|first section} +``` + +If section title is unique, `|` is not needed. + +``` +@<hd>{first section} +``` + +If you want to refer another chapter (file), you should add the chapter ID. + +Usage: + +``` +@<hd>{preface|Introduction|first section} +``` + +When section has the label, you can use the label. + +``` +=={intro} Introduction +: +=== first section +: +@<hd>{intro|first section} +``` + + +### Heading of columns + +You can refer the heading of a column with `@<column>`. + +Usage: + +``` +@<column>{The usage of Re:VIEW} +``` + +You can refer labels. + +``` +==[column]{review-application} The application of Re:VIEW +: +@<column>{review-application} +``` + +## Links + +You can add a hyperlink with `@<href>` and `//label`. +Notation of the markup is `@<href>{URL, anchor}`. If you can use URL itself +as anchor, use `@<href>{URL}`. +If you want to use `,` in URL, use `\,`. + +Usage: + +``` +@<href>{http://github.com/, GitHub} +@<href>{http://www.google.com/} +@<href>{#point1, point1 in document} +@<href>{chap1.html#point1, point1 in document} +//label[point1] +``` + +## Words file + +By creating a word file with key / value pair, `@<w>{key}` or `@<wb>{key}` will be expanded the key to the corresponding value. `@<wb>` means bold style. + +This word file is a CSV file with extension .csv. This first columns is the key, the second row is the value. + +``` +"LGPL","Lesser General Public License" +"i18n","""i""nternationalizatio""n""" +``` + +Specify the word file path in `words_file` parameter of `config.yml`. You can specify multiple word files as `word_file: ["common.csv", "mybook.csv"]`. + +Usage: + +```review +@<w>{LGPL}, @<wb>{i18n} +``` + +(In HTML:) + +``` +Lesser General Public License, ★"i"nternationalizatio"n"☆ +``` + +Values are escaped by the builder. It is not possible to include inline commands in the value. + +## Comments + +If you want to write some comments that do not output in the document, you can use comment notation `#@#`. + +Usage: + +``` +#@# Must one empty line +``` + +If you want to write some warnings, use `#@warn(...)`. + +Usage: + +``` +#@warn(TBD) +``` + +When you want to write comments in the output document, use `//comment` and `@<comment>` with the option `--draft` of review-compile command. + +Usage: + +``` +@<comment>{TODO} +``` + +## Raw Data Block + +When you want to write non-Re:VIEW line, use `//raw` or `//embed`. + +### `//raw` block + +Usage: + +``` +//raw[|html|<div class="special">\nthis is a special line.\n</div>] +``` + +In above line, `html` is a builder name that handle raw data. +You can use `html`, `latex`, `idgxml` and `top` as builder name. +You can specify multiple builder names with separator `,`. +`\n` is translated into newline(U+000A). + +Output: + +(In HTML:) + +``` +<div class="special"> +this is a special line. +</div> +``` + +(In other formats, it is just ignored.) + +Note: `//raw` and `@<raw>` may break structured document easily. + +### `//embed` block + +Usage: + +``` +//embed{ +<div class="special"> +this is a special line. +</div> +//} + +//embed[html,markdown]{ +<div class="special"> +this is a special line. +</div> +//} +``` + +In above line, `html` and `markdown` is a builder name that handle raw data. + +Output: + +(In HTML:) + +``` +<div class="special"> +this is a special line. +</div> +``` + +(In other formats, it is just ignored.) + +### Nested itemize block + +Re:VIEW itemize blocks basically cannot express nested items. Also, none of itemize blocks allow to contain another itemize block or paragraph/image/table/list. + +As a workaround, Re:VIEW 4.2 provides an experimental `//beginchild` and `//endchild`. If you want to include something in an itemize block, enclose it with `//beginchild` and `//endchild`. It is also possible to create a multiple nest. + +``` + * UL1 + +//beginchild +#@# child of UL1 start + + 1. UL1-OL1 + +//beginchild +#@# child of UL1-OL1 start + +UL1-OL1-PARAGRAPH + + * UL1-OL1-UL1 + * UL1-OL1-UL2 + +//endchild +#@# child of UL1-OL1 end + + 2. UL1-OL2 + + : UL1-DL1 + UL1-DD1 + : UL1-DL2 + UL1-DD2 + +//endchild +#@# child of UL1 end + + * UL2 +``` + +Output: + +(In HTML:) + +``` +<ul> +<li>UL1 +<ol> +<li>UL1-OL1 +<p>UL1-OL1-PARAGRAPH</p> +<ul> +<li>UL1-OL1-UL1</li> +<li>UL1-OL1-UL2</li> +</ul> +</li> + +<li>UL1-OL2</li> +</ol> +<dl> +<dt>UL1-DL1</dt> +<dd>UL1-DD1</dd> +<dt>UL1-DL2</dt> +<dd>UL1-DD2</dd> +</dl> +</li> + +<li>UL2</li> +</ul> +``` + +(This is an experimental implementation. Names and behaviors may change in future versions.) + +## Inline Commands + +### Styles + +``` +@<kw>{Credential, credential}:: keyword. +@<bou>{appropriate}:: bou-ten. +@<ami>{point}:: ami-kake (shaded text) +@<u>{AB}:: underline +@<b>{Please}:: bold +@<i>{Please}:: italic +@<strong>{Please}:: strong(emphasis) +@<em>{Please}:: another emphasis +@<tt>{foo($bar)}:: teletype (monospaced font) +@<tti>{FooClass}:: teletype (monospaced font) and italic +@<ttb>{BarClass}:: teletype (monospaced font) and bold +@<code>{a.foo(bar)}:: teletype (monospaced font) for fragments of code +@<tcy>{}:: short horizontal text in vertical text +@<ins>{sentence}:: inserted part (underline) +@<del>{sentence}:: deleted part (strike through) +``` + +### References + +``` +@<chap>{advanced}:: chapter number like `Chapter 17` +@<title>{advanced}:: title of the chapter +@<chapref>{advanced}:: a chapter number and chapter title like `Chapter 17. advanced topic` +@<list>{program}:: `List 1.5` +@<img>{unixhistory}:: `Figure 1.3` +@<table>{ascii}:: `Table 1.2` +@<eq>{emc2}:: `Equation 1.1` +@<hd>{advanced|Other Topics}:: `7-3. Other Topics` +@<column>{another-column}:: reference of column. +``` + +### Other inline commands + +``` +@<ruby>{Matsumoto, Matz}:: ruby markups +@<br>{}:: linebreak in paragraph +@<uchar>{2460}:: Unicode code point +@<href>{http://www.google.com/, google}:: hyper link(URL) +@<icon>{samplephoto}:: inline image +@<m>{a + \alpha}:: TeX inline equation +@<w>{key}:: expand the value corresponding to the key. +@<wb>{key}:: expand the value corresponding to the key with bold style. +@<raw>{|html|<span>ABC</span>}:: inline raw data inline. `\}` is `}`, `\\` is `\`, and `\n` is newline. +@<embed>{|html|<span>ABC</span>}:: inline raw data inline. `\}` is `}` and `\\` is `\`. +@<idx>{string}:: output a string and register it as an index. See makeindex.md. +@<hidx>{string}:: register a string as an index. A leveled index is expressed like `parent<<>>child` +@<balloon>{abc}:: inline balloon in code block. For example, `@<balloon>{ABC}` produces `←ABC`. This may seem too simple. To decorate it, modify the style sheet file or override a function by `review-ext.rb` +``` + +## Commands for Authors (pre-processor commands) + +These commands are used in the output document. In contrast, +commands as below are not used in the output document, used +by the author. + +``` +#@#:: Comments. All texts in this line are ignored. +#@warn(...):: Warning messages. The messages are showed when pre-process. +#@require, #@provide:: Define dependency with keywords. +#@mapfile(filename) ... #@end:: Insert all content of files. +#@maprange(filename, range name) ... #@end:: Insert some area in content of files. +#@mapoutput(command) ... #@end:: Execute command and insert their output. +``` + +You should use these commands with preprocessor command `review-preproc`. + +## Internationalization (i18n) + +Re:VIEW support I18N of some text like `Chapter`, `Figure`, and `Table`. +Current default language is Japanese. + +You add the file locale.yml in the project directory. + +Sample local.yml file: + +```yaml +locale: en +``` + +If you want to customize texts, overwrite items. +Default locale configuration file is in lib/review/i18n.yml. + +Sample local.yml file: + +```yaml +locale: en +image: Fig. +table: Tbl. +``` + +### Re:VIEW Custom Format + +In `locale.yml`, you can use these Re:VIEW custom format. + +* `%pA` : Alphabet (A, B, C, ...) +* `%pa` : alphabet (a, b, c, ...) +* `%pAW` : Alphabet (Large Width) A, B, C, ... +* `%paW` : alphabet (Large Width) a, b, c, ... +* `%pR` : Roman Number (I, II, III, ...) +* `%pr` : roman number (i, ii, iii, ...) +* `%pRW` : Roman Number (Large Width) Ⅰ, Ⅱ, Ⅲ, ... +* `%pJ` : Chainese Number 一, 二, 三, ... +* `%pdW' : Arabic Number (Large Width for 0..9) 1, 2, ...,9, 10, ... +* `%pDW' : Arabic Number (Large Width) 1, 2, ... 10, ... + +Usage: + +``` +locale: en + part: Part. %pRW + appendix: Appendix. %pA +``` + +## Other Syntax + +In Re:VIEW, you can add your customized blocks and inlines. + +You can define customized commands in the file `review-ext.rb`. + +Usage: + +```ruby +# review-ext.rb +ReVIEW::Compiler.defblock :foo, 0..1 +class ReVIEW::HTMLBuilder + def foo(lines, caption = nil) + puts lines.join(",") + end +end +``` + +You can add the syntax as below: + +``` +//foo{ +A +B +C +//} +``` + +``` +# Result +A,B,C +``` + +## HTML/LaTeX Layout + +`layouts/layout.html.erb` and `layouts/layout.tex.erb` are used as layout file. +You can use ERb tags in the layout files. + +Sample layout file(layout.html.erb): + +```html +<html> + <head> + <title><%= @config["booktitle"] %> + + + <%= @body %> +
+ + +``` diff --git a/articles/doc/format_idg.ja.md b/articles/doc/format_idg.ja.md new file mode 100644 index 0000000..17e87b8 --- /dev/null +++ b/articles/doc/format_idg.ja.md @@ -0,0 +1,108 @@ +# Re:VIEW フォーマット InDesign XML 形式拡張 + +Re:VIEW フォーマットから、Adobe 社の DTP ソフトウェア「InDesign」で読み込んで利用しやすい XML 形式に変換できます (通常の XML とほぼ同じですが、文書構造ではなく見た目を指向した形態になっています)。実際には出力された XML を InDesign のスタイルに割り当てるフィルタをさらに作成・適用する必要があります。 + +基本のフォーマットのほかにいくつかの拡張命令を追加しています。 + +このドキュメントは、Re:VIEW 3.0 に基づいています。 + +## 追加したブロック +これらのブロックは基本的に特定の書籍向けのものであり、将来廃棄する可能性があります。 + +* `//insn[タイトル]{ 〜 //}` または `//box[タイトル]{ 〜 //}` : 書式 +* `//planning{ 〜 //}` または `//planning[タイトル]{ 〜 //}` : プランニング +* `//best{ 〜 //}` または `//best[タイトル]{ 〜 //}` : ベストプラクティス +* `//security{ 〜 //}` または `//security[タイトル]{ 〜 //}` : セキュリティ +* `//expert{ 〜 //}` : エキスパートに訊く +* `//point{ 〜 //}` または `//point[タイトル]{ 〜 //}` : ワンポイント +* `//shoot{ 〜 //}` または `//shoot[タイトル]{ 〜 //}` : トラブルシューティング +* `//term{ 〜 //}` : 用語解説 +* `//link{ 〜 //}` または `//link[タイトル]{ 〜 //}` : 他の章やファイルなどへの参照説明 +* `//practice{ 〜 //}` : 練習問題 +* `//reference{ 〜 //}` : 参考情報 + +## 相互参照 + +`//label[〜]` でラベルを定義し、`@{〜}` で参照します。XML としては `
{table_object_soc}に示すようにSnapdragon以外にもさまざまなSoCが存在します。 +Androidで機種依存の不具合が起こりやすいのはこのためです。 + +//table[table_object_soc][Androidの主要なSoC]{ +シリーズ名 メーカー 搭載されている端末の傾向 +-------------------- +Snapdragon Qualcomm社 幅広い端末で使用されている +Helio MediaTek社 一部の廉価端末で使用されている +Kirin HiSilicon社 Huawei社製の端末 +Exynos Samsung社 Samsung社製の端末 +//} + +パフォーマンスチューニングを行う際には、その端末のSoCに何が使用されていて、それがどのようなスペックのものなのかを理解することが重要です。 + +//info{ +Snapdragonの命名はこれまで、「Snapdragon」という文字列と3桁の数字の組み合わせが主流でした。 + +この数字には意味があります。800番台はフラッグシップモデルで、いわゆるハイエンド端末に搭載されます。 +ここから小さい数字になるほど性能と価格が低下し、400番台になるといわゆるローエンド端末になります。 + +たとえ400番台であっても発売時期が新しいほど性能が向上するため一概には言えませんが、基本的には数字が大きいほど性能が高いとみなすことができます。 + +さらに、この命名規則だと近いうちに番号が足りなくなってしまうため、今後はSnapdragon 8 Gen 1のような命名に変更することが2021年に発表されました。 + +このような命名規則については端末の性能を判別するための指標となるため、パフォーマンスチューニングの際には覚えておくと便利です。 +//} + +==={basic_hardware_cpu} CPU +@{CPU, Central Processing Unit}はコンピューターの頭脳とも言うべき存在で、プログラムの実行はもちろん、コンピューターを構成するさまざまなハードウェアとの連携も行っています。 +実際にパフォーマンスチューニングする場合に、CPUの中でどういう処理が行われてどういう特性があるかを知ることで役立つので、ここではパフォーマンス観点で説明します。 + +===={basic_hardware_cpu_basic} CPUの基本 + +プログラムの実行速度を決めるのは単純な演算能力だけではなく、複雑なプログラムのステップをいかに高速に実行できるかどうかです。 +たとえばプログラムの中には四則演算もありますが、分岐処理もあります。 +CPUにとっては次にどの命令が呼び出されるかは、プログラムを実行するまではわかりません。 +そこでCPUは、さまざまな命令を高速に連続で処理できるようハードウェアが設計されています。 + +//image[basic_cpu_pipeline][CPUのパイプライン・アーキテクチャ] + +CPU内部での命令が処理する流れをパイプラインと呼び、パイプラインの中で次の命令を予測しながら処理されています。 +次の命令がもし予測されていない場合は、パイプライン・ストールと呼ばれる一時停止が発生し一度リセットされます。 +ストールする原因の大部分は分岐処理です。分岐自体もある程度は結果を先読みしていますが、それでも間違えることはあります。 +内部構造を覚えなくてもパフォーマンスチューニングは可能ですが、 +こういうことを知っておくだけでもコードを書く際にループの中で分岐を避けるなどが意識できるようになります。 + +//image[basic_cpu_pipeline_stall][CPUのパイプライン・ストール] + +===={basic_hardware_cpu_compute} CPUの演算能力 + +CPUの演算能力は、クロック周波数(単位はHz)とコア数で決定されます。クロック周波数は1秒間に何回CPUが動作できるかを表します。 +そのためクロック周波数が高ければ高いほどプログラムの実行速度は速いです。 + +一方でコア数は、CPUの並列演算能力に寄与します。コアはCPUの動作する基本単位で、それが複数ある場合はマルチコアと呼ばれています。 +もともとはシングルコアのみしかありませんでしたが、シングルコアの場合は複数のプログラムを実行させるために、交互に動作させるプログラムを切り替えています。 +これをコンテキストスイッチと呼び、そのコストはとても高いです。 +スマートフォンに慣れている場合、動作しているアプリ(プロセス)は常に1つと思うかもしれませんが、実際にはOSなどさまざまなプロセスが並行して動作しています。 +そこでこのような状況でも最適な処理能力を提供するために、コアを複数積んだマルチコアが主流となりました。 +スマートフォン向けの場合、2022年現在2-8コア程度が主流です。 + +近年のマルチコア(とくにスマートフォン向け)は非対称コア(big.LITTLE)を搭載するCPUが主流となってきました。 +非対称コアとは、高性能コアと省電力コアを一緒に搭載しているようなCPUを指します。 +非対称コアのメリットは普段は省電力コアのみを動かして電池消費を節約し、ゲームなどパフォーマンスを出さないといけない時にコアを切り替えて使えるというところです。 +ただし省電力コアの分、並列性能の最大値は低下するので、コア数だけでは判断できないことに注意が必要です。 + +//image[basic_cpu_multicore][Snapdragon 8 gen 1の異種コア構成] + +またプログラムが複数のコアを使い切れるかどうかは、プログラムの並列処理の記述に依存します。 +たとえばゲームエンジン側で物理エンジンを別スレッドで動作させるなどの効率化してあるケースや、UnityのJobSystemなどを通じて並列処理を活用しているケースもありますが、 +ゲームのメインループ自体の動作は並列化できないため、マルチコアであってもコア自体の性能は高いほうが有利です。 + +===={basic_hardware_cpu_cache} CPUのキャッシュメモリ + +CPUとメインメモリは物理的に離れた場所に存在し、アクセスするためにほんの僅かな時間(レイテンシ)が必要になります。 +そのためプログラムを実行する際にメインメモリに格納されたデータにアクセスしようとすると、この距離が性能の大きなボトルネックとなります。 +そこでこのレイテンシの問題を解決するために、CPU内部にはキャッシュメモリが搭載されています。 +キャッシュメモリは、主にメインメモリに格納されているデータの一部を格納することで、プログラムが必要とするデータに素早くアクセスできるようにします。 +キャッシュメモリにはL1、L2、L3キャッシュがあり、数字が小さいほど高速ですが小容量です。 +どれぐらい小容量かというと、L3キャッシュでも2-4MBレベルです。 +そのためCPUキャッシュにはすべてのデータを保存することはできず、あくまで直近扱っているデータのみがキャッシュされます。 + +//image[basic_cpu_cache][CPUのL1、L2、L3キャッシュとメインメモリとの関係] + +そこでプログラムのパフォーマンスを高めるためには、データをいかにキャッシュに効率よく載せるかが鍵となりますが、 +プログラム側で自由にキャッシュを制御できないので、データの局所性が重要となります。 +ゲームエンジンにおいてはデータの局所性を意識したメモリ管理は難しいですが、 +UnityのJobSystemなど一部の仕組みではデータの局所性を高めたメモリ配置を実現できます。 + +==={basic_hardware_gpu} GPU +CPUがプログラムを実行することに特化している一方で、@{GPU, Graphics Processing Unit}は画像処理やグラフィックスの描画に特化したハードウェアです。 + + +===={basic_hardware_gpu_basic} GPUの基本 +GPUはグラフィックス処理に特化させるため、CPUとは大きく構造が異なり、単純な計算を大量かつ並行して処理できるような設計となっています。 +たとえば1枚の画像を白黒にしたい場合、CPUを使って計算する場合はある座標のRGB値をメモリから読み取り、グレースケールに変換してメモリに戻す処理を画素毎に実行する必要があります。 +このような処理は分岐もなく、かつそれぞれの画素の計算は他の画素の結果に依存しないので、各画素における計算を並列で行うことが容易です。 + +そこでGPUでは大量のデータに対して同じ演算を適用するような並列処理が高速に実行でき、その結果グラフィックス処理が高速に実行できます。 +とくにグラフィックス系では浮動小数点の演算が大量に必要となるので、GPUは浮動小数点演算が得意です。 +そのため1秒間に何回浮動小数点の演算が行えるかというFLOPSと呼ばれる性能指標が一般的に用いられます。 +また演算能力だけではわかりづらいので、1秒間に何画素描画できるかというフィルレートと呼ばれる指標も用いられます。 + +//image[basic_cpu_gpu_difference][CPUとGPUの違い] + +===={basic_hardware_gpu_compute} GPUの演算能力 +GPUのハードウェアの特徴として、整数および浮動小数点の演算ユニットを含んだコアが大量(数十〜数千)に配置されているところにあります。 +コアを多く配置するために、CPUで必要だった複雑なプログラムを実行するのに必要なユニットは不要なので削ってあります。 +またCPUと同じように動作するクロック周波数が高ければ高いほど1秒間にたくさん演算できます。 + +===={basic_hardware_gpu_memory} GPUのメモリ +GPUももちろん、データを処理するために一時的に保存するためのメモリ領域を必要とします。 +通常、この領域はメインメモリと異なり、GPU専用のメモリとなります。 +そのため何かしらの処理を行うためには、メインメモリからGPUのメモリにデータを転送する必要があります。 +処理後には逆の手順でメインメモリに戻します。 +たとえば複数の解像度の大きいテクスチャの転送など転送量が大きい場合、転送に時間がかかり処理のボトルネックとなるため注意が必要です。 + +//image[basic_gpu_memory][GPUのメモリ転送] + +ただしモバイルにおいては、GPU専用のメモリを搭載するのではなく、メインメモリをCPUとGPUで共用するアーキテクチャが一般的です。 +これはGPUのメモリ容量を動的に変えることができるメリットがある一方で、転送帯域をCPUとGPUでシェアするというデメリットがあります。 +またこの場合においてもCPUとGPUのメモリ領域との間で、データの転送は必要です。 + +====[column] GPGPU + +CPUでは不得意だった大量データに対する並列演算がGPUでは高速に実行できるため、近年はGPUをグラフィックス処理以外の目的にも適用する事例があり、@{GPGPU, General Purpose GPU}と呼ばれています。 +とくにAIなどの機械学習や、ブロックチェーンなどの計算処理に適用される事例が多くあり、そのためGPUの需要が急増し、価格高騰などの影響も出ています。 +またUnityにおいてもコンピュートシェーダーという機能を利用することで、GPGPUを利用できます。 + +====[/column] + +==={basic_memory} メモリ +CPUはその時の計算に必要なデータのみを持つため、基本的にすべてのデータはメインメモリに保持されます。 +物理容量以上のメモリを使うことはできないため、使いすぎるとメモリを確保できなくなり、プロセスがOSから強制終了させられます。 +一般的にこれを@{OOM, Out Of Memory}でKillされたと呼びます。 +2022年現在のスマートフォンでは4-8GBのメモリ容量を備えた端末がメジャーですが、 +それでもメモリを使いすぎないように気をつける必要があります。 + +また前述のようにメモリがCPUと離れているため、メモリを意識した実装を行うかどうかでパフォーマンス自体も変わってきます。 +この節ではパフォーマンスを意識した実装が行えるように、プログラムとメモリの関係を解説します。 + +===={basic_hardware_memory} メモリ・ハードウェア +メインメモリがSoCの中にあったほうが物理的な距離上有利ではあるのですが、メモリはSoCには含まれていません。 +これはSoCの中に含まれているとメモリ搭載量を端末ごとに変えられないなどの理由があります。 +とは言えメインメモリが低速だとプログラムの実行速度に顕著に影響するので、比較的高速なバスでSoCとメモリを繋ぎます。 +このメモリとバスの規格でスマートフォンで一般的に使われているのが@{LPDDR}という規格です。 +LPDDRにもいくつかの世代がありますが、理論上は数Gbps程度の転送速度です。 +もちろん常に理論性能を引き出せるわけではないですが、ゲーム開発ではここがボトルネックとなることはほぼないためそこまで意識する必要はありません。 + +===={basic_memory_os} メモリとOS +1つのOSの中ではたくさんのプロセスが同時に実行されていて、主にシステムプロセスとユーザープロセスがあります。 +システム系はOSを動作させるための重要な役割のプロセスが多く、サービスとして常駐しそのほとんどがユーザーの意思とは関係なく動き続けます。 +一方でユーザー系はユーザーの意思で起動したプロセスで、OSを動作させるためには必須ではありません。 + +スマートフォンでのアプリの表示状態としてフォアグラウンド(最前面)とバックグラウンド(非表示)状態があり、 +一般的には特定のアプリをフォアグラウンドにした場合は他のアプリはバックグラウンドになります。 +アプリがバックグラウンドにある間も復帰処理をスムーズにするためにプロセスは一時停止状態で存在し、メモリもそのまま維持されます。 +ところが全体で使用しているメモリが不足してきた場合は、OSで決められた優先順位にしたがってプロセスをKillします。 +この時にKillされやすいのが、メモリをたくさん使っているバックグラウンド状態のユーザー系アプリ(≒ゲーム)です。 +つまりメモリをたくさん使うゲームは、バックグラウンドに移った際にKillされる可能性が高くなり、その結果ゲームに戻ってきても起動からのやり直しとなるためユーザー体感が悪化します。 + +もしメモリを確保しようとした際、他に殺せるプロセスがなかった場合は自身がKillの対象となります。 +またiOSなどのように、物理容量の一定割合以上のメモリを1つのプロセスで使えないように制御されている場合もあります。 +そのためそもそもメモリを確保できる限界というのは存在します。 +2022年時点ではメジャーなRAMが3GBのiOS端末では、1.3~1.4GB程度が限界となりますので、ゲームを作る上ではこの辺りが上限となりやすいです。 + +===={basic_memory_swap} メモリスワップ +現実にはさまざまなハードウェアの端末があり、搭載されているメモリの物理容量がとても小さいものもあります。 +OSはそのような端末でもなるべく多くのプロセスを動作させるために、さまざまな手法で仮想的なメモリ容量を確保しようとします。 +それがメモリスワップです。 + +メモリスワップで使われる1つの手法がメモリの圧縮です。 +しばらくアクセスのないメモリを中心に、圧縮してメモリ上に保管することで物理容量を節約します。 +ただし圧縮と展開コストがあるため、利用が活発な領域に対して行われず、たとえばバックグラウンドに行ったアプリに対して行われます。 + +もう1つの手法が不使用メモリのストレージ退避です。 +PCのようなストレージが潤沢なハードウェアではプロセスを終了してメモリを確保するのではなく、 +あまり使われていないメモリをストレージに退避させることで物理メモリの空きを確保しようとする場合があります。 +これはメモリ圧縮より大容量のメモリを確保できるというメリットがありますが、ストレージはメモリより低速なのでパフォーマンス上の制約が強いのと、 +そもそもストレージのサイズが小さいスマートフォンではあまり現実的ではないため採用されていません。 + +===={basic_stack_heap} スタックとヒープ +@{スタック}と@{ヒープ}という言葉を一度は聞いたことがあるかもしれません。 +スタックは実はプログラムの動作に深く関係する専用の固定領域になります。 +関数が呼び出されるタイミングで引数やローカル変数などの分が確保され、元の関数へ戻る際に確保した分を解放し、戻り値を積み上げます。 +つまり関数の中で次の関数を呼び出す場合、現時点の関数の情報をそのままに、次の関数をメモリに積んでいきます。 +このようにすることで関数呼び出しの仕組みを実現しています。 +スタックメモリはアーキテクチャに依りますが1MBと容量自体がとても少ないので、限られたデータのみを格納します。 + +//image[basic_stack][スタックの動作模式図] + +一方でヒープはプログラム内部で自由に使えるメモリ領域になります。 +プログラムが必要とすればいつでもメモリ確保命令(Cではmalloc)を出し、大容量のデータを確保して使うことができます。 +もちろん使い終わったら解放処理(free)が必要です。 +C#ではメモリ確保と解放処理が自動的にランタイム側で行われるため、実装者が明示的に行うことはありません。 + +OS側はいつどれぐらいのメモリ容量が必要とされるかがわからないため、必要とされたタイミングでメモリの空き領域から確保して渡します。 +メモリ確保しようとした際に、連続してそのサイズを確保できない場合はメモリ不足となります。 +この連続というキーワードが重要です。 +一般的にメモリ確保と解放を繰り返すと、@{メモリの断片化}が発生します。 +メモリが断片化すると、全体合計での空き領域が足りていても、連続で空いている領域がない場合が考えられます。 +このような場合、OSがまずは@{ヒープの拡張}を実行します。 +つまりプロセスに割り当てるメモリを新規に割り当てることで、連続領域を確保します。 +ただしシステム全体での有限なメモリのため、新規に割り当てるメモリがなくなった場合はOSからプロセスをKillされてしまいます。 + +//image[basic_stack_heap][スタックとヒープ] + +スタックとヒープを比較した際にはメモリ確保のパフォーマンスに顕著な差が生じます。 +それは関数に必要なスタックのメモリ量はコンパイル時点で確定するため、メモリ領域は確保済みなのに対し、 +ヒープは実行するまで必要なメモリ量がわからないため、都度空き領域から探して確保するからです。 +これがヒープが遅く、スタックは速いという所以です。 + +#@# スタックのコピーコストの話いれる? + +====[column] Stack Overflowエラー + +Stack Overflowエラーは関数の再帰呼び出しなどでスタックメモリを使い切ったときに出てしまうエラーです。 +iOS/Androidのデフォルトのスタックサイズは1MBのため、再帰呼び出しによる探索規模が大きくなると発生しやすくなります。 +一般的には再帰呼び出しにならない、ないしは再帰呼び出しが深くならないアルゴリズムへの変更などで対策が可能です。 + +====[/column] + +==={basic_hardware_storage} ストレージ +実際にチューニングを進めると、ファイルを読み込む場面で時間がかかっていることが多いことに気付くかもしれません。 +ファイルを読み込むということは、ファイルが保存されているストレージからデータを読み出して、プログラムから扱えるようにメモリに書き込んでいます。そこで実際に何が起きているかを知っておくとチューニングする時に便利です。 + +まず一般的なハードウェアアーキテクチャの場合は、データを永続化するために専用のストレージを持ちます。 +ストレージは大容量かつ電源なしでデータを永続化できる(不揮発)という特徴があります。 +この特徴を活かし、膨大なアセットはもちろんのこと、アプリ本体のプログラムなどもストレージに格納され、起動時などにストレージから読み込まれて実行されます。 + +//image[basic_far_storage][SoCとストレージの関係性] + +//embed[latex]{ +\clearpage +//} + +====[column] RAMとROM + +とくに日本ではスマホのメモリのことをRAM、ストレージのことをROMと書くことが主流となっていますが、実はROMはRead Only Memoryのことを指します。 +その名の通り読み取り専用で書き込みできないはずなのに、この用語が使われるのは日本の慣習が強いようです。 + +====[/column] + +ところがこのストレージに対する読み書きの処理は、いくつかの観点からプログラムの実行周期と比較してとても遅いものとなっています。 + + * CPUとの物理的な距離がメモリと比べて離れているため、レイテンシが大きく読み書きの速度が遅い + * 命令されたデータとその周辺を含めてブロック単位で読み込むのでムダが多い + * シーケンシャルな読み書きは速い一方で、ランダムな読み書きは遅い + +とくにこのランダムな読み書きが遅いというのは重要なポイントです。 +そもそもどういう場面でシーケンシャルになってどういう場面でランダムになるかと言えば、1つのファイルを先頭から順番に読み書きする場合はシーケンシャルになりますが、 +1つのファイルの複数箇所を飛び飛びに読み書きしたり、複数の小さなファイルを読み書きする場合はランダムになります。 +注意したいのは、同じディレクトリにある複数のファイルを読み書きする場合でも、物理的に連続して配置されているとは限らないため、物理的に離れている場合はランダムになります。 + +====[column] ストレージからの読み出し処理 +ストレージからファイルを読み出す時に、細かい部分は省略しますが、ざっくり下記の流れで処理されます。 + + 1. プログラムがストレージから読み込みたいファイルの領域をストレージコントローラーに命令する + 2. ストレージコントローラーが命令を受け取り、データのある物理上の読み込む領域を計算する + 3. データを読み込む + 4. データをメモリ上に書き込む + 5. プログラムがメモリを通してデータにアクセスする + +またハードウェアやアーキテクチャによってはコントローラーなどのレイヤーが増えたりもします。 +正確に覚えておく必要はないですが、メモリからの読み出しと比較してハードウェアの処理工程が多いというのは意識しましょう。 + +====[/column] + +また一般的なストレージは1つのファイルを4KBなどのブロック単位に書き込むことで、パフォーマンスと空間効率を達成しています。 +このブロックは1つのファイルだとしても物理的に連続して配置されるとは限りません。 +ファイルが物理的に分散している状態を@{断片化, フラグメンテーション}と呼び、断片化を解消する操作を@{デフラグ}と呼びます。 +PCで主流だったHDDでは断片化が問題となることが多かったのですが、フラッシュストレージになり影響はほぼなくなりました。 +スマホにおいてはファイルの断片化を意識する必要はありませんが、PCを考慮する場合は気をつける必要があります。 + +//image[basic_fragmentation][ストレージの断片化] + +====[column] PCとスマホにおけるストレージの種類 +PCの世界ではHDDとSSDが主流です。HDDを見たことないという人もいるかもしれませんが、CDのように円盤状に記録されるメディアで、ディスクの上をヘッドが動いて磁気を読み取ります。 +そのため構造的にも大きく、また物理的な動きが発生するためレイテンシが大きい装置でした。 +近年はSSDが普及し、これはHDDと異なり物理的な動きが発生しないため高速な性能を発揮しますが、その一方で読み書き回数の限界(寿命)があるため頻繁に読み書きが発生すると使えなくなるという特徴があります。 +スマホはSSDとは違いますが、NANDと呼ばれるフラッシュメモリの一種が使われています。 + +====[/column] + +最後に、実際にスマホにおいてストレージがどれぐらいの読み書きの速度があるかですが、2022年現在の1つの目安としては読み込みで100MB/s程度となります。 +仮に10MBのファイルを読み取りたい場合では、理想的な状況であってもファイル全体を読み取るために100ms必要となります。 +さらに複数の細かいファイルを読み込む場合はランダムアクセスが発生するので、ますます読み取りに時間がかかるようになります。 +このように実は意外とファイルの読み込みに時間がかかるというのは常に意識しておいた方がよいです。 +個別の端末の具体的な性能に関してはベンチマーク結果を集めたサイト@{storage_benchmark}があるので参考にしましょう。 + +//footnote[storage_benchmark][@{https://maxim-saplin.github.io/cpdt_results/}] + +最後にまとめると、ファイルの読み書きが発生する場合は以下の観点を意識するとよいです。 + + * ストレージの読み書き速度は意外と遅く、メモリと同等の速度を期待しない + * 同時に読み書きするファイルの数はできる限り減らす(タイミングを分散させる、1つのファイルに纏めるなど) + +=={basic_graphics} レンダリング +ゲームにおいてレンダリングの処理負荷はしばしばパフォーマンスに悪影響を及ぼします。 +したがって、レンダリングに関する知識はパフォーマンスチューニングを行う上で必須であるといえます。 +そのためこの節では、レンダリングの基礎知識についてまとめます。 + +==={basic_graphics_pipeline} レンダリングパイプライン +コンピュータグラフィックスでは、3Dモデルの頂点座標やライトの座標と色などのデータに対して一連の処理を行なうことで、最終的に画面上の各画素に出力する色を出力します。 +この処理の仕組みを@{レンダリングパイプライン}と呼びます。 + +//image[graphics_pipeline_01][レンダリングパイプライン] + +レンダリングパイプラインは、CPUからGPUに必要なデータを送るところから始まります。 +描画するべき3Dモデルの頂点座標やライトの座標をはじめとして、オブジェクトの材質の情報やカメラの情報などさまざまなデータが送られます。 + +このとき送られてくるのは、3Dモデルの頂点座標やカメラの座標、向き、画角などそれぞれ個別のデータです。 +GPUはこれらの情報をまとめて「そのカメラでそのオブジェクトを映した場合に、画面上のどの位置にオブジェクトが表示されるか」を計算して求めます。 +この処理を座標変換と呼びます。 + +オブジェクトが画面上のどの位置に表示されるかが決まったら、次にオブジェクトの色を求める必要があります。 +そこで今度はGPUは「そのライトでその材質のモデルを照らしたときに、画面上の各画素に対応する部分はどのような色になるか」を計算して求めます。 + +//image[graphics_pipeline_02][位置と色を計算] + +上述の処理のうち、「画面上のどの位置にオブジェクトが表示されるか」は@{頂点シェーダー}と呼ばれるプログラムにより計算され、「画面上の各画素に対応する部分はどのような色になるか」は@{フラグメントシェーダー}と呼ばれるプログラムにより計算されます。 + +そしてこれらのシェーダーは自由に記述できます。 +したがって、頂点シェーダーやフラグメントシェーダーに重い処理を書いてしまうと処理負荷が増大します。 + +また、頂点シェーダーの処理は3Dモデルの頂点の数だけ処理されるため、頂点の数が多いほど処理負荷が大きくなります。 +フラグメントシェーダーはレンダリング対象の画素数が多いほど処理負荷が大きくなります。 + +====[column] 実際のレンダリングパイプライン +実際のレンダリングパイプラインでは頂点シェーダーやフラグメントシェーダー以外にも多くのプロセスが存在しますが、本書ではパフォーマンスチューニングに必要な概念の理解を目的としているため、簡易的な説明に留めます。 + +====[/column] + +==={basic_graphics_overdraw} 半透明描画とオーバードロー +レンダリングを行うにあたり、対象のオブジェクトの透明度は重要な問題です。 +たとえばいま、カメラから見たときに一部分が重なっている2つのオブジェクトについて考えます。 + +//image[graphics_overdraw_01][重なっている2つのオブジェクト] + +まずこれらのオブジェクトが両方とも不透明であるケースを考えます。 +この場合、カメラから見て手前にあるオブジェクトから順番に描画処理が行われます。 +こうすると、奥側のオブジェクトを描画する際に、手前のオブジェクトに重なって見えていない部分は処理する必要がありません。つまりこの部分はフラグメントシェーダーの演算をスキップできるということになり、結果として処理負荷を最適化できます。 + +//image[graphics_overdraw_02][不透明描画] + +一方、両方のオブジェクトが半透明だった場合には、手前のオブジェクトに重なっている部分であっても奥側のオブジェクトが透けて見えていなければ不自然です。この場合には、カメラから見て奥側にあるオブジェクトから順番に描画処理を行い、重なった部分の色はすでに描画されている色とブレンドします。 + +//image[graphics_overdraw_03][半透明描画] + +このように、半透明描画は不透明描画と異なり、オブジェクト同士が重なっている部分についても描画処理を行う必要があります。 +もし画面いっぱいに描画される半透明なオブジェクトが2つ存在していたら、画面いっぱい分の処理が2回行われるということになります。このように、半透明なオブジェクトを重ねて描画することを@{オーバードロー}と呼びます。オーバードローが多すぎるとGPUに大きな処理負荷がかかり、パフォーマンスの低下に繋がるため、半透明描画を行う際には適切にレギュレーションを設ける必要があります。 + +====[column] フォワードレンダリングを想定 +レンダリングパイプラインにはいくつかの実装方法があります。そのうち、本項の記述はフォワードレンダリングを想定しています。デファードレンダリングなど他のレンダリング手法には部分的に当てはまらない点もあります。 + +====[/column] + +==={basic_graphics_batching} ドローコール・セットパスコールとバッチング +レンダリングの際にはGPUだけではなくCPUにも処理負荷がかかります。 + +上述の通り、オブジェクトをレンダリングする際にはCPUからGPUに描画するための命令を出します。 +これは@{ドローコール}と呼ばれ、レンダリングするオブジェクトの数だけ実行されます。 +またこのときに、テクスチャなどの情報が前回のドローコールで描画したオブジェクトのものと異なっている場合には、それらをGPUに設定する処理を行います。これは@{セットパスコール}と呼ばれ、比較的重い処理になります。この処理はCPUのレンダースレッドで行われるため、CPUに処理負荷がかかり、多すぎるとパフォーマンスに影響を及ぼします。 + +Unityには、ドローコールを削減するために@{ドローコールバッチング}と呼ばれる仕組みが実装されています。 +これは同じテクスチャなどの情報、つまり同じマテリアルを持つオブジェクトのメッシュをあらかじめCPU側の処理で結合してしまい、1回のドローコールで描画する仕組みです。 +ランタイムでバッチングする@{ダイナミックバッチング}と、あらかじめ結合したメッシュを作成しておく@{スタティックバッチング}があります。 + +また、@{Scriptable Render Pipeline}には@{SRP Batcher}という仕組みが実装されています。 +これを使うと、シェーダーバリアントが同一であれば、メッシュやマテリアルが違っていたとしてもセットパスコールを1回にまとめることができます。 +ドローコールは減りませんが、大きな処理負荷がかかるのはセットパスコールであるため、こちらを減らすための仕組みです。 + +これらのバッチングについてのより詳細な情報は@{tuning_practice_graphics|practice_graphics_draw_call}を参照してください。 + +====[column] GPUインスタンシング +バッチングに似た効果を得られる機能として、@{GPUインスタンシング}があります。 +これはGPUの機能を使うことで、同じメッシュを持つオブジェクトを一度のドローコール・セットパスコールで描画できる機能です。 + +====[/column] + +=={basic_asset_data} データの表現方法 +ゲームには画像や3Dモデル、音声、アニメーションなどさまざまなデータが使われます。 +これらがデジタルデータとしてどのように表現されているかを知ることは、メモリやストレージの容量を計算したり、圧縮などの設定を適切に行ったりする上で重要です。 +この節では基本的なデータの表現方法についてまとめます。 + +==={basic_asset_data_bit} ビットとバイト +コンピューターが表現できる最小の単位はビットです。 +1ビットでは2進数の1桁で表せる範囲、つまり0か1の2通りの組み合わせを表現できます。 +これではたとえばスイッチのON・OFFなどといった簡単な情報しか表せません。 + +//image[basic_asset_data_bit_01][1ビットの情報量] + +ここでビットを2つ使うと、2進数の2桁で表せる範囲、つまり4通りの組み合わせを表現できることがわかります。 +4通りなのでたとえば上・下・左・右のどのキーが押されたかといった情報を表せそうです。 + +//image[basic_asset_data_bit_02][2ビットの情報量] + +同様に8ビットになると2進数の8桁で表せる範囲、つまり2通り ^ 8桁 = 256通りです。 +ここまでくると色々な情報が表現できそうです。 +そしてこの8ビットは1バイトという単位で表されます。 +つまり1バイトとは256通りの情報量を表せる単位であるということができます。 + +//image[basic_asset_data_bit_03][8ビットの情報量] + +また、さらに大きな数を表す単位として、1000バイトを表す1キロバイト (KB)や、1000キロバイトを表す1メガバイト (MB)が存在します。 + +====[column] キロバイトとキビバイト +上記では1KBを1,000バイトと書きましたが、文脈によっては1KBを1,024バイトとする場合もあります。 +明示的に呼び分ける場合には、1000バイトを1キロバイト (KB)と呼び、1,024バイトを1キビバイト (KiB)と呼びます。 +メガバイトについても同様です。 + +====[/column] + +==={basic_asset_data_texture} 画像 +画像データはピクセルの集合として表されています。 +たとえば8 × 8ピクセルの画像であれば、合計8 × 8 = 64個のピクセルで構成されています。 + +//image[basic_asset_data_texture_01][画像データ] + +このとき、各ピクセルはそれぞれ色のデータを持っています。 +では色はデジタルデータでどのように表現されるのでしょうか。 + +まず色は赤 (Red)、緑 (Green)、青 (Blue)、透明度 (Alpha)の4つの要素を組み合わせて作られます。 +これらをチャンネルと呼び、それぞれのチャンネルの頭文字をとってRGBAと表現します。 + +よく使われるTrue Colorという色の表現方法では、RGBAの各値をそれぞれ256段階で表します。 +前節で説明した通り、256段階とはつまり8ビットです。 +すなわちTrue Colorは4チャンネル × 8ビット = 32ビットの情報量で表すことができます。 + +//image[basic_asset_data_texture_02][1色の情報量] + +したがって、たとえば8 × 8ピクセルのTrue Colorの画像であればその情報量は8ピクセル × 8ピクセル × 4チャンネル × 8ビット = 2,048ビット = 256バイトとなります。 +1,024 × 1,024ピクセルのTrue Colorの画像であれば、その情報量は1,024ピクセル × 1,024ピクセル × 4チャンネル × 8ビット = 33,554,432ビット = 4,194,304バイト = 4,096キロバイト = 4メガバイトとなります。 + +==={basic_asset_data_compression} 画像の圧縮 +実際には、画像は圧縮されたデータとして使用されることがほとんどです。 + +圧縮とは、データの格納方法を工夫することでデータ量を減らすことです。 +たとえばいま、同じ色をしたピクセルが5つ隣り合っていたとします。 +この場合、各ピクセルの色情報を5つ持つよりも、色の情報ひとつと、それが5個並んでいるという情報を持った方が情報量は減ります。 + +//image[basic_asset_data_compression_01][圧縮] + +実際にはもっと複雑な圧縮方法がたくさん存在します。 + +具体例として、モバイルで代表的な圧縮フォーマットであるASTCを紹介します。 +ASTC6x6というフォーマットを適用すると、1024x1024のテクスチャが4メガバイトから約0.46メガバイトに圧縮されます。 +つまり、容量は8分の1以下に圧縮されたという結果となり、圧縮を行うことの重要性を認識できます。 + +参考までに、モバイルで主に利用されるASTCフォーマットの圧縮率について以下に記載します。 + +//table[compression][圧縮形式と圧縮率]{ +圧縮形式 圧縮率 +-------------------- +ASTC RGB(A) 4x4 0.25 +ASTC RGB(A) 6x6 0.1113 +ASTC RGB(A) 8x8 0.0625 +ASTC RGB(A) 10x10 0.04 +ASTC RGB(A) 12x12 0.0278 +//} + +なおUnityでは、テクスチャのインポート設定によりさまざまな圧縮方法を、プラットフォームごとに指定できます。 +そのため非圧縮の画像をインポートし、このインポート設定により圧縮をかけることで最終的に使用されるテクスチャを生成するというやり方が一般的となっています。 + +====[column] GPUと圧縮形式 +あるルールに基づいて圧縮した画像は、当然ですがそのルールに基づいて展開する必要があります。 +この展開処理はランタイムで行われます。 +この処理負荷を最小限に抑えるため、GPUが対応した圧縮形式を使うことが重要です。 +モバイルデバイスのGPUが対応している代表的な圧縮形式としてASTCが挙げられます。 + +====[/column] + +==={basic_asset_data_mesh} メッシュ +3DCGでは、3D空間上に三角形を多数繋ぎ合わせることで立体形状を表現しています。 +この三角形の集まりを@{メッシュ}と呼びます。 + +//image[basic_asset_data_mesh_01][三角形の組み合わせによる立体] + +この三角形は、データとしては3D空間上の3点の座標情報として表すことができます。 +この各点を@{頂点}と呼び、その座標を@{頂点座標}と呼びます。 +またメッシュ1つあたりの頂点情報はすべて1つの配列に格納されます。 + +//image[basic_asset_data_mesh_02][頂点情報] + +頂点情報は1つの配列に格納されるため、そのうちのどれを組み合わせて三角形を構成するかを表す情報が別途必要です。 +これを@{頂点インデックス}と呼び、頂点情報の配列のインデックスを表すint型の配列として表現されます。 + +//image[basic_asset_data_mesh_03][頂点インデックス] + +//embed[latex]{ +\clearpage +//} + +オブジェクトにテクスチャを貼り付けたり、ライティングを行なったりする上ではさらに追加の情報が必要です。 +たとえばテクスチャをマッピングするにはUV座標が必要です。 +またライティングをする上では、頂点カラーや法線、接線などの情報も使われます。 + +次の表は、主な頂点情報と1頂点あたりの情報量をまとめたものです。 + +//table[table_object_vertex_info][頂点情報]{ +名前 1頂点あたりの情報量 +-------------------- +頂点座標 3次元のfloat = 12バイト +UV座標 2次元のfloat = 8バイト +頂点カラー 4次元のfloat = 16バイト +法線 3次元のfloat = 12バイト +接線 3次元のfloat = 12バイト +//} + +メッシュのデータは頂点の数や1つの頂点で扱う情報の量が増えるほど大きくなるため、頂点数や頂点情報の種類を事前に決めておくことは重要です。 + +==={basic_asset_data_animation} キーフレームアニメーション +ゲームではUIのアニメーションや3Dモデルのモーションなど、多くの箇所にアニメーションを使用します。 +アニメーションの代表的な実現手法として、キーフレームアニメーションがあります。 + +キーフレームアニメーションは、ある時間(キーフレーム)における値を表すデータの配列で構成されます。 +キーフレーム間の値は補間により求められるので、あたかも滑らかに連続したデータであるかのように取り扱うことができます。 + +//image[basic_asset_data_animation_01][キーフレーム] + +なおキーフレームが持つ情報は時間と値の他に、接線やその重みといったものがあります。 +これらを補間の計算に利用することで、少ないデータ量でより複雑なアニメーションを実現することできます。 + +//image[basic_asset_data_animation_02][接線と重み] + +キーフレームアニメーションにおいてはキーフレームが多ければ多いほど複雑なアニメーションを表現できます。 +しかしながら、データ量もキーフレームの数に応じて増大します。 +このような理由から、キーフレームの数は適切に設定する必要があります。 + +できるだけ同じようなカーブを保ちつつキーフレームを削減してデータ量を圧縮する手法もあります。 +Unityの場合、モデルのインポート設定で次図のようにキーフレームを削減できます。 + +//image[basic_asset_data_animation_03][インポート設定] + +設定方法の詳細は@{tuning_practice_asset|practice_asset_animation}を参照してください。 + +=={basic_unity} Unityの仕組み + +Unityエンジンが実際にどういう仕組みで動いているかを理解することは、ゲームをチューニングする上で重要であることはいうまでもありません。 +この節で知っておくべきUnityの動作原理を説明します。 + +==={basic_unity_output_binary} バイナリとランタイム + +まずここではUnityが実際にどういう仕組みでランタイムを動かしているかを解説します。 + +===={basic_unity_output_binary_csharp} C#とランタイム + +Unityでゲームを作る際、開発者はC#で挙動をプログラミングします。 +Unityでゲームを開発する際、度々コンパイル(ビルド)が実行されるように、C#はコンパイラ型言語です。 +ところがC#が伝統的なC言語などと異なるのは、コンパイルするとマシンで単体実行可能な機械語ではなく、 +.NETにおける@{中間言語, Intermediate Language; 以降IL}にコンパイルされることです。 +ILに変換された実行コードは単体では実行できないので、.NET Frameworkのランタイムを用いて逐次機械語に変換しながら実行されます。 + +//image[basic_csharp_il][C#のコンパイル過程] + +一度ILを挟むのは、機械語に変換してしまうと単一のプラットフォームでしか実行できないバイナリとなってしまうためです。 +ILであれば、どのようなプラットフォームでもそのプラットフォームに対応したランタイムを用意するだけで動作するようになるため、 +プラットフォーム毎にバイナリを用意する必要がなくなります。 +そのためUnityの基本原理としては、ソースコードをコンパイルして得られたILをそのままそれぞれの環境向けのランタイムで実行することで、マルチプラットフォームを実現しています。 + +====[column] ILコードを確認してみよう + +普段は目にすることが少ないILコードは、メモリ確保や実行速度などのパフォーマンスを意識する上で非常に重要です。 +たとえば配列とListでは、一見同じforeachループでも異なるILコードが出力され、配列の方がパフォーマンスに優れているコードとなります。 +また意図しない隠れたヒープアロケーションも発見できるかもしれません。 +こういったC#とILコードの対応感覚を身につけるために、普段から自分の書いたC#コードのIL変換結果を確認しておくことはオススメです。 +Visual StudioやRiderといったIDEでILコードを閲覧できますが、ILコード自体はアセンブリと呼ばれる低級言語のため理解するのが難しい言語です。 +そのような場合にはSharpLab@{sharplab}というWebサービスを利用するとC# -> IL -> C#とILから逆変換したコードを確認することで理解しやすくなります。 +本書の後半の@{tuning_practice_script_csharp}にて、実際の変換例を紹介します。 + +====[/column] + +//footnote[sharplab][@{https://sharplab.io/}] + +===={basic_unity_output_binary_il2cpp} IL2CPP + +前述のようにUnityでは基本的にはC#をILコードにコンパイルしてランタイムで実行しますが、2015年頃から一部の環境で問題が生じるようになりました。 +それはiOSやAndroidで動作するアプリの64bit対応です。 +C#はILコードを実行するためにそれぞれの環境で動作するためのランタイムが必要になるのは前述の通りですが、 +実はそれまでのUnityは長年.NET FrameworkのOSS実装である@{Mono}をフォークしてUnity自ら改変して利用していました。 +つまりUnityが64bit対応するためには、フォークしたMonoを64bit対応させる必要がありました。 +もちろんそれはとてつもない労力が必要となるため、Unityはここで代わりに@{IL2CPP}と呼ばれる技術を開発することでこの難題を乗り切りました。 + +IL2CPPとは名前の通りIL to CPPのことであり、ILコードをC++コードに変換する技術です。 +C++はどのような開発環境でもネイティブサポートされるような汎用性の高い言語であるため、 +C++コードに出力してしまえばそれぞれの開発ツールチェインにて機械語にコンパイルすることが可能です。 +したがって64bit対応はツールチェインの仕事となるため、Unity側はその対応をする必要がなくなります。 +またC#と違ってビルド時点で機械語にコンパイルされるため、ランタイムにて機械語に変換する必要がなくなり、パフォーマンスが向上するという恩恵もあります。 + +C++コードは一般的にビルドに時間を要するという欠点はありますが、64bit対応とパフォーマンスを一挙に解決するIL2CPPという技術はUnityの要となりました。 + +===={basic_unity_output_binary_runtime} Unityランタイム + +ところでUnityでは開発者はC#でゲームをプログラミングしますが、エンジンと呼ばれるUnity自体のランタイムは実はC#で動いているわけではありません。 +ソース自体はC++で記述され、プレイヤーと呼ばれる部分は各環境で実行するために事前にビルドされた状態で配布されます。 +UnityがエンジンをC++で記述するのは、いくつかの理由が考えられます。 + + * 高速かつ省メモリのパフォーマンスを得るため + * なるべく多くのプラットフォームに対応するため + * エンジンの知的財産権の保護のため(ブラックボックス化) + +開発者が記述したC#コードはあくまでC#で動作するため、Unityではネイティブで動作するエンジン部分と、C#ランタイムで動作するユーザーコード部分に2つの領域が必要となります。 +エンジンとユーザーコードは、実行中に適宜データをやり取りすることで動作しています。 +たとえば@{GameObject.transform}をC#から呼び出した場合、 +シーンの状態などゲームの実行状態はすべてエンジン内部で管理されているため、 +まずネイティブ呼び出しを行ってネイティブ領域のメモリデータにアクセスし、 +C#に値を返すという手順を踏んでいます。 +ここで注意したいのは、C#とネイティブではメモリは共有されないため、C#で必要となったデータは都度C#側でメモリが確保されることです。 +またAPI呼び出しもネイティブ呼び出しが発生するなど高価なものになるため、頻繁に呼び出さずに値をキャッシュするという最適化手法が必要となります。 + +//image[basic_unity_memory][Unityにおけるメモリの状態イメージ] + +このように、Unityを開発する上では見えないエンジン部分もある程度意識する必要があります。 +そのため適宜Unityエンジンのネイティブ領域とC#を繋ぐインターフェイスのソースコードを見るとよいでしょう。 +幸いにもUnity社がC#の部分であればGitHubで公開@{unity_cs_ref}しているため、ほとんどネイティブ呼び出しになっていることがわかるなど非常に役立ちます。 +必要に応じて活用することをオススメします。 + +//footnote[unity_cs_ref][@{https://github.com/Unity-Technologies/UnityCsReference}] + +==={basic_unity_output_asset} アセットの実体 + +前節で説明したように、Unityエンジンはネイティブで実行されているため、基本的にはC#側ではデータを持ちません。 +アセットの取り扱いに関しても同様で、ネイティブ領域でアセットをロードし、C#に参照を返したり、データをコピーして返していたりするだけです。 +そのためアセットをロードする際は、大別すると、Unityエンジン側でロードさせるためにパスを指定をする方法と、バイト配列など生データを直接渡す方法の2種類があります。 +パスを指定した場合はネイティブ領域でロードするためC#側でメモリを消費することはありませんが、 +バイト配列などデータをC#側からロード・加工して渡した場合はC#側とネイティブ側で二重にメモリを消費してしまいます。 + +またアセットの実体がネイティブ側にあるため、アセットの多重ロードやリークに関する調査の難易度も上がります。 +これは開発者は主にC#側のプロファイリングやデバッグを中心に行うためです。 +C#側の実行状態だけ見ても理解することは難しく、エンジン側の実行状態と突き合わせながら解析する必要がありますが、 +ネイティブ領域のプロファイリングはUnityが提供するAPIに依存するためツールが限られるという問題があります。 +本書でさまざまなツールを駆使して分析する手法を紹介しますが、その際にC#とネイティブの空間を意識すると理解しやすくなります。 + +==={basic_unity_thread} スレッド + +スレッドはプログラムの実行単位で、一般的には1つのプロセスの中に複数のスレッドを生成しながら処理が進みます。 +CPUの1つのコアは同時に1つのスレッドしか処理することができないため、 +複数のスレッドを処理するために高速にスレッドを切り替えながらプログラムを実行します。 +これを@{コンテキストスイッチ}と呼びます。 +コンテキストスイッチする際はオーバーヘッドが生じるため、頻繁に発生すると処理効率が低下してしまいます。 + +//image[basic_thread][スレッドの模式図] + +プログラムの実行時には基底となる@{メインスレッド}が生成され、そこからプログラムが必要に応じて別のスレッドを生成・管理します。 +Unityのゲームループはシングルスレッドで動作する設計となっているため、 +ユーザーが記述したスクリプトは基本的にはメインスレッド上で動作することになります。 +逆にメインスレッド以外からUnity APIを呼び出そうとすると、ほとんどのAPIはエラーが発生してしまいます。 + +メインスレッドから別のスレッドを作成して処理を実行する場合、そのスレッドがいつ実行されて、いつ完了するかはわかりません。 +そのためスレッド間で処理を同期させる手段として@{シグナル}と呼ばれる機構があります。 +別スレッドの処理を待機する場合、そのスレッドからシグナルを通知してもらうことで待機を解除できます。 +このシグナル待機はUnity内部でも使われているためプロファイリング時などに観測できますが、WaitFor~という名前の通りただ別の処理を待機しているだけということは注意しましょう。 + +==== Unity内部のスレッド + +とはいえあらゆる処理をメインスレッドで実行していると、プログラム全体の処理に時間がかかるようになってしまいます。 +複数の重い処理があり、それが相互に依存がなかった場合、ある程度処理を同期することで並列処理を行うことができれば、 +プログラムの実行を短縮することが可能となります。 +こうした高速化のために、ゲームエンジン内部では並列処理が多数用いられます。 +その1つが@{レンダースレッド, Render Thread}です。 +名前の通りレンダリング専用のスレッドで、メインスレッドで計算したフレームの描画情報を、 +グラフィックスコマンドとしてGPUに送る役割を担います。 + +//image[basic_render_thread][メインスレッドとレンダースレッド] + +メインスレッドとレンダースレッドはパイプラインのように実行されるため、 +レンダースレッドが処理中に次のフレームの計算が始まります。 +ところがもしレンダースレッド内で1フレームを処理する時間が長くなってくると、 +次のフレームの描画の計算が終わったとしても描画を開始することができなくなり、 +メインスレッドは待たされることになります。 +ゲーム開発ではメインスレッド、レンダースレッドどちらが重くなってもFPSが低下してしまうため注意しましょう。 + +==== 並列処理可能なユーザー処理のスレッド化 + +またゲーム特有の部分として、物理エンジンや揺れものなど並列処理を実行できる計算タスクが多数存在します。 +そのような計算をメインスレッド以外で実行させるために、Unityでは@{ワーカースレッド, Worker Thread}が存在します。 +ワーカースレッドはJobSystemを通して生成された計算タスクを実行します。 +JobSystemを利用することでメインスレッドの処理負荷を軽減できる場合は積極的に利用しましょう。 +もちろんJobSystemを利用せずに、自前でスレッドを生成する方法もあります。 + +スレッドはパフォーマンスチューニングで便利な半面、使いすぎると逆にパフォーマンスが低下したり、 +処理の複雑性が向上する危険性もあるため、闇雲に使わないことをオススメします。 + +==={basic_unity_game_loop} ゲームループ + +Unityを含む一般的なゲームエンジンは、@{ゲームループ, プレイヤーループ}と呼ばれる、エンジンのルーチン処理があります。 +簡潔にループを表現するのならば、概ね以下のようになります。 + + 1. キーボード、マウス、タッチディスプレイなどのコントローラーの入力処理 + 2. 1フレームの時間で進行すべきゲームの状態を計算 + 3. 新しいゲームの状態をレンダリング + 4. ターゲットFPSに応じて、次のフレームまで待機 + +このループを繰り返すことでゲームを映像としてGPUに出力します。 +もし1フレーム内の処理に時間がかかるようになると、もちろんFPSが低下することになります。 + +==== Unityにおけるゲームループ + +Unityにおけるゲームループは、皆さん一度は見たことがあるUnityの公式リファレンスにゲームループの模式図@{unity_gameloop}が存在します。 + +//image[basic_monobehaviour_flowchart][Unityのイベントの実行順序] + +この図は厳密にはMonoBehaviourのイベントの実行順を表したもので、ゲームエンジンとしてのゲームループ@{unity_playerloop}とは異なりますが、 +開発者が知っておくべきゲームループとしてはこれで十分です。 +とくに重要なイベントとして、@{Awake, OnEnable, Start, FixedUpdate, Update, LateUpdate, OnDisable, OnDestroy}と各種コルーチンの処理タイミングです。 +イベントの実行順やタイミングを勘違いしてしまうと、思わぬメモリリークや余計な計算につながってしまう可能性があります。 +そのため重要イベントの呼び出しタイミングや、同イベント内での実行順序などの性質は把握しておくべきでしょう。 + +物理演算に関しては、通常のゲームループと同じ間隔で実行していると衝突判定されずにオブジェクトがすり抜けてしまうなど特有の問題があります。 +そのため通常は物理演算ルーチンのループを高頻度に回すように、ゲームループとは異なる間隔でループを回します。 +ただ闇雲に回すとメインのゲームループの更新処理と競合する可能性があるため、ある程度は処理を同期させる必要があります。 +そのため物理演算が必要以上に重くなるとフレームの描画処理に影響したり、またフレームの描画処理が重くなると物理演算が遅れてすり抜けが発生したりと、 +互いに影響する可能性があるため注意しましょう。 + +//footnote[unity_gameloop][@{https://docs.unity3d.com/ja/current/Manual/ExecutionOrder.html}] +//footnote[unity_playerloop][@{https://tsubakit1.hateblo.jp/entry/2018/04/17/233000}] + +==={basic_unity_gameobject} GameObject +前述のように、Unityのエンジン自体はネイティブで動作しているため、C#のUnity APIもその大部分は内部のネイティブAPIを呼び出すためのインターフェイスです。 +それは@{GameObject}やそれにアタッチするコンポーネントを定義する@{MonoBehaviour}も同様で、常にC#側からネイティブの参照を持ち続けることになります。 +ところがネイティブ側でデータを管理しつつ、C#側でもそれらの参照を持っている場合、破棄のタイミングで不都合が発生します。 +それはネイティブ側で破棄されたデータに対して、C#からの参照を勝手に消すことができないからです。 + +実際に@{unity_gameobject_destroy_test}で破棄したGameObjectがnullかどうかチェックしていますが、ログには@{true}が出力されます。 +これは標準のC#の挙動としては不自然で、@{_gameObject}にはnullを代入していないため@{GameObject}型のインスタンスの参照が残っているはずです。 + +//embed[latex]{ +\clearpage +//} + +//list[unity_gameobject_destroy_test][破棄後の参照テスト][C#]{ +public class DestroyTest : UnityEngine.MonoBehaviour +{ + private UnityEngine.GameObject _gameObject; + + private void Start() + { + _gameObject = new UnityEngine.GameObject("test"); + StartCoroutine(DelayedDestroy()); + } + + System.Collections.IEnumerator DelayedDestroy() + { + // cache WaitForSeconds to reuse + var waitOneSecond = new UnityEngine.WaitForSeconds(1f); + yield return waitOneSecond; + + Destroy(_gameObject); + yield return waitOneSecond; + + // _gameObject is not null, but result is true + UnityEngine.Debug.Log(_gameObject == null); + } +} +//} + +これはUnityのC#側の仕組みで、破棄済みデータへのアクセスの制御を行っているからです。 +実際にUnityのC#実装部の@{UnityEngine.Object}のソースコード@{github_unity_object}を参照すると、以下のようになっています。 + +//list[unity_object_source][UnityEngine.Objectの==オペレーターの実装][C#]{ + // 抜粋 + public static bool operator==(Object x, Object y) { + return CompareBaseObjects(x, y); + } + + static bool CompareBaseObjects(UnityEngine.Object lhs, + UnityEngine.Object rhs) + { + bool lhsNull = ((object)lhs) == null; + bool rhsNull = ((object)rhs) == null; + + if (rhsNull && lhsNull) return true; + + if (rhsNull) return !IsNativeObjectAlive(lhs); + if (lhsNull) return !IsNativeObjectAlive(rhs); + + return lhs.m_InstanceID == rhs.m_InstanceID; + } + + static bool IsNativeObjectAlive(UnityEngine.Object o) + { + if (o.GetCachedPtr() != IntPtr.Zero) + return true; + + if (o is MonoBehaviour || o is ScriptableObject) + return false; + + return DoesObjectWithInstanceIDExist(o.GetInstanceID()); + } +//} + +要約すると、null比較をしたときはネイティブ側のデータが存在するかどうかをチェックしているために、破棄されたインスタンスへのnull比較が@{true}になります。 +そのためにnullでない@{GameObject}のインスタンスが一部nullのように振る舞います。 +この特性は一見すると便利なのですが、非常に厄介な側面もあります。 +それは@{_gameObject}は実際にはnullではないので、メモリリークを引き起こすからです。 +@{_gameObject}1個分のメモリリークは当然ですが、たとえばそのコンポーネントの中からマスターなどの巨大なデータへの参照を持っている場合、 +C#としては参照が残るため、ガベージコレクションの対象とはならないので巨大なメモリリークに繋がってしまいます。 +これを回避するためには、@{_gameObject}にnullを代入するなどの対策が必要となります。 + +//footnote[github_unity_object][@{https://github.com/Unity-Technologies/UnityCsReference/blob/c84064be69f20dcf21ebe4a7bbc176d48e2f289c/Runtime/Export/Scripting/UnityEngineObject.bindings.cs}] + + +==={basic_unity_assetbundle} AssetBundle + +スマホ向けゲームはアプリのサイズに制限があり、すべてのアセットをアプリに含めることができません。 +そのため必要に応じてアセットをダウンロードするために、UnityにはAssetBundleという複数のアセットをパッキングして、動的にロードする仕組みがあります。 +一見簡単に扱えるように感じるかもしれませんが、大規模プロジェクトの場合は適切に設計しないと思わぬところでメモリをムダに使ってしまうなど、 +メモリやAssetBundleに対する十分な理解と丁寧な設計が求められます。 +そのためこの節ではAssetBundleについてチューニングの観点で知っておくべきことを説明します。 + +===={basic_unity_assetbundle_compress} AssetBundleの圧縮設定 + +AssetBundleはビルド時にデフォルトでLZMA圧縮されます。 +これを@{BuildAssetBundleOptions}の@{UncompressedAssetBundle}に変えることで無圧縮に、 +@{ChunkBasedCompression}に変えることでLZ4圧縮に変更することが可能です。 +これらの設定の差は以下の@
{assetbundle_compression}のような傾向があります。 + +//table[assetbundle_compression][AssetBundleの圧縮設定による違い]{ +項目 無圧縮 LZMA LZ4 +------------------------------------------------------------- +ファイルサイズ 特大 特小 小 +ロード時間 速い 遅い かなり速い +//} + +つまりロード時間を最速にするなら無圧縮がよいですが、ファイルサイズが致命的に大きくなるためスマートフォンにおける記憶領域の浪費を避けるためには基本的に使用できません。 +一方でLZMAはファイルサイズが一番小さくなりますが、アルゴリズムの問題で展開に時間がかかる、部分的な展開処理ができないという欠点があります。 +LZ4は速度とファイルサイズのバランスのよい圧縮設定で、@{ChunkBasedCompression}の名の通り部分展開が可能なためLZMAのように全体を展開しなくても部分読み込みが可能です。 + +またAssetBundleには端末キャッシュ時に圧縮設定を変える@{Caching.compressionEnabled}があります。 +つまり配信はLZMAで、端末でLZ4に変換することで、ダウンロードサイズを最小にしつつ、実際に使う際にはLZ4の恩恵を受けられるようになります。 +ただし端末側で再圧縮するということは、それだけ端末でのCPUの処理コストがかかる、メモリや記憶領域を一時的に浪費してしまうといった問題があります。 + +===={basic_unity_assetbundle_dependency} AssetBundleの依存関係と重複 + +あるアセットが複数のアセットから依存されている場合、AssetBundle化する際には注意が必要です。 +たとえばマテリアルAとマテリアルBがテクスチャCに依存している場合、テクスチャをAssetBundle化せずに、マテリアルAとBだけAssetBundle化すると、 +生成される2つのAssetBundleのそれぞれにテクスチャCが含まれるため、重複してムダになってしまいます。 +もちろん容量を使うという点でもムダなのですが、2つのマテリアルをメモリにロードする際にテクスチャが別々にインスタンス化されるため、メモリもムダにしてしまいます。 + +同一アセットが複数のAssetBundleに含まれるのを避けるためには、テクスチャCも単体でAssetBundle化してマテリアルのAssetBundleから依存される形にするか、 +マテリアルA、BとテクスチャCを1つにしたAssetBundleにする必要があります。 + +//image[basic_assetbundle_dependency][AssetBundleの依存関係がある例] + +===={basic_unity_assetbundle_instance} AssetBundleからロードされたアセットの同一性 + +AssetBundleからのアセットをロードする時の重要な性質として、AssetBundleがロードされている間は同じアセットを何度ロードしても同じインスタンスが返ってきます。 +これはUnity内部でロード済みのアセットを管理していることを示し、Unity内部ではAssetBundleとアセットは紐付けられた状態になります。 +この性質を利用することで、ゲーム側でアセットのキャッシュ機構を作らずにUnity側に委ねることも可能です。 + +ただし@{AssetBundle.Unload(false)}でアンロードした場合のアセットは、@{basic_assetbundle_leak}のように再度同じAssetBundleから同じアセットをロードしても別インスタンスとなるため、注意が必要です。 +これはアンロードするタイミングでAssetBundleとアセットの紐付けが解除されるためで、アセットの管理が宙に浮いた状態に状態になるためです。 + +//image[basic_assetbundle_leak][AssetBundleとアセットの管理が不適切でメモリリークする例] + +===={basic_unity_assetbundle_destroy} AssetBundleからロードしたアセットの破棄 + +@{AssetBundle.Unload(true)}でAssetBundleをアンロードする場合は、ロードしたアセットも完全に破棄されるためにメモリに関してとくに困ることはありませんが、 +@{AssetBundle.Unload(false)}を使用する場合は適切なタイミングでアセットのアンロード命令を呼び出さないとアセットが破棄されません。 +そのため後者を使用する場合は、シーン切替時などでアセットが破棄されるように適切に@{Resources.UnloadUnusedAssets}を呼び出す必要があります。 +また@{Resources.UnloadUnusedAssets}の名前の通り、参照が残っている場合は解放されないことにも注意が必要です。 +なお、Addressableを使用する場合は内部で@{AssetBundle.Unload(true)}を呼び出します。 + +=={basic_csharp} C#の基礎知識 + +この節では、パフォーマンスチューニングをする上で欠かせない、C#の言語仕様やプログラム実行時の挙動について説明します。 + +==={basic_csharp_stack_heap} スタックとヒープ + +@{basic|basic_stack_heap}ではプログラム実行時のメモリ管理方式としてのスタックとヒープが存在することを紹介しました。 +スタックはOSが管理するのに対して、ヒープはプログラム側が管理します。 +つまりヒープメモリがどうやって管理されているかを知ることで、メモリを意識した実装を行うことができます。 +ヒープメモリの管理の仕組みは、プログラムの元となったソースコードの言語仕様に依るところが大きいので、C#におけるヒープメモリの管理について解説します。 + +本来のヒープメモリは必要なタイミングでメモリ確保し、使い終わったらメモリを解放する必要があります。 +もしメモリを解放しない場合はメモリリークとなり、アプリケーションが使うメモリ領域が膨らみ、最終的にはクラッシュに繋がってしまいます。 +ところがC#には明示的なメモリ解放処理はありません。 +これはC#のプログラムが実行される.NETランタイム環境では、ヒープメモリがランタイムによって自動で管理され、使い終わったメモリは適切なタイミングで解放されるためです。 +このためヒープメモリのことを@{マネージドヒープ}とも呼びます。 + +スタックに確保されたメモリは関数のライフタイムと一致するので、関数の最後にメモリを解放してあげるだけでよいのですが、 +ヒープで確保されたメモリは関数のライフタイムを超えて生存することがほとんどです。 +つまりヒープメモリを必要としたり使い終わったりするタイミングがさまざまであるため、自動かつ効率よくヒープメモリを使うための仕組みが必要になります。 +詳細については次の項で紹介しますが、その仕組みを@{ガベージコレクション, Garbage Collection}と呼びます。 + +実はUnityにおける@{GC.Alloc}は独自の用語で、ガベージコレクションで管理されているヒープメモリに確保(Allocation)されたメモリのことを表しています。 +そのためGC.Allocを減らすことは、動的に確保されるヒープメモリの量を減らすことになります。 + +==={basic_csharp_gc} ガベージコレクション + +C#のメモリ管理において、未使用のメモリの検索や解放はガベージコレクション、略して「GC」と呼ばれます。 +ガベージコレクターは周期的に実行されます。ただし、正確な実行タイミングはアルゴリズムによって異なります。 +これにより、ヒープ上のすべてのオブジェクトが一斉調査され、すでに参照されなくなっているすべてのオブジェクトが削除されます。 +つまり、参照の外されたオブジェクトが削除され、メモリ領域が解放されます。 + +ガベージコレクターにはさまざまなアルゴリズムがありますが、UnityではデフォルトでBoehm GCアルゴリズムが使用されています。 +Boehm GCアルゴリズムの特徴は、「非世代別」で「非圧縮型」であることです。 +「非世代別」とは、ガベージコレクションを1回実行するごとにヒープ全体を一斉調査しなければならないことを意味しています。 +このため、ヒープが拡張するのに応じて検索範囲も拡がるためパフォーマンスが低下します。 +「非圧縮型」とは、オブジェクト同士の隙間を詰めるためにメモリ内のオブジェクト移動が行われないことを意味します。 +つまり、メモリ上に細かい隙間を生む断片化が起こりやすく、マネージヒープの拡張がされやすい傾向にあります。 + +それぞれ計算コストが高い処理でありかつ他の処理をすべて止めてしまう同期的な処理であるため、 +ゲーム中に走るといわゆる「Stop the World」と呼ばれる処理落ちの原因に繋がります。 + +Unity 2018.3からはGCModeを指定できるようになり、一時的に無効化することが可能になりました。 + +//listnum[GCMode][][csharp]{ +GarbageCollector.GCMode = GarbageCollector.Mode.Disabled; +//} + +しかし、当然のことながら無効化している期間にGC.Allocをしてしまうと、ヒープ領域は拡張かつ消費され、 +最終的には新たに確保できなくなりアプリのクラッシュへと繋がります。メモリ使用量は簡単に増大していくため、 +無効化している期間ではGC.Allocが一切行われないように実装する必要があり、 +実装コストも高くなることから実際に利用できる場面は限られています。 +(例: シューティングゲームのシューティングパートのみ無効化するなど) + +また、Unity 2019からIncremental GCが選択できるようになりました。Incremental GCでは、 +ガベージコレクションの処理がフレームを跨いで行われるようになり、大きなスパイクは以前より軽減可能になりました。 +しかしながら、1フレームあたりの処理時間を削減しつつ最大限のパワーを発揮しなければならないようなゲームの場合、 +突き詰めるとGC.Allocの発生を避けた実装が必要になります。 +具体的な例については、@{tuning_practice_script_csharp|practice_script_csharp_sample}で述べます。 + +====[column] いつから取り組むべきか + +ゲームはコード量も多くなるため、全機能実装完了してからパフォーマンスチューニングを実施すると +往々にしてGC.Allocを回避できないような設計/実装に遭遇してしまうことがあります。 +設計初期段階から、どこで発生するのか常に意識した上でコーディングしていくと、 +作り直しによるコストも軽減できるようになり、トータルでの開発効率は改善される傾向にあります。 + +理想的な実装の流れとしては、まずはスピード重視でプロトタイプを制作し手触りや遊びのコアとなる部分を検証し、 +その次の本制作フェーズに進む際に一度設計を見直し再構築します。この再構築するフェーズでGC.Allocの +撲滅に取り組むと健全でしょう。場合によってはコードの可読性を下げてでも高速化を図る必要も出てくるため、 +プロトタイプから取り組んでいては開発速度も低下してしまいます。 + +====[/column] + +==={basic_csharp_struct} 構造体(struct) + +C#では複合型の定義はクラスと構造体が存在します。大前提、クラスは参照型、構造体は値型となります。 +MSDNの「Choosing Between Class and Struct」@{choosing_class_struct}を +引用しつつ、それぞれの特性と選択すべき基準、使い方の注意事項について確認します。 + +//footnote[choosing_class_struct][@{https://docs.microsoft.com/en-us/dotnet/standard/design-guidelines/choosing-between-class-and-struct}] + +===={basic_csharp_memory_allocation} メモリの割り当て先の違い +参照型と値型の1つ目の違いは、メモリの割り当て先が異なる点です。少々正確性には欠けますが、次のように認識しておいて問題はありません。 +参照型はメモリ上のヒープ領域に割り当てられ、ガベージコレクションの対象となります。 +値型はメモリ上のスタック領域に割り当てられ、ガベージコレクションの対象にはなりません。 +値型の割り当てと割り当て解除は、参照型よりも一般的に低コストです。 + +ただし、参照型のフィールドに宣言されている値型やstatic変数はヒープ領域に割り当てられます。 +このため、構造体として定義した変数が必ずしもスタック領域に割り当てられるわけではない点に注意しましょう。 + +===={basic_csharp_array} 配列の扱い +値型の配列はインラインで割り当てられ、配列要素は値型の実体(インスタンス)がそのまま並びます。 +一方、参照型の配列では、配列要素は参照型の実体への参照(アドレス)が並びます。 +したがって、値型の配列の割り当てと割り当て解除は、参照型よりもはるかに低コストです。 +また、ほとんどの場合、値型の配列は参照の局所性(空間的局所性)が大幅に向上するため、 +CPUキャッシュメモリのヒット確率が高くなり、処理が高速化しやすくなるメリットがあります。 + +===={basic_csharp_value_copy} 値のコピー +参照型の代入(割り当て)では、参照(アドレス)がコピーされます。一方、値型の代入(割り当て)では、値全体がコピーされます。 +アドレスのサイズは32bit環境の場合で4バイト、64bit環境の場合で8バイトとなります。 +したがって、大きな参照型の割り当ては、アドレスサイズより大きな値型の割り当てよりも低コストです。 + +また、メソッドを用いたデータのやり取り(引数・戻り値)に関しても、参照型は参照(アドレス)が値渡しされるのに対し、 +値型はインスタンスそのものが値渡しされます。 + +//listnum[MyStruct_MyClass][][csharp]{ +private void HogeMethod(MyStruct myStruct, MyClass myClass){...} +//} + +たとえばこちらのメソッドでは、@{MyStruct}の値全体がコピーされます。つまり、@{MyStruct}のサイズが大きくなるとその分コピーコストも増大します。 +一方@{MyClass}の方では、@{myClass}の参照が値としてコピーされるだけになるため、@{MyClass}のサイズが増大しても +コピーコストはアドレスサイズ分のみであるため一定になります。コピーコストの増加は処理負荷に直結するため、扱うデータサイズに応じて適切に選択する必要があります。 + +===={basic_csharp_invariant} 不変性 +参照型のインスタンスに加えた変更は、同じインスタンスを参照している別の場所にも影響します。 +一方、値型のインスタンスは、値渡しされるときにコピーが生成されます。値型のインスタンスが変更された場合、 +当然、そのインスタンスのコピーには影響しません。コピーはプログラマによって明示的に作成されるのではなく、 +引数が渡されるとき、または戻り値が返されるときに暗黙的に作成されます。 +プログラマとしては値を変更したつもりが、実はコピーに対して値をセットしていただけで、 +目的の処理とは異なっていたという不具合を一度は経験していることでしょう。 +変更可能な値型は多くのプログラマに混乱を招くおそれがあるため、値型は不変であることが推奨されています。 + +====[column] 参照渡し +よくある誤用で「参照型は常に参照渡しになる」が挙げられますが、先述した通り参照(アドレス)のコピーが基本であり、 +参照渡しはref/in/outパラメーター修飾子を用いたときに行われます。 + +//listnum[ref_MyClass][][csharp]{ +private void HogeMethod(ref MyClass myClass){...} +//} + +参照型の値渡しでは参照(アドレス)がコピーされていたため、インスタンスの置き換えをしてもコピー元のインスタンスには影響しませんでしたが、 +参照渡しにすると元のインスタンスの置き換えも可能になります。 + +//listnum[ref_MyClass_replace][][csharp]{ +private void HogeMethod(ref MyClass myClass) +{ + // 引数で渡された元のインスタンスを書き換えてしまう + myClass = new MyClass(); +} +//} + +====[/column] + +===={basic_csharp_boxing} ボックス化 +ボックス化とは、値型から@{object}型、または値型からインターフェイス型へ変換するプロセスのことです。 +ボックスはヒープに割り当てられ、ガベージコレクションの対象になるオブジェクトです。 +そのため、ボックス化とボックス化解除が過剰になると、GC.Allocが発生します。これに対し、参照型がキャストされるとき、 +このようなボックス化は行われません。 + +//listnum[simple_boxing][値型からobject型にキャストするとボックス化][csharp]{ +int num = 0; +object obj = num; // ボックス化 +num = (int) obj; // ボックス化解除 +//} + +このようにわかりやすく無意味なボックス化を使うことはありませんが、 +メソッドで使われている場合はどうでしょうか。 + +//listnum[method_boxing][暗黙キャストでボックス化が行われる例][csharp]{ +private void HogeMethod(object data){ ... } + +// 中略 + +int num = 0; +HogeMethod(num); // 引数でボックス化 +//} + +このようなケースで、無意識のうちにボックス化してしまっているケースは存在します。 + +簡単な代入と比べて、ボックス化およびボックス化解除は負荷の大きいプロセスです。 +値型をボックス化するときは、新しいインスタンスを割り当てて構築する必要があります。 +また、ボックス化ほどではありませんが、ボックス化解除に必要なキャストも大きな負荷がかかります。 + +===={basic_csharp_class_or_struct} クラスと構造体を選ぶ基準について + + * 構造体を検討すべき条件: + ** 型のインスタンスが小さく、有効期間が短いことが多い場合 + ** 他のオブジェクトに埋め込まれることが多い場合 + + * 構造体を避ける条件: ただし、型が次のすべての特性を持つ場合を除く + ** プリミティブ型(@{int}、@{double}など)と同様に、論理的に単一の値を表すとき + ** インスタンスのサイズが16バイト未満である + ** 不変(イミュータブル)である + ** 頻繁にボックス化する必要がない + +上記の選択条件に当てはまらないものの、構造体と定義されている型も多数存在しています。 +Unityで頻繁に使用されている@{Vector4}や@{Quaternion}など、16バイト未満ではありませんが構造体で定義されています。 +これらを効率よく扱う方法を確認した上で、コピーコストが増大しているようでしたら回避する方法を含めて選択し、 +場合によっては自前で同等の機能を持った最適化版を作ることも検討してください。 + +=={basic_algorithm} アルゴリズムと計算量 + +ゲームプログラミングにはさまざまなアルゴリズムが利用されます。 +アルゴリズムは作り方次第で計算結果は同じでも、途中の計算過程が異なることでパフォーマンスは大きく変わることがあります。 +たとえば、C#に標準で用意されているアルゴリズムはどれくらい効率のよいものなのか、 +あなたが実装したアルゴリズムはどれくらい効率のよいものなのか、それぞれ評価する尺度が欲しくなります。 +これらを測る目安として、計算量という指標が用いられています。 + +=== 計算量について +計算量とはアルゴリズムの計算効率を測る尺度のことで、細かく分けると時間効率を測る時間計算量やメモリ効率を測る領域計算量などがあります。 +計算量オーダーは@{O}記法(ランダウの記号)で表されます。計算機科学や数学的な定義などはここでは本質ではないため、気になる方は他の書籍を参照してください。 +また、本稿では計算量と記載しているものは時間計算量として取り扱います。 + +一般的に使われるおもな計算量は@{O(1)}、@{O(n)}、@{O(n^2)}、@{O(n\log n)}のように表記されます。括弧内の@{n}はデータ数を示しています。 +ある処理がどれくらいデータ数に依存して処理回数が増えていくかをイメージするとわかりやすいでしょう。計算量の観点から性能を比較すると、 +@{O(1) < O(\log n) < O(n) < O(n\log n) < O(n^2) < O(n^3)}となります。 +@
{order_sample}にデータ数と計算ステップ数の比較と、@{basic_order_graph}に対数表示した比較グラフを示しました。 +@{O(1)}はデータ数によらないため比べるまでもなく明らかに性能が高いため除いてあります。たとえば、@{O(\log n)}はデータ数が1万サンプルあったとしても計算ステップ数は13、1,000万サンプルあったとしても +計算ステップ数が23回と極めて優秀であることがわかります。 + +//table[order_sample][おもな計算量におけるデータ数と計算ステップ数]{ +@{n} @{O(\log n)} @{O(n)} @{O(n\log n)} @{O(n^2)} @{O(n^3)} +10 3 10 33 100 1,000 +100 7 100 664 10,000 1,000,000 +1,000 10 1,000 9,966 1,000,000 1,000,000,000 +10,000 13 10,000 132,877 100,000,000 1,000,000,000,000 +//} + +//image[basic_order_graph][各計算量の対数表示による性能差比較] + +それぞれの計算量を示すため、いくつかコードサンプルを挙げて行きます。まず、@{O(1)}はデータ数に依存せず一定の計算量であることを示します。 + +//listnum[order_1][@{O(1)}のコード例][csharp]{ +private int GetValue(int[] array) +{ + // arrayには何らかの整数値が入っている配列とする + var value = array[0]; + return value; +} +//} + +このメソッドの存在意義はさておき、明らかにarrayのデータ数に依存することなく処理は一定回数(ここでは1回)で終わります。 + +次に@{O(n)}のコード例を見てみましょう。 + +//listnum[order_n][@{O(n)}のコード例][csharp]{ +private bool HasOne(int[] array, int n) +{ + // arrayはlength=nで、何らかの整数値が入っているとする + for (var i = 0; i < n; ++i) + { + var value = array[i]; + if (value == 1) + { + return true; + } + } +} +//} + +こちらは、整数値の入った配列に@{1}が存在していたら@{true}を返すだけの処理です。偶然@{array}の最初に@{1}が入っていたら最速で処理が終わる可能性もありますが、 +@{array}のなかにどこにも@{1}がない場合や、@{array}の最後にはじめて@{1}があった場合にはループは最後まで回るため@{n}回処理をすることになります。 +この最悪のケースのときを@{O(n)}として表し、データ数に応じて計算量が増えていくイメージが浮かぶことでしょう。 + +次に@{O(n^2)}のときの例を見てみましょう。 + +//listnum[order_n2][@{O(n^2)}のコード例][csharp]{ +private bool HasSameValue(int[] array1, int[] array2, int n) +{ + // array1, array2はlength=nで、何らかの整数値が入っているとする + for (var i = 0; i < n; ++i) + { + var value1 = array1[i]; + for (var j = 0; j < n; ++j) + { + var value2 = array2[j]; + if (value1 == value2) + { + return true; + } + } + } + + return false; +} +//} + +こちらは二重ループで2つの配列のどこかに同じ値が含まれていたら@{true}を返すだけのメソッドです。 +最悪のケースを考えるとすべて不一致のケースとなるため、その場合は@{n^2}回処理が走ることになります。 + +//info{ +余談ですが、計算量の考え方では最大次数の項のみで表現します。上記例の3つのメソッドを1回ずつ実行するメソッドを作ると、 +最大次数の@{O(n^2)}になります。(@{O(n^2+n+1)}にはなりません) + +また、計算量はあくまでデータ数が十分多いときの目安であり、実計測時間と必ずしも連動するものではないことに注意しておきましょう。 +@{O(n^5)}のような巨大な計算量に見えてもデータ数が少ない場合、問題にならないケースもあるため、 +計算量は参考にしつつも都度データ数を考慮して問題ない処理時間に収まるか測定することを推奨します。 + +//} + +=== 基本的なコレクションとデータ構造 +C#にはさまざまなデータ構造を持つコレクションクラスが用意されています。 +よく使うものを例に挙げつつ、おもなメソッドの計算量を踏まえてそれぞれどういうシチュエーションで採用するべきかを紹介します。 + +ここで紹介しているコレクションクラスにおけるメソッドの計算量についてはMSDNにすべて掲載されているため、 +最適なコレクションクラスを選定するときに確認できるとより安全でしょう。 + +==== List +もっともよく使われているであろう@{List}です。データ構造は配列です。 +データの並び順が重要な場合や、インデックスによるデータの取得や更新が多い場合に用いると効果的です。 +逆に要素の挿入や削除が多くなる場合には操作したインデックス以降のコピーが必要になり計算量が大きくなるため、 +@{List}の使用を避けたほうが無難でしょう。 + +また、Addでキャパシティを超えようとしたときには、配列の確保メモリの拡張が行われます。 +メモリの拡張時は現在のCapacityの2倍を確保することになるため、Addを@{O(1)}で使うためにも +拡張を発生させずに使用できるように適切な初期値を設定して使用しましょう。 + +//table[collection_list][List]{ +メソッド 計算量 +---------------------------- +Add @{O(1)}ただしキャパシティを超えたときは@{O(n)} +Insert @{O(n)} +IndexOf/Contains @{O(n)} +RemoveAt @{O(n)} +Sort @{O(n\log n)} +//} + +==== LinkedList +@{LinkedList}のデータ構造は連結リストです。連結リストは基本的なデータ構造で、各ノードが次のノードの参照を持っているようなイメージです。 +C#の@{LinkedList}は双方向の連結リストであるため、前後のノードへの参照をそれぞれ持っています。@{LinkedList}は、要素の追加や削除に強い特徴がありますが、 +配列内の特定の要素にアクセスするのは苦手です。頻繁に追加や削除を行う必要があるような一時的にデータを保持する処理を作りたいときなどに適しています。 + +//table[collection_linkedlist][LinkedList]{ +メソッド 計算量 +---------------------------- +AddFirst/AddLast @{O(1)} +AddAfter/AddBefore @{O(1)} +Remove/RemoveFirst/RemoveLast @{O(1)} +Contains @{O(n)} +//} + +==== Queue +@{Queue}は先入れ先出し法: FIFO(First in first out)を実現したコレクションクラスです。 +入力操作などを管理するときなど、いわゆる待ち行列を実装するときに用いられます。 +@{Queue}では循環配列が用いられています。@{Enqueue}で要素を末尾に追加して、@{Dequeue}で先頭の要素を取り出しつつ削除します。 +キャパシティを超えて追加する際には拡張が行われます。@{Peek}は削除をせずに先頭の要素を取り出す操作です。計算量を見ても明らかなように +@{Enqueue}と@{Dequeue}に留めて使うと +高いパフォーマンスを得られますが、探索などの操作には向かないでしょう。@{TrimExcess}はキャパシティを削減するメソッドですが、 +パフォーマンスチューニング観点から見ると、そもそもキャパシティが増減しないように使用できるとさらに@{Queue}の強みを活かせます。 + +//table[collection_queue][Queue]{ +メソッド 計算量 +---------------------------- +Enqueue @{O(1)}ただしキャパシティを超えたときは@{O(n)} +Dequeue @{O(1)} +Peek @{O(1)} +Contains @{O(n)} +TrimExcess @{O(n)} +//} + +==== Stack +@{Stack}は後入れ先出し法: LIFO(Last in first out)を実現したコレクションクラスです。 +@{Stack}は配列で実装されています。@{Push}で先頭に要素を追加し、@{Pop}で先頭の要素を取り出しつつ削除します。 +@{Peek}は削除をせずに先頭の要素を取り出す操作です。 +よく使われる場面としては画面遷移を実装するときに遷移時に進んだ先のシーン情報を@{Push}しておき、戻るボタンを押したときに@{Pop}するときなどが挙げられます。 +@{Stack}も@{Queue}と同様に@{Push}と@{Pop}のみを用いると高いパフォーマンスが得られます。要素の探索などは行わずに、キャパシティの増減にも注意しましょう。 + +//table[collection_stack][Stack]{ +メソッド 計算量 +---------------------------- +Push @{O(1)}ただしキャパシティを超えたときは@{O(n)} +Pop @{O(1)} +Peek @{O(1)} +Contains @{O(n)} +TrimExcess @{O(n)} +//} + +==== Dictionary +これまで紹介したコレクションは順序に意味を持つものでしたが、@{Dictionary}は索引性に特化したコレクションクラスです。 +データ構造はハッシュテーブル(連想配列の一種)で実装されています。キーに対応する値がある辞書(辞書の場合単語がキー、説明が値)のような構造です。 +@{Dictionary}はメモリを多く消費するデメリットはありますが、その分参照速度が@{O(1)}と高速です。 +列挙や探索を必要とせず、値を参照することに重きを置くようなケースでとても重宝します。また、キャパシティの事前設定を必ず行いましょう。 + +//table[collection_dictionary][Dictionary]{ +メソッド 計算量 +---------------------------- +Add @{O(1)}ただしキャパシティを超えたときは@{O(n)} +TryGetValue @{O(1)} +Remove @{O(1)} +ContainsKey @{O(1)} +ContainsValue @{O(n)} +//} + +=== 計算量を下げる工夫 +これまで紹介したコレクション以外にもさまざまなものが用意されています。 +もちろん、@{List}(配列)だけでも同様の処理を実装することは可能ですが、 +より適したコレクションクラスを選択することで計算量の最適化が可能になります。 +計算量を意識してメソッドを実装をしていくだけでも重い処理を避けることができるようになるでしょう。 +コード最適化における1つの切り口として、自分が作ったメソッドの計算量を確認して、 +より少ない計算量にできないか検討してみてはいかがでしょうか。 + +====[column] 工夫の手段: メモ化 +とある複雑な計算をしなければならないような、とても高い計算量のメソッド(@{ComplexMethod})があるとします。 +しかしどうにも計算量を減らすことができないときもあるでしょう。 +こうしたときに用いられる手段としてメモ化と呼ばれる手法があります。 + +ここでの@{ComplexMethod}は引数を与えると対応した結果が一意に返るものとします。 +まず、渡された引数が初回のときには複雑な処理を通します。計算後、引数と計算結果を@{Dictionary}に入れてキャッシュしておきます。 +2回目以降はまずはキャッシュされてないか調べ、すでにキャッシュされていたらその結果だけを返して終了します。 +こうすることで、初回がどれだけ高い計算量でも2回目以降は@{O(1)}に抑えることが可能です。 +もし事前に渡されうる引数がある程度決まっているようでしたら、ゲームの前に計算を済ませてキャッシュしておくことで、 +事実上@{O(1)}の計算量で処理することが可能になります。 + +====[/column] diff --git a/articles/text/contributors.re b/articles/text/contributors.re new file mode 100644 index 0000000..91b8c1f --- /dev/null +++ b/articles/text/contributors.re @@ -0,0 +1,85 @@ +={contributors} 著者紹介 +本書に携わった著者を紹介します。 +なお、各著者のプロフィールや担当箇所は執筆時点のものになります。 + +==== 飯田 卓也(Takuya Iida) +株式会社グレンジ所属、SGEコア技術本部兼務 / エンジニアリングマネージャー + +@{tuning_start}、@{profile_tool}などの執筆を担当。現在は子会社を跨いで最適化に関わっています。 +業務上さまざまなことをしていますが、開発速度や品質の向上を目指して日々励んでいます。 + +==== 矢野 春樹(Haruki Yano)/ Twitter:@harumak_11 / GitHub:Haruma-K +株式会社サイバーエージェント SGEコア技術本部 / クライアントサイドエンジニア + +@{basic}の@{basic|basic_graphics}、@{basic|basic_asset_data}などの執筆を担当。 +開発効率向上のための共通基盤開発を業務の主軸としています。 +業務でも個人でもUnity用のいろんなOSSを開発して公開しています。 +UnityブログLIGHT11も運用中。 + +==== 石黒 祐輔(Yusuke Ishiguro) +株式会社サイバーエージェント SGEコア技術本部 + +@{basic}の一部と、@{tuning_practice_assetbundle}の執筆を担当。 +Amebaゲーム(現クオリアーツ)の基盤開発チームにUnityエンジニアとして配属され、 +リアルタイム基盤、チャット基盤、AssetBundle管理基盤「Octo」、認証・課金基盤などさまざまな基盤の開発に従事。 +現在はSGEコア技術本部に異動し、基盤全般の開発をリードしつつ、ゲーム事業部全体の開発効率と品質の最適化に注力。 + +==== 袴田 大貴(Daiki Hakamata) +株式会社サイバーエージェント SGEコア技術本部 + +@{tuning_practice_script_unity}の執筆を担当。 +株式会社グレンジ、株式会社ジークレストにてゲーム開発・運用に従事。 +現在はSGEコア技術本部に所属し基盤を開発中。 + +==== 中村 光寿(NAKAMURO.)/ Twitter: @megalo_23 +株式会社アプリボット所属 / ゲームクリエイター + +@{basic|basic_csharp}、@{tuning_practice_script_csharp}の前半の執筆を担当。 +本書を執筆することで開発終盤にヘルプで呼ばれる機会を減らし、新しいゲームを開発する時間を確保することを企んでいる。 +ゲーム開発では最適化やディレクション、譜面制作から声の出演までと活動は幅広い。個人ではFamulite Lab.でアプリ運営中。 + +==== 大庭 俊介(Shunsuke Ohba)/ Twitter:@ohbashunsuke +株式会社サムザップ所属 / エンジニアリングマネージャー + +@{tuning_practice_asset}の執筆を担当。 +元デザイナーのエンジニア。Flashを使ったインタラクティブなWebサイトの制作後、株式会社サイバーエージェントに中途入社。アメーバピグの開発後、Unityエンジニアに転向。麻雀、ピンボール、リアルタイムバトルなど数多くのゲームの立ち上げにエンジニアリーダーとして参画。 +個人ではTwitterやブログ「渋谷ほととぎす通信(https://shibuya24.info)」で情報発信しています。 + +==== 石井 岳(Gaku Ishii) +株式会社サムザップ所属 / サーバー兼クライアントサイドエンジニア + +@{tuning_practice_player_settings}、@{tuning_practice_third_party}の執筆を担当。 +株式会社サムザップへ配属後、Unityエンジニアとして新規ゲームアプリの開発に従事する。複数のアプリのリリースに携わった後、サーバーサイドエンジニアへ転向。 +現在サムザップではサーバーサイドエンジニア、SGEコア技術本部ではUnityエンジニアとしてサーバー/クライアント両面で活動中。 + + +==== 斉藤 俊介(Shunsuke Saito)/ Twitter: @shun_shun_mummy +株式会社カラフルパレット所属 / クライアントサイドエンジニア + +@{tuning_practice_script_csharp}の一部の執筆を担当。 +株式会社カラフルパレットに配属後、担当プロジェクトでクライアントサイドのリアルタイム通信の設計・実装や、UIシステム周りの開発に従事。 +また、既存機能のチューニングやテンプレートコードの自動生成ツール開発なども行う。 + +//embed[latex]{ +\clearpage +//} + +==== 田村 和範(Kazunori Tamura) +株式会社クオリアーツ所属 + +@{tuning_practice_ui}の執筆を担当。 +株式会社クオリアーツにて、Unityエンジニアとしてゲーム開発や社内基盤開発に従事。 +社内基盤は主にUIに関わるものを開発。 +AIによるゲーム開発の効率化にも興味を持ち、ゲーム事業部内でのAIの活用を目指して奮闘中。 + +==== 山口 智也(Tomoya Yamaguchi)/ Twitter: @togucchi +株式会社カラフルパレット所属 / クライアントサイドエンジニア + +@{tuning_practice_graphics}の執筆を担当。 +株式会社カラフルパレットにて3D描画やライブ関連のシステムの開発に従事。 +現在は3D関連の新規技術の検証などを行っている。 + +==== 向井 祐一郎(Yuichiro Mukai)/ Twitter: @yucchiy_ +株式会社アプリボット所属 / クライアントサイドエンジニア + +@{tuning_practice_physics}と@{tuning_practice_script_csharp}の一部の執筆を担当。 diff --git a/articles/text/end_of_summary.re b/articles/text/end_of_summary.re new file mode 100644 index 0000000..e1ab76e --- /dev/null +++ b/articles/text/end_of_summary.re @@ -0,0 +1,12 @@ +={end_of_summary} さいごに +本書はここで終わりです。 +本書を通して「パフォーマンスチューニングには自信がない」という方が「なんとなく分かった、やってみたい」と思えるようになったなら幸いです。 +プロジェクト内で実践する人が増えると、問題への対処が格段に早くなり、プロジェクトの安定感は増していくことでしょう。 + +また本書で紹介した内容では解決できない複雑な事象に出会うことがあるかもしれません。 +しかし、そんなときでもやることは変わらないでしょう。 +それはプロファイルを行い、原因を分析し、何かしらの手を打っていくことです。 + +ここから先は実践を通してあなた自身の知識や経験、そして発想力を存分に活かしてください。 +そうしてパフォーマンスチューニングを楽しんでもらえると幸いです。 +最後までお読み頂きありがとうございました。 diff --git a/articles/text/preface.re b/articles/text/preface.re new file mode 100644 index 0000000..6140d81 --- /dev/null +++ b/articles/text/preface.re @@ -0,0 +1,52 @@ +={preface} はじめに +本書はUnityアプリケーションのパフォーマンスチューニングで +困った時にリファレンスとして活用されることを目指して作成しました。 + +パフォーマンスチューニングは過去のノウハウが活用できるため、属人化が進みやすい分野であると感じています。 +未経験の方からするとなんとなく難しそうという印象を持ってしまうかもしれません。 +それは性能低下の原因が多岐に渡ることが理由の1つでしょう。 + +しかしパフォーマンスチューニングのワークフローは型にはめることができます。 +そのフローに従うことで原因の特定は容易になり、その事象にあった解決策を探すだけになります。 +解決策を模索するときには知識や経験が手助けになるでしょう。 +そこで本書は「ワークフロー」や「経験からくる知識」を主に学習できるように設計しました。 + +社内向けドキュメントとして制作予定でしたが、せっかくならたくさんの方に見て頂き、よりブラッシュアップできればと考えています。 +手に取って頂いた方にとって、少しでも開発の手助けになれば幸いです。 + +=={preface_book} 本書について +本書はスマートフォン向けアプリを前提とした内容になります。 +一部の解説は他プラットフォームに当てはまらないものもあるかもしれませんがご留意ください。 +また本書内で利用するUnityのバージョンは、とくに断りがない場合Unity 2020.3.24f1とします。 + +=={preface_usage} 本書の構成 +本書は大きく分けて3部構成となっています。 +@{basic}まではチューニングを行うまでの基礎知識、@{profile_tool}では各種計測ツールの使い方、そして@{tuning_practice_asset}以降はさまざまなチューニングプラクティスを取り上げます。 +それぞれの章は独立しているため、自身のレベルに合わせて必要な箇所のみ読み進めていくのもよいでしょう。 +次に各章の概要について説明します。 + +@{tuning_start}では、パフォーマンスチューニングのワークフローに関して説明します。 +まずは取りかかるまでの事前準備について解説し、次に原因を切り分けて調査を進めていく方法について解説します。 +この章を読むことでパフォーマンスチューニングに着手できる状態を目指します。 + +@{basic}では、ハードウェア、描画の流れ、Unityの仕組みなど、 +パフォーマンスチューニングを行う上で知っておくとよい基礎知識について解説します。 +@{basic}以降を読み進めていく中で、知識が不足していると感じたときに読み返すのもよいでしょう。 + +@{profile_tool}では、原因調査に使用するさまざまなツールの使い方を学ぶ事ができます。 +計測ツールをはじめて利用する際にリファレンスとして活用することを推奨します。 + +@{tuning_practice_asset}以降の「Tuning Practice」ではアセットからスクリプトまで、さまざまなプラクティスを詰め込んだ内容になります。 +ここに記載されている内容は現場ですぐに使えるものが多いので、ぜひ一読してみてください。 + +==={preface_github} GitHub +本書のリポジトリ@{unity_performance_tuning_bible}を公開します。随時、加筆修正を行う予定です。 +またPRやIssueを用いて修正点の指摘や追記の提案が可能です。 +よろしければご利用ください。 + +//footnote[unity_performance_tuning_bible][@{https://github.com/CyberAgentGameEntertainment/UnityPerformanceTuningBible/}] + +=={preface_disclaimer} 免責事項 +本書に記載された内容は情報の提供のみを目的としています。 +したがって、本書を用いた開発、製作、運用は必ずご自身の責任と判断によって行ってください。 +これらの情報による開発、製作、運用の結果について当社はいかなる責任も負いません。 diff --git a/articles/text/profile_tool.re b/articles/text/profile_tool.re new file mode 100644 index 0000000..c4028e0 --- /dev/null +++ b/articles/text/profile_tool.re @@ -0,0 +1,1250 @@ +={tool} プロファイリングツール +プロファイリングツールはデータを収集・解析し、ボトルネックの特定やパフォーマンスの指標を決める際に使用します。 +これらのツールはUnityエンジンが提供しているものだけでもいくつかあります。 +他にもXcodeやAndroid Studioと言ったネイティブ準拠のツールや、GPUに特化したRenderDocなど、さまざまなツールが存在します。 +そのためそれぞれのツールの特徴を理解し、適切に取捨選択することが重要になるでしょう。 +この章ではそれぞれのツールの紹介とプロファイル方法について取り上げ、適切に使用できる状態を目指します。 + +==={before_profiling} 計測時の注意点 +Unityはエディター上でアプリケーションを実行できるため「実機」と「エディター」のどちらでも計測が可能です。 +計測を行うにあたって、それぞれの環境での特徴を押さえておく必要があります。 + +エディターで計測する場合、トライ&エラーを素早くできることが最大の強みでしょう。 +しかしエディター自身の処理負荷や、エディターが使用しているメモリ領域も計測されるため、計測結果にノイズが多くなります。 +また実機とはスペックがまったく違うので、ボトルネックがわかりにくく結果が違うこともあるでしょう。 + +そのためプロファイリングは基本的には@{実機での計測を推奨}します。 +ただし「どちらの環境でも発生する」場合に限り、作業コストが低いエディターだけで作業を完結させるのが効率的です。 +大体はどちらの環境でも再現しますが、まれにどちらかの環境でしか再現しないことがあります。 +そのため、まずは実機で現象を確認します。次にエディターでも再現を確認した上で、エディター上で修正するというフローがよいでしょう。 +もちろん最後に実機での修正確認は必ず行いましょう。 + +=={tool_unity_profiler} Unity Profiler +Unity ProfilerはUnityエディターに組み込まれているプロファイリングツールです。 +このツールは1フレームごとに情報を収集できます。 +計測できる項目は多岐に渡り、それぞれの項目をプロファイラーモジュールと呼び、Unity 2020のバージョンでは14項目も存在します。 +このモジュールはいまなお更新されており、Unity 2021.2では新たにAssetに関するモジュールや、File I/Oに関するモジュールも追加されました。 +このようにUnity Profilerは、さまざまなモジュールがあるため、ざっくりとパフォーマンスの外観を掴むのに最適なツールと言えるでしょう。 +モジュール一覧は@{profiler_module_list}のようになります。 + +//image[profiler_module_list][プロファイラーモジュール一覧] + +これらのモジュールはプロファイラー上に表示するかどうかの設定が可能です。 +ただし表示していないモジュールは計測もされていません。逆に言うとすべてを表示している場合、それだけエディターに負荷がかかります。 +//image[profiler_modules][Profiler Modulesの表示/非表示機能] + +また、プロファイラーツール全体で共通する便利な機能を紹介します。 +//image[profiler_often_use_command][Profilerの機能説明] + +@{profiler_often_use_command}において「①」は各モジュールが計測している項目が列挙されています。 +この項目をクリックすることで右側のタイムラインへの表示・非表示の切り替えができます。 +必要な項目だけを表示することでビューが見やすくなるでしょう。 +またこの項目はドラッグで並び替えることもでき、右側のグラフはその並び順で表示されます。 +「②」は計測したデータの保存、読み込みの機能です。必要であれば計測結果を保存しておくとよいでしょう。 +保存されるデータはプロファイラー上に表示しているデータのみです。 + +本書籍では@{profiler_module_list}のうち、使用頻度が高いCPU UsageとMemoryモジュールについて解説を行います。 + +==={how_to_use_unity_profiler} 計測方法 +ここではUnity Profilerで実機を用いた計測方法について取り上げます。 +ビルド前に行う作業とアプリケーション起動後に行う作業の2つに分けて解説します。 +なお、エディターでの計測方法は実行中に計測ボタンを押下するだけなので詳細は割愛します。 + +===={how_to_use_unity_profiler_for_build} ビルド前に行う作業 +ビルド前に行う作業は@{Development Build}を有効にすることです。 +この設定が有効化されることでプロファイラーと接続ができるようになります。 + +また、より詳細に計測を行う@{Deep Profile}というオプションもあります。 +このオプションを有効にすると、すべての関数コールの処理時間が記録されるため、ボトルネックとなる関数の特定が容易にできます。 +欠点としては計測自体に非常に大きなオーバーヘッドを要するため、動作が重くなり、メモリも大量に消費します。 +そのため処理にとても時間がかかっているように見えても、通常のプロファイルではそれほどでもないこともあるので注意してください。 +基本的には通常のプロファイルでは情報が足りない場合にのみ使用します。 +//info{ +Deep Profileは大規模プロジェクトなどでメモリを多く使用している場合、メモリ不足で計測ができないこともあるでしょう。 +その場合は@{cpu_module_unity_profiler}節の@{cpu_module_sampler}を参考に、独自に計測処理を追加するしかありません。 +//} + +これらの設定方法はスクリプトから明示的に指定する方法と、GUIから行う方法があります。 +まずスクリプトから設定する方法を紹介します。 + +//listnum[setting_development_build_by_script][スクリプトからのDevelopment Buildを設定する方法][csharp]{ +BuildPlayerOptions buildPlayerOptions = new BuildPlayerOptions(); +/* シーンやビルドターゲットの設定は省略 */ + +buildPlayerOptions.options |= BuildOptions.Development; +// Deep Profileモードを有効にしたい場合のみ追加 +buildPlayerOptions.options |= BuildOptions.EnableDeepProfilingSupport; + +BuildReport report = BuildPipeline.BuildPlayer(buildPlayerOptions); +//} +@{setting_development_build_by_script}で重要な点は@{BuildOptions.Development}を指定することです。 + +次にGUIから設定する場合は、Build Settingsから@{development_build_setting}のようにDevelopment Buildにチェックしてビルドをします。 +//image[development_build_setting][Build Settings] + +===={how_to_use_unity_profiler_for_connect}アプリケーション起動後に行う作業 +アプリケーション起動後にUnity Profilerと接続する方法は「リモート接続」と「有線(USB)接続」の2種類があります。 +リモート接続は有線接続に比べて環境の制約が増え、プロファイルが思ったようにできないこともあります。 +たとえば同じWifiネットワークへの接続が必須だったり、Androidのみモバイル通信を無効にする必要があったり、他にもポート解放が必要だったりします。 +そのため、本節では手順がシンプルで確実にプロファイルができる有線接続について取り上げます。 +もしリモート接続をしたい場合は、公式ドキュメントを見ながら試してみてください。 + +はじめにiOSの場合、プロファイラーへの接続は次のような手順で行います。 + + 1. Build SettingsからTarget PlatformをiOSに変更しておく + 2. デバイスをPCに接続し、Development Buildのアプリケーションを起動する + 3. Unity Profilerから接続先の端末を選択する (@{connect_profiler_by_iphone}) + 4. Recordを開始する + +//image[connect_profiler_by_iphone][接続先端末の選択] + +//info{ +計測を行うUnityエディターは、ビルドしたプロジェクトでなくても構いません。 +計測用に新規プロジェクトを作成すると動作も軽量なのでオススメです。 +//} + +次にAndroidの場合は、iOSに比べ少し手順が増えます。 + + 1. Build SettingsからTarget PlatformをAndroidに変更しておく + 2. デバイスをPCに接続し、Development Buildのアプリケーションを起動する + 3. @{adb forward}コマンドを入力する。(コマンドの詳細は後述) + 4. Unity Profilerから接続先の端末を選択する + 5. Recordを開始する + +@{adb forward}コマンドには、アプリケーションのPackage Nameが必要になります。 +たとえばPackage Nameが「jp.co.sample.app」だった場合、以下のように入力します。 +//listnum[adb_tunnel_command][adb forwardコマンド]{ +adb forward tcp:34999 localabstract:Unity-jp.co.sample.app +//} + +adbが認識されていない場合は、adbのパス設定を行ってください。 +設定方法に関してはWeb上に解説してある情報がいくつもあるため割愛します。 + +簡易トラブルシューティングとして、接続できない場合は以下を確認してください。 + + * 両デバイス共通 + ** 実行したアプリケーションの右下にDevelopment Buildという表記があるか + * Androidの場合 + ** 端末のUSBデバッグを有効化しているか + ** @{adb forward}コマンドの入力したパッケージ名が正しいか + ** @{adb devices}コマンドを入力した際に、デバイスが正常に認識されているか + + 補足となりますがBuild And Runで直接アプリケーションを実行した場合、上述の@{adb forward}コマンドが内部的に行われます。 + そのため計測時にコマンド入力は必要ありません。 + +====[column]{autoconnect_profiler} Autoconnect Profiler + +ビルド設定にはAutoconnect Profilerというオプションがあります。 +このオプションはアプリ起動時にエディターのプロファイラーに自動接続するための機能です。 +そのためプロファイルに必須の設定ではありません。リモートプロファイル時も同様です。 +WebGLのみ、このオプションがないとプロファイルできませんが、モバイルに関してはそれほど重宝するオプションではないでしょう。 + +もう少し踏み込んだ話をすると、このオプションが有効な場合、ビルド時にエディターのIPアドレスがバイナリへ書き込まれ、起動時にそのアドレスへ接続を試みるようになります。 +専用のビルドマシンなどでビルドしている場合、そのマシンでプロファイルをしない限りは不要です。 +むしろアプリケーション起動時に自動接続がタイムアウトする(8秒ほどの)待ち時間が増えるだけになります。 + +スクリプトからは@{BuildOptions.ConnectWithProfiler}というオプション名になっており、必須のように勘違いしやすいので気をつけましょう。 + +====[/column] + +==={cpu_module_unity_profiler} CPU Usage +CPU Usageは@{profiler_cpu_usage}のように表示されます。 +//image[profiler_cpu_usage][CPU Usageモジュール (Timeline表示)] + +このモジュールの確認方法は大きく分けて次の2つになります。 + + * Hierarchy (Raw Hierarchy) + * Timeline + + まずはHierarchyビューに関して、表示内容と使い方について解説します。 + +===={cpu_module_hierarchy} 1. Hierarchyビュー +Hierarchyビューは@{profiler_hierarchy_view}のような表示になります。 +//image[profiler_hierarchy_view][Hierarchyビュー] + +このビューの特徴としては、リスト形式で計測結果が並んでおり、ヘッダーの項目でソートが可能なことです。 +調査を行う際はリストの中から気になる項目を開いていくことで、ボトルネックが判別できます。 +ただし、表示されている情報は「選択しているスレッド」で費やされた時間の表示です。 +たとえば、Job Systemやマルチスレッドレンダリングを使用している場合、別スレッドでの処理時間は含まれません。 +確認したい場合は@{profiler_hieracrhy_thread}のようにスレッドを選択することで可能です。 +//image[profiler_hieracrhy_thread][スレッド選択] + +//embed[latex]{ +\clearpage +//} + +次にヘッダーの項目について解説します。 +//table[hierarchy_header][Hierarchyヘッダー情報]{ +ヘッダー名 説明 +-------------------- +Overview サンプル名。 +Total この関数の処理にかかった合計時間。(%表示) +Self この関数自体の処理時間。サブ関数の処理時間は含まれません。(%表示) +Calls 1フレーム内で呼ばれた回数。 +GC Alloc この関数で割り当てたスクリプトのヒープメモリ。 +Time ms Totalをmsで表示したもの。 +Self ms Selfをmsで表示したもの。 +//} + +Callsは複数回の関数呼び出しを1つの項目としてまとめるのでビューとして見やすくなります。 +しかし、すべてが等しい処理時間なのか、うち1つだけ処理時間が長いのかはわかりません。 +このような場合に@{Raw Hierarchyビュー}を利用します。 +Raw Hierarchyビューは、必ずCallsが1に固定されるという点でHierarchyビューと違います。 +@{profile_raw_hierarchy}ではRaw Hierarchyビューにしているため、同じ関数呼び出しが複数表示されています。 +//image[profile_raw_hierarchy][Raw Hierarchyビュー] + +今までの内容をまとめるとHierarchyビューは次のような用途で使用します。 + + * 処理落ちしているボトルネックの把握、最適化 (Time ms、Self ms) + * GCアロケーションの把握、最適化 (GC Allocation) + +これらの作業を行う際は、目的の項目ごとに降順ソートを行ってから確認することを推奨します。 + +//info{ +項目を開いていく際、深い階層になっていることがよくあります。 +この時、Macの場合はOptionキー(Windowsの場合はAltキー)を押下しながら開くことで、すべての階層が開けます。 +逆にキーを押しながら項目を閉じると、その階層以下が閉じた状態になります。 +//} + +===={cpu_module_timeline} 2. Timelineビュー +もう1つの確認方法となるTimelineビューは次のような表示になります。 +//image[profiler_timeline][Timelineビュー] + +Timelineビューでは、Hierarchyビューの項目がボックスとして可視化されているため、一目で全体を見たときに負荷のかかっている箇所が直感的にわかります。 +そしてマウスによる操作が可能なため、階層が深い場合もドラッグするだけで全容が把握できます。 +さらにTimelineではわざわざスレッドを切り替える必要がなく、すべて表示されています。 +そのため各スレッドでいつどのような処理が行われているかも簡単に把握できます。 +このような特徴から、主に次のような用途で使用します。 + + * 処理負荷を全体俯瞰してみたい + * 各スレッドの処理負荷を把握、チューニングしたい + +Timelineが向いていないのは、重い処理順を把握するためのソート操作や、アロケーション総量を確認する場合などです。 +そのためアロケーションのチューニングに関してはHierarchyビューの方が向いているでしょう。 + +===={cpu_module_sampler} 補足:Samplerについて +関数単位で処理時間を計測する場合、2通りの方法があります。 +1つは先に紹介したDeep Profileモードです。もう1つはスクリプトに直接埋め込む方法です。 + +直接埋め込む場合は次のように記載します。 +//listnum[profile_begin_samle][Begin/EndSampleを用いた方法]{ +using UnityEngine.Profiling; +/* ...省略... */ +private void TestMethd() +{ + for (int i = 0; i < 10000; i++) + { + Debug.Log("Test"); + } +} + +private void OnClickedButton() +{ + Profiler.BeginSample("Test Method") + TestMethod(); + Profiler.EndSample() +} +//} + +埋め込んだサンプルはHierarchyビュー、Timelineビューのどちらにも表示されます。 +//image[sampler_display_example][Samplerの表示] + +もう1つ特筆すべき特徴があります。それはプロファイリングのコードはDevelopment Buildでない場合、呼び出し側が無効化されるためオーバーヘッドはゼロになることです。 +今後処理負荷が増えそうな箇所には事前に仕込んでおくのも手かもしれません。 + +BeginSampleメソッドはstatic関数なので気軽に使えますが、似たような機能を持ったCustomSamplerというのも存在します。 +こちらはUnity 2017以降に追加され、BeginSampleより計測のオーバーヘッドが少ないため、より正確な時間が計測できるという特徴があります。 + +//embed[latex]{ +\clearpage +//} + +//listnum[profile_custom_sampler][CustomSamplerを用いた方法]{ +using UnityEngine.Profiling; +/* ...省略... */ +private CustomSampler _samplerTest = CustomSampler.Create("Test"); + +private void TestMethod() +{ + for (int i = 0; i < 10000; i++) + { + Debug.Log("Test"); + } +} + +private void OnClickedButton() +{ + _samplerTest.Begin(); + TestMethod(); + _samplerTest.End(); +} +//} + +違いとしては事前にインスタンスを生成しておく必要があるところです。 +CustomSamplerは計測後、スクリプト内で計測時間を取得できるのも特徴です。 +より正確性が必要な場合や、処理時間に応じて警告などを出したい場合は、CustomSamplerを使用するのがよいでしょう。 + +==={memory_module_unity_prfoiler} Memory +メモリモジュールは@{profiler_memory}のように表示されます。 +//image[profiler_memory][Memoryモジュール] + +このモジュールの確認方法は次の2つになります。 + + * Simpleビュー + * Detailedビュー + +まずはSimpleビューに関して、表示内容と使い方について解説します。 + +===={memory_module_simple_view} 1. Simpleビュー +Simpleビューは@{profiler_memory_simple}のような表示になります。 +//image[profiler_memory_simple][Simpleビュー] + +ビューに記載されている項目について説明します。 + + : Total Used Memory + Unityが割当済み(使用中)のメモリ合計量。 + : Total Reserved Memory + Unityが現在確保しているメモリの合計量。 + OS側にあらかじめ一定量の連続メモリ領域をプールとして確保しておき、必要になったタイミングで割り当てていきます。 + プール領域が足りなくなると再度OS側に要求し拡張します。 + : System Used Memory + アプリケーションで使用しているメモリの合計量。 + この項目では、Total Reservedで計測されていない項目(プラグインなど)も計測されます。 + ただし、これでもすべてのメモリ割り当ては追跡できません。 + 正確に把握する場合は、Xcodeなどネイティブ準拠のプロファイリングツールを使う必要があります。 + +@{profiler_memory_simple}のTotal Used Memoryの右側に記載されている項目の意味は次の通りです。 + +//table[simple_view_total_detail][Simple View用語説明]{ +用語名 説明 +-------------------- +GC ヒープ領域で使用しているメモリ量。GC Allocなどが要因で増加します。 +Gfx Texture、Shader、Meshなどで確保しているメモリ量。 +Audio オーディオ再生のために使用しているメモリ量。 +Video Video再生のために使用しているメモリ量。 +Profiler プロファイリングを行うために使用しているメモリ量。 +//} +用語名に関する補足ですが、Unity 2019.2以降から「Mono」は「GC」に、「FMOD」は「Audio」に表記が変更されました。 + +@{profiler_memory_simple}には他にも、次に示すアセットの使用個数とメモリ確保量も記載されています。 + + * Texture + * Mesh + * Material + * Animation Clip + * Audio Clip + +また、次に示すようなオブジェクト数やGC Allocationに関する情報もあります。 + + : Asset Count + ロード済みのアセット総数。 + : Game Object Count + シーンに存在するゲームオブジェクト数。 + : Scene Object Count + シーンに存在するコンポーネントやゲームオブジェクトなどの総数。 + : Object Count + アプリケーションで生成した、ロードしたすべてのオブジェクト総数。 + この値が増えている場合、何かのオブジェクトがリークしている可能性が高いです。 + : GC Allocation in Frame + 1フレーム内でAllocationの発生した回数と総量。 + +最後に、これらの情報からSimpleビューの利用ケースについてまとめます。 + + * ヒープ領域やReservedの拡張タイミングの把握・監視 + * 各種アセットやオブジェクトがリークしていないかの確認 + * GC Allocationの監視 + +//info{ +Unity 2021以降のSimpleビューはUIが大きく改善され、表示項目が見やすくなりました。 +内容そのものには大きな変更はないため、ここで紹介した知識はそのまま使えます。 +ただし名称が一部変更されているので注意してください。 +たとえばGCがManaged Heapという名称に変更されました。 +//image[profile_memory_2021][2021以降のシンプルビュー] +//} + +===={memory_module_detailed_view}2. Detailedビュー +Dtailedビューは@{profiler_memory_detailed}のような表示になります。 +//image[profiler_memory_detailed][Detailedビュー] +この表示結果は「Take Sample」ボタンを押下することで、その時点でのメモリスナップショットを取得できます。 +Simpleビューと違いリアルタイム更新ではないため、表示を更新したい場合は再度Take Sampleを行う必要があります。 + +@{profiler_memory_detailed}の右側に「Referenced By」という項目があります。 +これは現在選択中のオブジェクトを参照しているオブジェクトが表示されます。 +リークしているアセットが存在する場合、オブジェクトの参照元の情報が解決の糸口になるでしょう。 +この表示は「Gather object references」を有効にしている場合のみ表示されます。 +この機能を有効にすると、Take Sample時の処理時間が伸びますが、基本的には有効にしておくとよいでしょう。 +//info{ +Referenced Byには@{ManagedStaticReferences()}という表記を見かけることがあります。 +これは何かしらのstaticなオブジェクトから参照されているということです。 +プロジェクトに精通している方なら、この情報だけである程度目星がつくかもしれません。 +そうでない場合は@{tool_heap_explorer}を使用することを推奨します。 +//} + +Detailedビューのヘッダー項目は、見たとおりの意味なのでここでは解説を行いません。 +操作方法は@{cpu_module_unity_profiler}の@{cpu_module_hierarchy}と同じで、 +ヘッダーごとのソート機能があり、項目が階層表示になっています。 +ここではName項目に表示されるトップノードについて解説を行います。 + +//table[memory_detailed_top_node][Detailedのトップノード]{ +名前 説明 +-------------------- +Assets シーンに含まれていないロードしたアセット。 +Not Saved コードによって実行時に生成されたアセット。@
{}たとえば、new Materiala()など、コードから生成したオブジェクトのこと。 +Scene Memory ロードしたシーンに含まれているアセット。 +Others 上記以外のオブジェクトで、Unityがさまざまなシステムで使用するものへの割り当て。 +//} + +トップノードの中でもOthersの中に記載されている項目は普段馴染みがないものばかりでしょう。 +その中でも知っておくとよいものを次に取り上げます。 + +//embed[latex]{ +\clearpage +//} + + : System.ExecutableAndDlls + バイナリやDLLなどに使用された割り当て量を示します。 + プラットフォームや端末によって取得できない場合があり、その場合は0Bとして扱われます。 + 共通フレームワークを使用している他のアプリケーションと共有している場合もあり、 + プロジェクトに対するメモリ負荷は記載されている数値ほど大きくはありません。 + この項目の削減に躍起になるより、Assetの改善をするほうがよいでしょう。 + 削減するには、DLLや不要なScriptを削減するのが効果的です。もっとも簡単な方法としては、Stripping Levelの変更です。 + ただし、実行時に型やメソッドが欠落するリスクがあるのでデバッグは入念に行いましょう。 + : SerializedFile + AssetBundle内のオブジェクトのテーブルや、型情報となるType Treeなどのメタ情報を示します。 + これはAssetBundle.Unload(true or false)で解放することが可能です。 + もっとも効率が良いのはアセットロード後、このメタ情報のみ解放するUnload(false)を行うことですが、 + 解放タイミングやリソースの参照管理をシビアに行わないと、リソースを二重ロードしたり、容易にメモリリークを引き起こすので注意してください。 + : PersistentManager.Remapper + メモリ上とディスク上のオブジェクトの関連性を管理しています。Remapperはメモリプールを利用し、足りなくなると倍々に拡張していき減少することはありません。 + 拡張されすぎないように気をつけましょう。具体的にはAssetBundleを大量にロードすると、マッピング領域が足りず拡張されます。 + そのため同時にロードされるファイル数を削減するために、不要なAssetBundleをアンロードするのがよいでしょう。 + また、1つのAssetBundleにその場で不要なアセットが大量に含まれている場合は分割するのがよいでしょう。 + +最後に、ここまで紹介してきた内容からDetailedビューの利用するケースをまとめます。 + + * 特定タイミングでのメモリの詳細な把握、チューニング + ** 不要なアセットがないか、想定外なものがないか + * メモリリークの調査 + +=={tool_profile_analyzer} Profile Analyzer +Profile AnalyzerはProfilerのCPU Usageで取得したデータをより細かく分析するためのツールです。 +Unity Profilerでは1フレーム単位のデータしか見られなかったのに対して、Profile Analyzerは指定したフレームの区間をもとに平均値や中央値、最小、最大値を取得できます。 +フレームごとにばらつきのあるデータを適切に扱えるため、最適化を行った際に改善効果をより明確に示せるようになるでしょう。 +またCPU Usageでは出来なかった計測データ同士の比較機能もあるため、最適化の結果を比較・可視化するのに非常に便利なツールです。 + +//image[profile_analyzer][Profile Analyzer] + +==={profile_analyzer_install} 導入方法 +このツールはPackage Managerからインストールが可能です。 +Unityが公式でサポートしているので、PackagesをUnity Registryに変更し、検索ボックスに「Profile」と入力すると表示されます。 +インストール後「Window -> Analysis -> Profile Analyzer」でツールが起動できます。 + +//image[profile_analyzer_install][PackageManagerからのインストール] + +==={profile_analyzer_top} 操作方法 +Profile Analyzerは起動直後は@{profile_analyzer_top_view}のようになっています。 +//image[profile_analyzer_top_view][起動直後] + +機能として「Single」と「Compare」の2つのモードがあります。 +Singleモードは1つの計測データを分析する場合に使用し、Compareモードは2つの計測データを比較する場合に利用します。 + +「Pull Data」はUnity Profilerで計測したデータを解析させ、結果を表示できます。 +事前にUnity Profilerで計測をしておきましょう。 + +「Save」「Load」はProfile Analyzerで分析したデータの保存・読み込みができます。 +もちろんUnity Profilerのデータだけを保持しておいても問題はありません。 +その場合はUnity Profilerでデータをロードし、Profile Analyzerで毎回Pull Dataを行う必要があります。 +その手順が面倒な場合に専用のデータとして保存しておくのがよいでしょう。 + +==={profile_analyzer_display_single} 解析結果(Singleモード) +解析結果の画面は次のような構成になっています。 +ここではマーカーという単語が出てきますが、処理の名称(メソッド名)を指しています。 + + * 分析区間の設定画面 + * 表示項目のフィルター入力画面 + * マーカーの中央値Top10 + * マーカーの分析結果 + * フレームのサマリ + * スレッドのサマリ + * 選択中マーカーのサマリ + +それぞれの表示画面について見ていきましょう。 + +===={prfoile_analyzer_frame_segment} 1. 分析区間の設定画面 +各フレームの処理時間が表示されており、初期状態では全フレームが選択された状態になっています。 +フレーム区間を@{profile_anlyzer_frame_segment}のようにドラッグで変更できるので、必要であれば調整しましょう。 +//image[profile_anlyzer_frame_segment][フレーム区間の指定] + +===={prfoile_analyzer_filters} 2. フィルター入力画面 +フィルター入力画面は分析結果のフィルタリングができます。 +//image[profile_analyzer_filter][フィルター入力画面] +それぞれの項目は次のようになります。 + +//table[analyzer_filter_explain][Filtersの項目]{ +項目名 説明 +-------------------- +Name Filter 検索したい処理名でフィルターします。 +Exclude Filter 検索から除外したい処理名でフィルターします。 +Thread 選択対象のスレッドが分析結果に表示されます。@
{}他スレッドの情報が必要であれば追加しましょう。 +Depth Slice CPU Usageで紹介したHierarchy表示の階層数です。@
{}たとえばDepthが3の場合は、階層が3つ目のものが表示されます。 +Analysis Type CPU Usageヘッダー項目にあったTotalとSelfの切り替えができます。 +Units 時間表示をミリ秒かマイクロ秒に変更できます。 +Marker Columns 分析結果のヘッダー表示を変更できます。 +//} + +Depth SliceがAllの場合、PlayerLoopというトップノードが表示されたり、同じ処理の階層違いが表示されるため、見づらいときがあります。 +その場合、Depthを2~3ぐらいに固定し、レンダリングやアニメーション、物理演算などのサブシステムが表示されるくらいに設定するとよいでしょう。 + +===={prfoile_analyzer_median_top10} 3. マーカーの中央値Top10 +この画面は各マーカーの処理時間を中央値で並び替え、上位10個のマーカーのみを示したものです。 +上位10個のマーカーがそれぞれどのくらい処理時間を占めているのかが一目でわかります。 +//image[profile_analyzer_median_top10][マーカーの中央値Top10] + +===={prfoile_analyzer_marker} 4. マーカーの分析結果 +それぞれのマーカーの分析結果が表示されています。 +Marker Nameに記載されている処理名と、Median(中央値)、Mean(平均値)の値から改善すべき処理を分析するのがよいでしょう。 +ヘッダー項目にマウスポインターを合わせると、その項目の説明が表示されるので、内容が分からない場合は参照してみてください。 + +//image[profile_analyzer_marker_detail][各処理の分析結果] + +====[column]{median_mean} 平均値と中央値 + +平均値はすべての値を足し合わせ、データ数で割った値です。 +それに対して中央値は、ソートしたデータの中央に位置する値のことを指します。データが偶数個の場合、中央にある前後のデータから平均値を取ります。 + +平均値は極端に値が離れているデータがあると影響を受けやすい性質があります。 +頻繁にスパイクが発生していたり、サンプリング数が十分でない場合、中央値を参考にする方がよい場合があるでしょう。 + +@{mean_median}は中央値と平均値で差が大きくでる例です。 +//image[mean_median][中央値と平均値] +この2つの値の特徴を知った上でデータを分析しましょう。 + +====[/column] + +===={prfoile_analyzer_frame_summary} 5. フレームのサマリ +この画面には計測したデータのフレーム統計情報が表示されています。 +//image[profile_analyzer_frame_summary][フレームサマリ画面][scale=0.5] + +分析しているフレームの区間情報や、値のばらつき具合を箱ひげ図(Boxplot)やヒストグラムを用いて表示しています。 +箱ひげ図は四分位数を理解する必要があります。 +四分位数とはデータをソートした状態で、@
{interquatile}のように値を定めたものです。 + +//table[interquatile][四分位数]{ +名前 説明 +-------------------- +最小値 (Min) 最小の値 +第1四分位数 (Lower Quartile) 最小値から25%の位置にある値 +中央値 (Median) 最小値から50%の位置にある値 +第3四分位数 (Upper Quartile) 最小値から75%の位置にある値 +最大値 (Max) 最大の値 +//} + +25%から75%の区間を箱で囲ったものを箱ひげグラフと言います。 +//image[box_plot][箱ひげグラフ] + +ヒストグラムは横軸に処理時間、縦軸にデータ数を示しており、こちらもデータの分布を見るのに便利です。 +フレームサマリではカーソルを合わせることで、区間とフレーム数を確認できます。 +//image[histogram][ヒストグラム] + +これらの図の見方を理解した上で、特徴を分析するとよいでしょう。 + +===={prfoile_analyzer_thread_summary} 6. スレッドのサマリ +この画面は選択しているスレッドの統計情報が表示されています。 +各スレッドに関する箱ひげ図を見ることができます。 +//image[profile_analyzer_thread_summary][フレームサマリ画面] + +===={prfoile_analyzer_marker_summary} 7. 選択中マーカーのサマリ +@{prfoile_analyzer_marker}画面で選択しているマーカーに関するサマリです。 +選択中のマーカーに関する処理時間が箱ひげ図やヒストグラムで表されます。 +//image[profile_analyzer_marker_summary][選択中マーカーのサマリ] + +==={profile_analyzer_display_compare} 解析結果(Compareモード) +このモードでは2つのデータを比較できます。 +上下それぞれのデータごとに解析する区間の設定ができます。 +//image[profile_analyzer_compare_frame_segment][比較データの設定] + +画面の使い方はSingleモードとほとんど変わりませんが、@{profile_analyzer_comapre_marker}のように「Left」と「Right」という単語が色々な画面に出てきます。 +//image[profile_analyzer_comapre_marker][マーカーの比較] +これはどちらのデータであるかを示しており、@{profile_analyzer_compare_frame_segment}で表示されている色と一致しています。 +Leftは上部のデータ、Rightは下部のデータです。 +このモードを利用することで、チューニング結果の良し悪しを簡単に分析することができるでしょう。 + +=={tool_frame_debugger} Frame Debugger +Frame Debuggerは現在表示されている画面がどのような流れでレンダリングされているかを分析できるツールです。 +このツールはデフォルトでエディターにインストールされており「Window -> Analysis -> Frame Debugger」で開きます。 + +エディターや実機での使用が可能です。 +実機で使用する際はUnity Profilerと同様「Development Build」でビルドされたバイナリが必要です。 +アプリケーションを起動し、デバイス接続先を選択した上で「Enable」を押下すると描画命令が表示されます。 +//image[frame_debugger_connect][FrameDebugger 接続画面] + +==={frame_debugger_enable} 分析画面 +「Enable」を押下すると次のような画面になります。 +//image[frame_debugger_enable][FrameDebugger キャプチャ] + +左枠は1項目が1つの描画命令になっており、上から順番に発行された命令が並んでいます。 +右枠は描画命令に関する詳細情報です。どのShaderがどのようなプロパティと共に処理されたかがわかります。 +この画面を見ながら次のようなことを意識して分析を行います。 + + * 不要な命令がないか + * 描画バッチングが適切に効いているか、まとめられないか + * 描画先の解像度が高すぎないか + * 意図しないShaderを使用していないか + +==={frame_debugger_detailed} 詳細画面 +前節で紹介した@{frame_debugger_enable}の右枠の内容を詳細に解説します。 + +==== 操作パネル +まずは上段部分にある操作パネルについてです。 +//image[frame_debugger_header_tool][上段の操作パネル] +RT0と記載されている部分は複数のレンダリングターゲットが存在する場合に変更できます。 +とくにマルチレンダーターゲットを利用している際に、それぞれのレンダリング状態を確認する際に重宝するでしょう。 +Channelsは、RGBAすべてか、いずれかの1チャンネルのみで表示するかの変更ができます。 +Levelsはスライダーになっており、描画結果の明るさを調節できます。 +たとえばアンビエントや間接照明など描画結果が暗い際に、明るさを調整して見やすくするのに便利です。 + +==== 描画概要 +この領域では描画先の解像度やフォーマットの情報がわかります。明らかに解像度の高い描画先があればすぐに気付くことができるでしょう。 +他にも、使用しているShader名、CullなどのPass設定、使用しているキーワードなどがわかります。 +下部に記載されている「Why this~」という文章は、描画のバッチングができなかった理由が書かれています。 +@{frame_debugger_shader_syntax}の場合、最初の描画コールを選択しているのでバッチングできないと記載されています。 +このように細かく原因が記載されているので、バッチングを工夫したい場合、この情報を頼りに調整するとよいでしょう。 +//image[frame_debugger_shader_syntax][中段の描画概要] + +==== Shaderプロパティの詳細情報 +この領域は描画しているShaderのプロパティ情報が記載されています。 +デバッグ時に重宝します。 +//image[frame_debugger_shader_property][下段のShaderプロパティの詳細情報] + +//info{ +プロパティ情報に表示されているTexture2Dがどのような状態になっているか詳細に確認したいときがあります。 +その際、Macの場合はCommandキー (Windowsの場合Controlキー)を押下しながら画像をクリックすると拡大できます。 +//image[frame_debugger_preview][Texture2Dのプレビューを拡大する] +//} + +=={tool_memory_profiler} Memory Profiler +Memory ProfilerはPreview PackageとしてUnityが公式で提供しているツールです。 +Unity ProfilerのMemoryモジュールと比較して、主に以下のような点で優れています。 + + * キャプチャしたデータがスクリーンショット付きでローカルに保存される + * 各カテゴリのメモリ占有量がビジュアライズされ、わかりやすい + * データの比較が可能 + +Memory Profilerはv0.4以降とそれ未満でUIが大きく変わりました。 +本書籍では執筆時点で最新版となるv0.5を使用します。 +またv0.4以降のバージョンで、すべての機能を利用する場合Unity 2020.3.12f1以降のバージョンが必要になります。 +さらにv0.4とv0.5は一見同じように見えますが、v0.5で大幅に機能がアップデートされました。 +とくにオブジェクトの参照関係がとても追いやすくなったので、基本的にはv0.5以降の利用を推奨します。 + +//image[memory_profiler_top][Memory Profiler] + +==={memory_profiler_install} 導入方法 +Unity 2020では、Preview版のパッケージは「Project Settings -> Package Manager」から「Enable Preview Packages」を有効にする必要があります。 +//image[enable_preview_package][Preview Packagesの有効化] + +その後、Unity RegistryのPackageからMemory Profilerをインストールします。 +インストール後「Window -> Analysis -> Memory Profiler」でツールが起動できます。 +//image[memory_profiler_install][PackageManagerからインストール] + +またUnity 2021以降では、パッケージの追加方法が変更されています。 +追加するには「Add Package by Name」を押下し「com.unity.memoryprofiler」と入力する必要があります。 +//image[memory_profiler_add_package_2021][2021以降の追加方法] + +==={memory_profiler_control} 操作方法 +Memory Profilerは大きく分けて4つの要素から構成されています。 + + * ツールバー + * Snapshot Panel + * 計測結果 + * Detail Panel + +それぞれの領域について解説を行います。 + +===={memory_profiler_tool_bar} 1. ツールバー +//image[memory_profiler_tool_bar][ツールバー領域] +@{memory_profiler_tool_bar}はHeaderのキャプチャを示しています。 +①のボタンは計測対象を選択できます。 +②のボタンは押下するとその時点のメモリを計測します。オプションで計測対象をNative Objectだけにしたり、スクリーンショットを無効にできます。 +基本デフォルト設定で問題ないでしょう。 +③のボタンは押下すると計測済みのデータを読み込みができます。 +その他「Snapshot Panel」や「Detail Panel」を押下することで、左右にある情報パネルの表示/非表示が可能です。 +Tree Mapの表示だけを見たい場合に非表示にすることがよいでしょう。 +また「?」を押下することで公式ドキュメントを開くことができます。 + +計測に関する注意点が1つあります。 +それは計測時に必要なメモリが新たに確保され、以降解放されないことです。 +ただし無限には増え続けず、何度か計測するといずれ落ち着きます。計測時のメモリ確保量はプロジェクトの複雑度によるでしょう。 +この前提を知らないと、膨れ上がったメモリ使用量を見てリークがあるかもと勘違いしてしまうので気をつけましょう。 + +===={memory_profiler_snapshot_panel} 2. Snapshot Panel +//image[memory_profiler_snapshot_panel_single][Snapshot Panel (Single)] + +Snapshot Panelには計測したデータが表示され、どのデータを見るか選択できます。 +このデータはアプリを起動してから終了するまでを1セッションとし、セッションごとにまとめられます。 +また計測データを右クリックすることで削除や名称のリネームが可能です。 + +上部に「Single Snapshot」「Compare Snapshots」があります。 +Compare Snapshotsを押下すると、計測データを比較するUIに表示が変わります。 +//image[memory_profiler_snapshot_panel_compare][Snapshot Panel (Comapre)] + +「A」がSingle Snapshotで選択したデータ、「B」がComapre Snapshotsで選択したデータです。 +入れ替えボタンを押下することで「A」と「B」を入れ替えることができるので、わざわざSingle Snapshot画面に戻らずに切り替えも可能です。 + +===={memory_profiler_snapshot_result} 3. 計測結果 +計測結果は「Summary」「Objects and Allocations」「Fragmentation」の3つのタブがあります。 +本節ではよく使用するSummaryを解説し、その他は補足として簡単に取り上げます。 +Summaryの画面上部はMemory Usage Overviewと呼ばれる領域で、現在のメモリの概要が表示されています。 +項目を押下するとDetail Panelに解説が表示されるので、分からない項目は確認するとよいでしょう。 +//image[memory_profiler_usage_overview][Memory Usage Overview] + +次に画面中部はTree Mapと呼ばれており、オブジェクトのカテゴリごとにメモリ使用量をグラフィカルに表示します。 +各カテゴリを選択すると、カテゴリ内のオブジェクトを確認できます。 +@{memory_profiler_tree_map}ではTexture2Dのカテゴリを選択している状態です。 +//image[memory_profiler_tree_map][Tree Map] + +そして画面下部はTree Map Tableと呼ばれています。ここではオブジェクトの一覧がテーブル形式で並んでいます。 +表示項目はTree Map Tableのヘッダーを押下することで、グループ化やソート、フィルターを行うことができます。 +//image[memory_profiler_tree_map_table_option][ヘッダー操作] + +とくにTypeをグループ化することで分析しやすくなるため、積極的に利用しましょう。 +//image[memory_profiler_tree_map_table_group][Typeによるグループ化] + +またTree Mapでカテゴリを選択した場合は、そのカテゴリ内のオブジェクトだけを表示するフィルターが自動で設定されます。 +//image[memory_profiler_tree_map_table][フィルターの自動設定] + +最後にCompare Snapshotsを使用した場合のUIの変化について説明します。 +Memory Usage Overviewにはそれぞれのオブジェクトの差分が表示されます。 +//image[memory_profiler_compare_snaphost_overview][Compare SnapshotsのMemory Usage Overview] + +またTree Map TableにはHeaderにDiffという項目が追加されます。 +Diffには次のようなタイプがあります。 + +//table[compare_tree_map_view][Tree Map Table (Compare)]{ +Diff 説明 +-------------------- +Same A、B同一オブジェクト +Not in A (Deleted) Aにあるが、Bにはないオブジェクト +Not in B (New) Aにはないが、Bにあるオブジェクト +//} + +これらの情報を見ながらメモリが増減しているかを確認できます。 + +===={memory_profiler_snapshot_detail} 4. Detail Panel +このパネルは選択しているオブジェクトの参照関係を追跡したい際に利用します。 +このReferenced Byを確認することで、参照を握り続けている原因を把握できるでしょう。 +//image[memory_profiler_detail_references][Referenced By] + +下部のSelection Detailsという箇所には、オブジェクトの詳細な情報が載っています。 +その中でも「Help」という項目には、解放するためのアドバイスなどが記載されています。 +何をするべきか分からないときは一読するとよいかもしれません。 +//image[memory_profiler_detail_selection][Selection Details] + + +===={memory_profiler_others} 補足:Summary以外の計測結果について + +「Objects and Allocations」はSummaryと違い、アロケーションなどのより細かい内容がテーブル形式で確認できます。 +//image[memory_profiler_objects_allocations][Table Viewの指定] +「Fragmentation」は仮想メモリの状況を可視化してくれるため、断片化の調査に利用できます。 +メモリアドレスなど直感的でない情報が多いため利用する難易度は高いでしょう。 +//image[memory_profiler_fragmentation][Fragmentation] + +またMemory Profilerのv0.6から「Memory Breakdowns」という新機能が追加されました。 +Unity 2022.1以降が必須となりますが、TreeMapをリスト表示で閲覧できたり、Unity Subsystemsなどのオブジェクト情報も見ることが可能になりました。 +他にも、重複の可能性があるオブジェクトを確認できるようになりました。 +//image[memory_profiler_memory_break_down][Memory Breakdowns] + +=={tool_heap_explorer} Heap Explorer +Heap Explorerは個人開発者であるPeter77@{author_of_heap_explore}氏のオープンソースのツールです。 +こちらはMemory Profilerと同様にメモリの調査を行う際によく利用するツールです。 +Memory Profilerは0.4以前のバージョンだと、参照がリスト形式で表示されないため追跡するのに非常に労力がかかりました。 +0.5以降で改善されたものの、対応していないUnityバージョンを利用している方もいるでしょう。 +その際の代替ツールとしてはまだまだ利用価値が高いので今回取り上げたいと思います。 +//footnote[author_of_heap_explore][@{https://github.com/pschraut}] + +//image[heap_explorer_top][Heap Explorer] + +==={heap_explorer_install} 導入方法 +本ツールはGitHubのリポジトリ@{url_of_heap_explorer}に記載されている +Package URL'sをコピーし、Package ManagerのAdd Package from Git urlから追加します。 +インストール後「Window -> Analysis -> Memory Profiler」でツールが起動できます。 +//footnote[url_of_heap_explorer][@{https://github.com/pschraut/UnityHeapExplorer}] + +==={heap_explorer_control} 操作方法 +Heap Explorerのツールバーは次のようになっています。 +//image[heap_explorer_header][Heap Explorerのツールバー] + + : 左右の矢印 + 操作の戻る、進むができます。とくに参照を追跡する場合に便利です。 + : File + 計測ファイルの保存、読み込みが可能です。.heapという拡張子で保存されます。 + : View + 表示画面の切り替えができます。色々な種類があるので、興味があればドキュメントを見てください。 + : Capture + 計測を行います。ただし@{計測対象はHeap Explorer上では変更ができません}。 + 対象の切り替えはUnity ProfilerなどのUnityが提供するツール上で変更する必要があります。 + またSaveはファイルに保存した上で結果が表示され、Analyzeは保存せずに表示します。 + なお、Memory Profilerと同様に、計測する際に確保されるメモリは解放されないので注意してください。 + +//image[heap_explorer_target][計測対象の切り替え] + +計測結果の画面は次のようになります。この画面をOverviewと呼びます。 +//image[heap_explorer_capture_top][Heap Explorerの計測結果 (Overview)] +Overviewの中でとくに気にするべきカテゴリは緑の線が引いてあるNative Memory UsageとManaged Memory Usageです。 +Investigateのボタンを押下するとそれぞれのカテゴリの詳細を確認できます。 + +以降ではカテゴリ詳細の表示について、重要な部分に絞って解説します。 + +===={heap_explorer_objects} 1. Object +Native MemoryをInvestigateした場合、C++ Objectsがこの領域に表示されます。 +Managed Memoryの場合はC# Objectsがこの領域に表示されます。 +//image[heap_explorer_objects_view][Object表示領域] + +ヘッダーにいくつか見慣れない項目があるので補足しておきます。 + + : DDoL + Don't Destroy On Loadの略です。シーンを遷移しても破棄しないオブジェクトとして指定されているかがわかります。 + : Persistent + 永続オブジェクトかどうかです。起動時にUnityが自動生成するものがこれに当たります。 + +これ以降に紹介する表示領域は、@{heap_explorer_objects_view}のオブジェクトを選択することで更新されます。 + +===={heap_explorer_referenced_by} 2. Referenced by +対象オブジェクトの参照元となるオブジェクトが表示されます。 +//image[heap_explorer_referenced_by][Referenced by] + +===={heap_explorer_references_to} 3. References to +対象オブジェクトの参照先となるオブジェクトが表示されます。 +//image[heap_explorer_references_to][References to] + +===={heap_explorer_path_to_root} 4. Path to Root +対象オブジェクトを参照しているルートオブジェクトが表示されます。 +メモリリークを調査する際、何が参照を握っているのか把握できるので重宝します。 +//image[heap_explorer_path_to_root][Path to Root] + +今までの項目をまとめると次のようなイメージになります。 +//image[heap_explorer_reference_explain][参照のイメージ] + +ここまで紹介したようにHeap Explorerにはメモリリークやメモリ調査に必要な機能が一式揃っています。 +動作も軽快なのでぜひこのツールの利用も検討してみてください。 +気に入ったら感謝の意を込めてStarもつけると尚よいでしょう。 + +=={tool_xcode} Xcode +XcodeはAppleが提供する統合開発環境ツールです。 +UnityでターゲットプラットフォームをiOSに設定した際、そのビルド成果物がXcodeプロジェクトになります。 +Unityで計測するよりも正確な数値が取れるので、厳密な検証をする際はXcodeを利用することを推奨します。 +本節ではDebug Navigator、GPU Frame Capture、Memory Graphの3つのプロファイリングツールに触れていきます。 + +==={tool_xcode_debug_start} プロファイル方法 +Xcodeからプロファイルする方法は2通りあります。 +1つ目はXcodeから直接端末にビルドし、アプリケーションを実行する方法です。 +@{xcode_run}に示すように実行ボタンを押下するだけでプロファイルが開始されます。 +ビルドを行う際の証明書などの設定は本書では割愛します。 +//image[xcode_run][Xcodeの実行ボタン] + +2つ目は実行中のアプリケーションをXcodeのデバッガーにアタッチする方法です。 +これはアプリを実行した後に、Xcodeメニューの「Debug -> Attach to Process」から実行中のプロセスを選択することでプロファイルができます。 +ただし、ビルド時の証明書が開発者用(Apple Development)でないといけません。 +Ad HocやEnterpriseの証明書ではアタッチできないので注意してください。 +//image[xcode_debugger_attach][Xcodeのデバッガーアタッチ] + +==={tool_xcode_debug_navi} Debug Navigator +Debug NavigatorはXcodeからアプリケーションを実行するだけで、CPUやMemoryなどのデバッグゲージが確認できます。 +アプリケーション実行後に@{xcode_debug_navi_tab}のスプレーマークを押下することで6つの項目が表示されます。 +もしくはXcodeメニューの「View -> Navigators -> Debug」から開くことも可能です。 +以降ではそれぞれの項目について解説します。 +//image[xcode_debug_navi_tab][Debug Navigatorの選択] + +===={tool_xcode_debug_navi_cpu} 1. CPUゲージ +どれぐらいCPUを使用しているかを見ることができます。 +また各スレッドごとの使用率も見ることが可能です。 +//image[xcode_debug_navi_cpu][CPUゲージ] + +//embed[latex]{ +\clearpage +//} + +===={tool_xcode_debug_navi_memory} 2. Memoryゲージ +メモリ消費量の概要を見ることができます。 +内訳などの細かい分析はできません。 +//image[xcode_debug_navi_memory][Memoryゲージ] + +===={tool_xcode_debug_navi_enery} 3. Energyゲージ +電力消費に関する概要を見ることができます。 +CPU、GPU、Networkなどの使用率の内訳が把握できます。 +//image[xcode_debug_navi_energy][Energyゲージ] + +===={tool_xcode_debug_navi_disk} 4. Diskゲージ +File I/Oの概要を見ることができます。 +予期せぬタイミングでファイルの読み書きが行われていないかのチェックに役立つでしょう。 +//image[xcode_debug_navi_disk][Diskゲージ] + +===={tool_xcode_debug_navi_network} 5. Networkゲージ +ネットワーク通信の概要を見ることができます。 +Diskと同じく予期しない通信をしていないかなどのチェックに役立つでしょう。 +//image[xcode_debug_navi_network][Networkゲージ] + +===={tool_xcode_debug_navi_fps} 6. FPSゲージ +このゲージはデフォルトでは表示されていません。 +@{tool_xcode_frame_debug}で解説するGPU Frame Captureを有効化すると表示されます。 +FPSはもちろんのこと、シェーダーステージの使用率、各CPUやGPUの処理時間を確認できます。 +//image[xcode_debug_navi_fps][FPSゲージ] + +==={tool_xcode_frame_debug} GPU Frame Capture +GPU Frame CaptureとはXcode上でフレームデバッグができるツールのことです。 +UnityのFrame Debuggerと同様に、レンダリングが完了するまでの過程が確認できます。 +Unityと比べシェーダーの各ステージでの情報量が多いため、ボトルネックの分析や改善に役立つかもしれません。 +以下では使用方法について解説します。 + +===={tool_xcode_frame_debug_ready} 1. 準備 +XcodeでGPU Frame Captureを有効にするにはスキームの編集が必要になります。 +初めに「Product -> Scheme -> Edit Scheme」でスキーム編集画面を開きましょう。 +//image[xcode_frame_debugger_scheme][スキーム編集画面] + +次に「Options」タブからGPU Frame Captureを「Metal」に変更しましょう。 +//image[xcode_frame_debugger_gpu_enable][GPU Frame Captureの有効化] + +最後に「Diagnostics」タブからMetalの「Api Validation」を有効にしましょう。 +//image[xcode_frame_debugger_api][Api Validationの有効化] + +===={tool_xcode_frame_debug_capture} 2. キャプチャ +実行中にデバッグバーからカメラマークを押下することでキャプチャが行われます。 +シーンの複雑度によりますが、初回のキャプチャには時間がかかるので気長に待ちましょう。 +なお、Xcode13以降の場合はMetalのアイコンに変更されています。 +//image[xcode_frame_debugger_capture_icon][GPU Frame Captureボタン] + +キャプチャが完了すると次のようなサマリー画面が表示されます。 +//image[xcode_frame_debugger_summary][サマリー画面] + +このサマリー画面からは描画の依存関係やメモリなどの詳細を確認できる画面へ遷移できます。 +またNavigatorエリアには描画に関するコマンドが表示されています。 +この表示方法は「View Frame By Call」と「View Frame By Pipeline State」があります。 +//image[xcode_frame_debugger_navigator_view][表示方法の変更] + +By Call表示では描画コマンドがすべて呼び出された順に並びます。 +これは描画の前準備になるバッファーの設定なども含まれるため非常に多くのコマンドが並ぶことになります。 +一方、By Pipeline Stateは各シェーダーで描画されたジオメトリに関する描画コマンドだけが並びます。 +どういったことを調査するかで表示を切り替えるとよいでしょう。 +//image[xcode_frame_debugger_view_frame_diff][表示の違い] + +Navigatorエリアに並ぶ描画コマンドは押下することで、そのコマンドに使用されているプロパティ情報が確認できます。 +プロパティ情報にはテクスチャ、バッファー、サンプラー、シェーダーの関数、ジオメトリなどがあります。 +それぞれのプロパティはダブルクリックすることで詳細を確認できます。 +たとえばシェーダーコードそのものや、サンプラーがRepeatなのかClampなのかといったことまで把握できます。 +//image[xcode_frame_debuggeer_command][描画コマンド詳細] + +またジオメトリのプロパティは頂点情報がテーブル形式で表示されるだけでなく、カメラを動かして形状の確認も可能です。 +//image[xcode_frame_debugger_geometry][ジオメトリビューワー] + +//embed[latex]{ +\clearpage +//} + +次にサマリー画面のPerformance欄にある「Profile」について説明します。 +このボタンを押下するとより細かい解析を開始します。 +解析が終わると描画にかかった時間がNavigatorエリアに表示されるようになります。 +//image[xcode_frame_debugger_navigator_profile][プロファイル後の表示] + +それだけでなく解析結果をCountersという画面でより詳細に確認ができます。 +この画面では各描画のVertexやRasterized、Fragmentなどの処理時間をグラフィカルに見ることが可能です。 +//image[xocde_frame_debugger_counter][Counters画面] + +続いてサマリー画面のMemory欄にある「Show Memory」について説明します。 +このボタンを押下するとGPUで使用しているリソースが確認できる画面へ遷移します。 +表示される情報は主にテクスチャとバッファーです。不要なものがないか確認するとよいでしょう。 +//image[xcode_frame_debugger_memory][GPUリソース確認画面] + +最後にサマリー画面のOverviewにある「Show Dependencies」について説明します。 +このボタンを押下すると各レンダーパスの依存関係が表示されます。 +Dependencyを見ていく際には「矢印が外向き」になっているボタン押下することで、その階層以下の依存関係をさらに開くことができます。 +//image[xcode_frame_debugger_dependency_open][Dependencyを開くボタン] +この画面はどの描画が何に依存しているかを見たいときに使用しましょう。 +//image[xcode_frame_debugger_dependency][階層を開いた状態] + +==={tool_xcode_memory_graph} Memory Graph +このツールではキャプチャタイミングでのメモリ状況を分析できます。 +左側のNavigatorエリアにはインスタンスが表示され、そのインスタンスを選択することで参照関係がグラフで表示されます。 +右側のInspectorエリアにはそのインスタンスの詳細情報が表示されます。 +//image[xcode_memory_graph_top][MemoryGraph キャプチャ画面] + +このツールではプラグインなどUnity上では計測できないオブジェクトのメモリ使用量を調査をする場合に利用するとよいでしょう。 +以下では使用方法について解説します。 + +===={tool_xcode_memory_graph_ready} 1. 事前準備 +Memory Graphで有益な情報を得るためにスキームの編集を行う必要があります。 +「Product -> Scheme -> Edit Scheme」でスキーム編集画面を開きましょう。 +そして「Diagnostics」タブから「Malloc Stack Logging」を有効にしましょう。 +//image[xcode_memory_graph_malloc_stack_logging][Malloc Stack Loggingの有効化] + +こちらを有効にすることでInspectorにBacktraceが表示されるようになり、どのような流れでアロケーションされたかがわかります。 +//image[xcode_memory_graph_backtrace][Backtraceの表示] + +===={tool_xcode_memory_graph_capture} 2. キャプチャ +アプリケーション実行中にデバッグバーから枝のようなアイコンを押下することでキャプチャが行われます。 +//image[xcode_memory_graph_icon][Memory Graph Captureボタン] + +またMemory Graphは「File -> Export MemoryGraph」からファイルとして保存が可能です。 +このファイルに対して、vmmapコマンドやheapコマンド、malloc_historyコマンドを駆使してさらに深い調査をすることが可能です。 +興味があればぜひ調べてみてください。 +例としてvmmapコマンドのSummary表示を紹介します。MemoryGraphだと掴みにくかった全体像を把握できます。 +//listnum[vmmap_summary][vmmap summaryコマンド]{ +vmmap --summary hoge.memgraph +//} + +//image[xcode_vmmap_summary][MemoryGraph Summary 表示] + +=={tool_instruments} Instruments +XcodeにはInstruments(インストゥルメンツ)という詳細な計測・分析を得意とするツールがあります。 +Instrumentsは「Product -> Analyze」を選択することでビルドが始まります。 +完了すると次のような計測項目のテンプレートを選択する画面が開きます。 +//image[instruments_menu][Instruments テンプレート選択画面] + +テンプレートの多さからわかるように、Instrumentsはさまざまな内容を分析できます。 +本節ではこの中からとくに利用する頻度の高い「Time Profiler」と「Allocations」を取り上げます。 + +==={tool_instruments_time_profiler} Time Profiler +Time Profilerはコードの実行時間を計測するツールです。 +Unity ProfilerにあったCPUモジュールと同じく、処理時間を改善する際に使用します。 + +計測を開始するためにはTime Profilerのツールバーにある赤い丸印のレコードボタンを押下する必要があります。 +//image[insturments_record_start][レコード開始ボタン] + +//embed[latex]{ +\clearpage +//} + +計測を行うと@{instruments_time_profile_result}のような表示になります。 +//image[instruments_time_profile_result][計測結果] +Unity Profilerと違いフレーム単位ではなく、区間での分析を行っていきます。 +下部のTree Viewには区間内の処理時間が表示されます。 +ゲームロジックの処理時間を最適化する場合、Tree ViewのPlayerLoop以下の処理を分析していくとよいでしょう。 + +Tree Viewの表示を見やすくするために、Xcodeの下部にあるCall Treesの設定を@{instruments_call_tress}のようにしておくとよいでしょう。 +とくにHide System Librariesにチェックすることで、手の出せないシステムコードが非表示になり調査がしやすくなります。 +//image[instruments_call_tress][Call Trees設定] + +このようにして処理時間を分析し、最適化していくことができます。 + +//info{ +Time Profilerではシンボル名がUnity Profilerと違います。 +大きく変わりはないですが「クラス名_関数名_ランダムな文字列」という表記になります。 +//image[xcode_time_profiler_il2cpp][Time Profilerでのシンボル名] +//} + +==={tool_instruments_allocations} Allocations +Allocationsはメモリ使用量を計測するためのツールです。メモリリークや使用量の改善を行う際に使用します。 +//image[instruments_allocations_view][Allocations 計測画面] + +計測を行う前に「File -> Recording Options」を開き「Discard events for freed memory」にチェックを入れましょう。 +//image[instruments_recording_options][オプション設定画面] + +このオプションを有効にするとメモリが解放された際に記録を破棄します。 +//image[instruments_recodring_options_diff][オプション設定による違い] +@{instruments_recodring_options_diff}を見てわかるようにオプションの有無で見た目が大きく変わります。 +オプションありの場合、メモリがアロケーションしたタイミングでのみ線が記録されます。 +また記録された線は、その確保された領域が解放されると破棄されます。 +つまりこのオプションを設定することで、線が残り続けていればメモリから解放されていないということになります。 +たとえばシーン遷移によってメモリを解放する設計の場合、遷移前のシーン区間内に線が多く残っていた場合メモリリークの疑いがあります。 +その際はTree Viewで詳細を追いましょう。 + +画面下部のTree ViewはTime Profilerと同様に指定した範囲の詳細が表示されます。 +このTree Viewの表示方法は4種類あります。 +//image[instruments_allocations_view_type][表示方法の選択] +もっともオススメの表示方法はCall Treesです。 +これによってどのコードによってアロケーションが発生したのかを追うことができます。 +画面下部にCall Treesの表示オプションがあるので、Time Profilerで紹介した +@{instruments_call_tress}と同じようにHide System Librariesなどのオプションを設定するとよいでしょう。 +@{instruments_allocations_call_tree}ではCall Treesの表示をキャプチャしました。 +12.05MBのアロケーションがSampleScriptのOnClickedで発生していることがわかります。 +//image[instruments_allocations_call_tree][Call Tree表示] + +最後にGenerationsという機能を紹介します。 +Xcodeの下部に「Mark Generations」というボタンがあります。 +//image[instruments_allocations_mark_generation][Mark Generationボタン] +これを押下するとそのタイミングでのメモリが記憶されます。 +その後、再度Mark Generationsを押下すると前回のデータと比較して新たに確保されたメモリ量が記録されます。 +//image[instruments_allocations_generations][Generations] +@{instruments_allocations_generations}の各Generationsは、 +詳細を見るとCall Tree形式で表示されるため、メモリ増加が何によって発生したかを追うことが可能です。 + +=={tool_android_studio} Android Studio +Android StudioはAndroidの統合開発環境ツールです。 +このツールを用いてアプリケーションの状態を計測できます。 +プロファイル可能な項目はCPU、Memory、Network、Energyの4つです。 +本節ではまずプロファイル方法について紹介し、その後CPUとMemoryの計測項目について解説します。 +//image[android_studio_profile][プロファイル画面] + +==={tool_android_studio_debug_start} プロファイル方法 +プロファイル方法は2通りあります。 +1つ目はAndroid Studio経由でビルドし、プロファイルする方法です。 +この方法ではまずAndroid StudioのプロジェクトをUnityから書き出します。 +Build Settingsで「Export Project」にチェックをいれてビルドします。 +//image[android_studio_export][プロジェクトのExport] + +次に書き出されたプロジェクトをAndroid Studioで開きます。 +そしてAndroid端末を接続した状態で、右上にあるゲージのようなアイコンを押下するとビルドが開始します。 +ビルド完了後、アプリが立ち上がりプロファイルが開始します。 +//image[android_studio_tool_bar][Profile開始アイコン] + +2つ目は実行中のプロセスをデバッガーにアタッチして計測する方法です。 +まずAndroid Studioメニューの「View -> Tool Windows -> Profiler」からAndroid Profilerを開きます。 +//image[android_studio_profiler_menu][Android Profilerを開く] + +次に開いたProfilerの@{SESSIONS}から計測対象のセッションを選択します。 +セッションを接続するためには、計測するアプリケーションを起動しておく必要があります。 +また@{Development Buildを適応したバイナリ}でないといけません。 +セッションの接続が完了するとプロファイルが開始します。 +//image[android_studio_profiler_attach][プロファイルするSESSIONを選択する] + +2つ目のデバッガーにアタッチする方法はプロジェクトを書き出す必要がなく、気軽に利用できるので覚えておくと良いでしょう。 + +//info{ +厳密にはUnityのDevelopment Buildではなく、AndroidManifest.xmlにてdebuggableやprofileableの設定を行う必要があります。 +UnityではDevelopment Buildを行った場合、自動的にdebuggableがtrueになります。 +//} + +==={tool_android_studio_cpu} CPU計測 +CPU計測の画面は@{android_studio_cpu_thread_select}のようになります。 +この画面だけでは何がどれぐらいの処理時間を消費しているかわかりません。 +より詳細に見るためには、詳細に見たいスレッドを選択する必要があります。 +//image[android_studio_cpu_thread_select][CPU計測トップ画面、スレッド選択] + +スレッド選択後、Recordボタンを押下することでスレッドのコールスタックが計測されます。 +@{android_studio_record_button}のように計測タイプがいくつかありますが「Callstack Sample Recording」で問題ないでしょう。 +//image[android_studio_record_button][Record開始ボタン] + +Stopボタンを押下すると計測が終了し、結果が表示されます。 +結果画面としてはUnity ProfilerのCPUモジュールのような表示になります。 +//image[android_studio_sampling_result][コールスタック計測結果画面] + +==={tool_android_studio_memory} Memory計測 +Memory計測の画面は@{android_studio_memory}のようになります。 +この画面ではメモリの内訳を見ることはできません。 +//image[android_studio_memory][Memory計測画面] + +メモリの内訳を見る場合は追加で計測を行う必要があります。 +計測方法は3種類あります。「Capture heap dump」は押下したタイミングでのメモリ情報が取得できます。 +それ以外のボタンは計測区間中のアロケーションを分析するためのものです。 +//image[android_studio_memory_record_options][Memory計測オプション] + +例として@{android_studio_memory_heap_dump}にHeap Dumpの計測結果をキャプチャしました。 +詳細分析するには少し粒度が粗いので難易度が高いでしょう。 +//image[android_studio_memory_heap_dump][Heap Dump結果] + +=={tool_graphics_render_doc} RenderDoc +RenderDocはオープンソースで開発されており、無料で利用できる高品質なグラフィックスデバッガーツールです。 +このツールは現時点ではWindowsとLinuxに対応していますが、Macには対応していません。 +またサポートしているGraphics APIはVulkanやOpenGL(ES)、D3D11、D3D12などです。 +そのためAndroidでの利用はできますがiOSでは利用できません。 + +本節ではAndroidアプリケーションを実際にプロファイルしてみます。 +ただしAndroidのプロファイリングにはいくつか制約があるので注意してください。 +まずAndroid OSバージョンは6.0以降が必須です。そして計測対象のアプリケーションはDebuggableを有効にする必要があります。 +これはビルド時にDevelopment Buildを選択しておけば問題ありません。 +なお、プロファイルに使用したRenderDocのバージョンはv1.18となります。 + +==={render_doc_ready} 計測方法 +まずはRenderDocの準備を行いましょう。 +公式サイト@{render_doc_url}からインストーラーをダウンロードし、ツールのインストールを行いましょう。 +インストール後、RenderDocのツールを開きます。 +//footnote[render_doc_url][@{https://renderdoc.org/}] + +//image[render_doc_top][RenderDoc 起動後の画面] + +次にAndroidデバイスをRenderDocと接続します。 +左下隅にある家マークを押下するとPCと接続されているデバイスリストが表示されます。 +リストの中より計測したい端末を選択してください。 +//image[render_doc_connection][デバイスとの接続] + +次に接続したデバイスから起動するアプリケーションを選択しましょう。 +右側にあるタブからLaunch Applicationを選択し、Executable Pathから実行するアプリを選択します。 +//image[render_doc_launch_application_tab][Launch Applicationタブ 実行アプリ選択] + +File Browserのウィンドウが開くので、今回計測するPacakge Nameを探しActivityを選択します。 +//image[render_doc_launch_application_select][計測するアプリケーション選択] + +最後にLaunch ApplicationからLaunchボタンを押下するとデバイスでアプリケーションが起動します。 +またRenderDoc上では計測するためのタブが新たに追加されます。 +//image[render_doc_capture_menu][計測用のタブ] + +試しにCapture Frame(s) Immediatelyを押下すると、フレームデータがキャプチャされ、Capture collectedに並びます。 +このデータをダブルクリックすることでキャプチャデータを開くことができます。 + +==={render_doc_capture_dislay} キャプチャデータの見方 +RenderDocにはさまざまな機能がありますが、本節では重要な部分に絞って機能を説明します。 +まず画面上部にはキャプチャしたフレームのタイムラインが表示されます。 +それぞれの描画コマンドがどのような順番で行われたかを視覚的に捉えることができます。 +//image[render_doc_timeline][タイムライン] + +次にEvent Browserです。ここには各コマンドが上から順番に記載されています。 +//image[render_doc_event_browser][Event Browser] + +Event Browser内の上部にある「時計マーク」を押下すると、各コマンドの処理時間が「Duration」に表示されます。 +計測タイミングによって処理時間が変動するのであくまで目安と考えるとよいでしょう。 +また、DrawOpaqueObjectsのコマンドの内訳を見ると3つがBatch処理されており、1つだけBatchから外れた描画になっているのがわかります。 + +次に右側にタブで並んでいる各ウインドウについて説明します。 +このタブの中にはEvent Browserで選択したコマンドの詳細情報を確認できるウインドウがあります。 +とくに重要なのはMesh Viewer、Texture Viewer、Pipeline Stateの3つです。 +//image[render_doc_window_tab][各ウインドウ] + +初めにPipeline Stateについてです。 +Pipeline Stateはそのオブジェクトが画面に描画されるまでの各シェーダーステージでどのようなパラメーターだったかを確認できます。 +また使用されたシェーダーやその中身も閲覧可能です。 +//image[render_doc_pipeline_state][Pipieline State] +パイプラインステートに表示されるステージ名は省略されているため正式名称を@
{pipeline_state_name}にまとめます。 +//table[pipeline_state_name][PipielineStateの正式名称]{ +ステージ名 正式名 +-------------------- +VTX Vertex Input +VS Vertex Shader +TCS Tessellation Control Shader +TES Tessellation Evaluation Shader +GS Geometry Shader +RS Rasterizer +FS Fragment Shader +FB Frame Buffer +CS Compute Shader +//} + +@{render_doc_pipeline_state}ではVTXステージが選択されており、トポロジーや頂点入力データが確認できます。 +他にも@{render_doc_pipeline_state_framebuffer}のFBステージでは、アウトプット先のテクスチャの状態やBlend Stateなどの詳細を見られます。 +//image[render_doc_pipeline_state_framebuffer][FB(Frame Buffer)のState] + +また@{render_doc_pipeline_state_fragment}のFSステージでは、フラグメントシェーダー内で利用されたテクスチャやパラメーターの確認ができます。 +//image[render_doc_pipeline_state_fragment][FS(Fragment Shader)のState] +FSステージの中央にあるResourcesには、使用されたテクスチャやサンプラーが表記されています。 +またFSステージの下段にあるUniform Buffersには、CBufferが表示されています。このCBufferにはfloatやcolorなど、数値情報のプロパティが格納されています。 +各項目の右側には「Go」という矢印アイコンがあり、これを押下するとデータの詳細を確認できます。 + +FSステージの上段には使用したシェーダーが表示されています。Viewを押下するとそのシェーダーコードを確認できます。 +表示をわかりやすくするためにDisassembly typeはGLSLがオススメです。 +//image[render_doc_shader_code][Shaderコードの確認] + +次にMesh Viewerです。これはMeshの情報を視覚的に見ることができ、最適化やデバッグに便利な機能です。 +//image[render_doc_mesh_viewer][Mesh Viewer] +Mesh Viewerの上段にはメッシュの頂点情報がテーブル形式で表記されています。 +下段にはカメラを動かして形状が確認できるプレビュー画面があります。 +どちらもInとOutでタブが分かれており、変換の前後で値や見た目がどのように変わったかを把握できます。 +//image[render_doc_mesh_view_in_out][Mesh ViewerのIn、OutのPreview表示] + +最後にTexture Viewerです。 +この画面ではEvent Browserで選択しているコマンドの「入力に使用したテクスチャ」と「出力結果」がわかります。 +//image[texture_viewer_input_output][Texture Viewer テクスチャ確認画面] +画面右側の領域では入出力のテクスチャを確認できます。表示されているテクスチャを押下すると画面左側の領域に反映されます。 +左側の画面はただ表示するだけでなく、カラーチャンネルをフィルターしたり、ツールバーの設定を適用させることができます。 +//image[texture_viewer_toolbar][Texture Viewerのツールバー] +@{texture_viewer_input_output}では、Overlayに「Wireframe Mesh」を選択していたので、 +このコマンドで描画されたオブジェクトに黄色のワイヤーフレーム表示がされており視覚的にわかりやすくなっています。 + +またTexture ViewerにはPixel Contextという機能があります。 +これは選択したピクセルの描画履歴を見るための機能です。 +履歴がわかるということはあるピクセルに対して塗りつぶしがどれぐらいの頻度で行われているかを把握できます。 +これはオーバードローの調査や最適化を行うのに便利な機能です。 +ただしあくまでもピクセル単位なので全体的にオーバードローを調査するのには向いていません。 +調査方法としては@{texture_viewer_input_output}の左側の画面で、調査したい箇所を右クリックするとPixel Contextにその位置が反映されます。 +//image[texture_viewer_pixel_context][Pixel Contextへの反映] +次にPixel ContextのHistoryボタンを押下すると、そのピクセルの描画履歴が確認できます。 +//image[texture_viewer_pixel_history][ピクセルの描画履歴] +@{texture_viewer_pixel_history}では4つの履歴があります。 +緑色の行は深度テストなどのパイプラインのテストにすべて合格し描画されたことを示します。一部のテストに失敗し描画されなかった場合は赤色になります。 +キャプチャした画像では、画面のクリア処理とカプセルの描画が成功し、PlaneとSkyboxが深度テストに失敗しています。 diff --git a/articles/text/tuning_practice_asset.re b/articles/text/tuning_practice_asset.re new file mode 100644 index 0000000..66d67c5 --- /dev/null +++ b/articles/text/tuning_practice_asset.re @@ -0,0 +1,496 @@ +={practice_asset} Tuning Practice - Asset +ゲーム製作ではテクスチャやメッシュ、アニメーション、サウンドなどさまざまな種類のアセットを大量に扱います。 +そこで本章ではそれらのアセットに関して、パフォーマンスチューニングを行う上で気をつけるべき設定項目など、実践的な知識についてまとめます。 + +=={practice_asset_texture} Texture +テクスチャの元となる画像データはゲーム製作において欠かせない存在です。 +その一方でメモリ消費量は比較的多くなるため設定は適切に行う必要があります。 + +==={practice_asset_texture_import} インポート設定 +@{texture_settings}はUnityにおけるテクスチャのインポート設定です。 +//image[texture_settings][テクスチャ設定][scale=0.75] + +この中でもとくに注意すべきものについて紹介します。 + +==={practice_asset_texture_read_write} Read/Write +このオプションはデフォルトでは無効になっています。無効な状態であればGPUメモリにしかテクスチャは展開されません。 +有効にした場合はGPUメモリだけでなくメインメモリにもコピーされるため、2倍の消費量になってしまいます。 +そのため@{Texture.GetPixel}や@{Texture.SetPixel}といったAPIを使用せずShaderでしかアクセスしない場合、必ず無効にしましょう。 + +またランタイムで生成したテクスチャは、@{makeNoLongerReadable}で示すようにmakeNoLongerReadableをtrueに設定することで、メインメモリへのコピーを回避できます。 +//listnum[makeNoLongerReadable][makeNoLongerReadableの設定][csharp]{ +texture2D.Apply(updateMipmaps, makeNoLongerReadable: true) +//} + +//info{ +GPUメモリからメインメモリへのテクスチャ転送は時間がかかるため、読み取り可能な場合は、どちらにもテクスチャを展開することでパフォーマンスの向上が図られています。 +//} + +==={practice_asset_texture_mipmap} Generate Mip Maps +Mip Mapの設定を有効にするとテクスチャのメモリ使用量が約1.3倍になります。 +この設定は3D上のオブジェクトに対して行うことが一般的で、遠くのオブジェクトに対して、ジャギ軽減やテクスチャ転送量削減を目的に設定します。 +2DスプライトやUI画像に対しては基本的に不要なので、無効にしておきましょう。 + +==={practice_asset_texture_aniso} Aniso Level +Aniso Levelはオブジェクトを浅い角度で描画した際に、テクスチャの見栄えをボケずに描画するための機能です。 +この機能は主に地面や床など遠くまで続いているオブジェクトに使用されます。 +Aniso Level値が高いほどその恩恵を受けられますが、処理コストはかさみます。 +//image[aniso_level_compare][Aniso Level適応イメージ][scale=1.0] + +設定値は0~16までありますが少し特殊な仕様となっています。 + + * 0: プロジェクト設定によらず必ず無効 + * 1: 基本的には無効。ただし、プロジェクト設定がForced Onの場合は、9~16の値にクランプされる + * それ以外: その値での設定 + +テクスチャをインポートするとデフォルトでは値が1になっています。 +そのためハイスペック端末が対象でない限りForced Onの設定は推奨できません。 +Forced Onの設定は「Project Settings -> Quality」のAnisotropic Texturesから設定可能です。 + +//image[project_settings_aniso][Forced Onの設定] + +Aniso Levelの設定が効果のないオブジェクトで有効になっていないか、もしくは効果のあるオブジェクトに対して無闇に高い設定値になっていないかを確認しましょう。 + +//info{ +Aniso Levelによる効果は、線形ではなく段階で切り替わっている挙動になっています。 +筆者が検証した結果では、0~1、2〜3、4~7、8以降という4段階で変化していました。 +//} + +==={practice_asset_texture_compress} 圧縮設定 +テクスチャは特別な理由がない限り圧縮するべきでしょう。 +もしプロジェクト内に無圧縮なテクスチャが存在した場合、ヒューマンエラーかレギュレーションがない可能性があります。早急に確認しましょう。 +圧縮設定に関する詳細は@{basic|basic_asset_data_compression}にてご確認ください。 + +このような圧縮設定に関してはヒューマンエラーが起きないようにTextureImporterを利用し自動化することを推奨します。 +//listnum[textureImporter][TextureImporterの自動化例][csharp]{ +using UnityEditor; + +public class ImporterExample : AssetPostprocessor +{ + private void OnPreprocessTexture() + { + var importer = assetImporter as TextureImporter; + importer.isReadable = false; // Read/Writeの設定なども可能 + + var settings = new TextureImporterPlatformSettings(); + // Android = "Android", PC = "Standalone"を指定 + settings.name = "iPhone"; + settings.overridden = true; + settings.textureCompression = TextureImporterCompression.Compressed; + settings.format = TextureImporterFormat.ASTC_6x6; // 圧縮形式を指定 + importer.SetPlatformTextureSettings(settings); + } +} +//} + +またすべてのテクスチャが同じ圧縮形式である必要はありません。 +たとえばUI画像の中でも全体的にグラデーションがかかっている画像は、圧縮による品質低下が目立ちやすいです。 +そのような場合は対象となる一部の画像だけ、圧縮率を低めに設定すると良いでしょう。 +逆に3Dモデルなどのテクスチャは品質低下がわかりにくいため、圧縮率を高くするなど適切な設定を探るのがよいでしょう。 + +=={practice_asset_mesh} Mesh +Unityにインポートしたメッシュ(モデル)を扱う場合の注意点を紹介します。インポートしたモデルデータは設定次第でパフォーマンスは上がります。注意すべきポイントは次の4つです。 + + * Read/Write Enabled + * Vertex Compression + * Mesh Compression + * Optimize Mesh Data + +==={practice_asset_mesh_read_write_enabled} Read/Write Enabled +Meshの注意点1つ目はRead/Write Enabledです。 +モデルのインスペクターにあるこのオプションはデフォルトでは無効になっています。 +//image[practice_asset_mesh_inspector][Read/Write設定] +ランタイム中、メッシュにアクセスする必要がなければ無効にしておきましょう。具体的にはモデルをUnity上に配置して、AnimationClipを再生するくらいの使い方であれば、Read/Write Enabledは無効で問題ありません。 + +有効にすると、CPUでアクセス可能な情報をメモリに保持するためメモリを2倍消費します。無効にするだけでメモリの節約になるためぜひ確認してみてください。 + +==={practice_asset_vertex_compression} Vertex Compression +Vertex Compressionはメッシュの頂点情報の精度をfloatからhalfに変更するオプションです。 +これによって@{ランタイム時のメモリ使用量とファイルサイズを小さくすることが可能}です。 +「Project Settings -> Player」のOtherで設定ができ、デフォルト設定では次のようになっています。 +//image[practice_asset_vertex_compression_settings][Vertex Compressionのデフォルト設定] + +ただし、このVertex Compressionは次のような条件に当てはまると無効化されるので気をつけましょう。 + + * Read/Writeが有効 + * Mesh Compressionが有効 + * Dynamic Batchingが有効かつ適応可能なメッシュ(頂点数が300以下、頂点属性が900以下) + +==={practice_asset_mesh_compression} Mesh Compression +Mesh Compressionはメッシュの圧縮率を変更できます。圧縮率が高いほどファイルサイズが小さくなりストレージの占める割合が削減されます。 +圧縮されたデータは実行時に展開されます。そのため@{ランタイム時のメモリ使用量には影響がありません}。 + +Mesh Compresssionの圧縮設定には4つの選択肢があります。 + + * Off: 非圧縮 + * Low: 低圧縮 + * Medium: 中程度の圧縮 + * High: 高圧縮 + +//image[practice_asset_mesh_compression][Mesh Compression] + +@{practice_asset_vertex_compression}で触れましたが、このオプションを有効にすると@{Vertex Compressionが無効化されます}。 +とくにメモリ使用量の制限が厳しいプロジェクトでは、このデメリットを把握した上で設定をしてください。 + +==={practice_asset_optimize_mesh_data} Optimize Mesh Data +Optimize Mesh Dataはメッシュに不要な頂点データを自動で削除する機能です。 +不要な頂点データの判定には使用しているShaderから自動判定されます。 +これはランタイム時のメモリ、ストレージともに削減効果があります。 +「Project Settings -> Player」のOtherで設定が可能です。 +//image[practice_asset_optimize_mesh_data][Optimize Mesh Dataの設定][scale=0.8] + +ただしこのオプションは頂点データが自動削除されるので便利ですが、予期せぬ不具合を引き起こす可能性があるので注意しましょう。 +たとえばランタイムでMaterialやShaderを切り替えた際、アクセスしたプロパティが削除されており描画結果がおかしくなることがあります。 +他にもMeshのみをアセットバンドル化する際、Materialの設定が正しくなかった場合に不要な頂点データと判定されることもあります。 +これはParticle SystemのようなMeshの参照だけを持たせる場合にありがちです。 + +=={practice_asset_material} Material +Material(マテリアル)はオブジェクトをどのように描画するか決める重要な機能です。 +身近な機能ですが、使い方を誤ると簡単にメモリリークしてしまいます。 +本節では安全なマテリアルを使用する方法を紹介します。 + +===={practice_asset_material_clone} パラメーターにアクセスするだけで複製される +マテリアルの最大の注意点は、パラメーターにアクセスするだけで複製されてしまうことです。そして複製されていることに気づきづらいことです。 + +次のコードを見てみましょう。 +//listnum[material_clone][Materialの複製される例][csharp]{ +Material material; + +void Awake() +{ + material = renderer.material; + material.color = Color.green; +} +//} + +マテリアルのcolorプロパティにColor.greenをセットしているだけの簡単な処理です。このrendererのマテリアルは複製されています。そして複製されたオブジェクトは明示的にDestroyする必要があります。 +//listnum[material_destroy][複製されたMaterialの削除例][csharp]{ +Material material; + +void Awake() +{ + material = renderer.material; + material.color = Color.green; +} + +void OnDestroy() +{ + if (material != null) + { + Destroy(material) + } +} +//} +このように複製されたマテリアルをDestroyすることでメモリリークを回避できます。 + +===={practice_asset_material_instantiate} 生成したマテリアルの掃除を徹底しよう +動的に生成したマテリアルもメモリリークの原因になりやすいです。生成したマテリアルも使い終わったら確実にDestroyしましょう。 + +次のサンプルコードを見てみましょう。 +//listnum[material_dynamic][動的に生成したMaterialの削除例][csharp]{ +Material material; + +void Awake() +{ + material = new Material(); // マテリアルを動的生成 +} + +void OnDestroy() +{ + if (material != null) + { + Destroy(material); // 使い終わったマテリアルをDestroy + } +} +//} +生成したマテリアルは使い終わったタイミング(OnDestroy)でDestroyしましょう。プロジェクトのルールや仕様に合わせて、適切なタイミングでマテリアルはDestroyしましょう。 + +=={practice_asset_animation} Animation +アニメーションは2D、3D問わず多く使用されるアセットです。 +本節ではAnimation ClipやAnimatorに関するプラクティスを紹介します。 + +==={practice_asset_animation_influence} スキンウェイト数の調整 +モーションは内部的にはそれぞれの頂点がどの骨からどれぐらい影響を受けているかを計算して位置を更新しています。 +この位置計算に加味する骨の数をスキンウェイト数、またはインフルエンス数と呼びます。 +そのためスキンウェイト数を調整することで負荷削減ができます。 +ただしスキンウェイト数を減らすと見た目がおかしくなる可能性があるので調整する際には検証しましょう。 + +スキンウェイト数は「Project Settings -> Quality」のOtherから設定が可能です。 +//image[practice_asset_animation_influence][スキンウェイトの調整] +この設定はスクリプトから動的に調整することも可能です。 +そのため低スペック端末はSkin Weightsを2に設定し、高スペック端末は4に設定するなどの微調整が可能です。 +//listnum[skinweight_settings][SkinWeightの設定変更][csharp]{ +// QualitySettingsを丸ごと切り替える方法 +// 引数の番号は設定画面のLevelsの並び順で上から0、1..となっています +QualitySettings.SetQualityLevel(0); + +// SkinWeightsだけ変更する方法 +QualitySettings.skinWeights = SkinWeights.TwoBones; +//} + +==={practice_asset_animation_key_frame_reduction} キーの削減 +アニメーションファイルはキーの数に依存してストレージとランタイム時のメモリを圧迫します。 +キー数を削減する方法の1つとしてAnim. Compressionという機能があります。 +このオプションはモデルのインポート設定からアニメーションタブを選択することで表示されます。 +Anim. Compressionを有効にするとアセットインポート時に不要なキーが自動で削除されます。 +//image[practice_asset_key_frame_reduction][Anim. Compression設定画面] +Keyframe Reductionは値の変化が少ない場合にキーが削減されます。 +具体的には直前の曲線と比較して誤差(Error)範囲内に収まっていた場合に削除されます。 +この誤差範囲は調整することが可能です。 +//image[practice_asset_animation_error][Errorの設定] +少しややこしいですがErrorの設定は項目によって値の単位が違います。 +Rotationは角度、PositionとScaleがパーセントです。 +キャプチャした画像の許容誤差はRotationが0.5度、PositionとScaleは0.5%となります。 +詳しいアルゴリズムはUnityのドキュメント@{animation_error_tolerance}にあるので気になる方は覗いてみてください。 +//footnote[animation_error_tolerance][@{https://docs.unity3d.com/Manual/class-AnimationClip.html#tolerance}] + +Optimalはさらにわかりにくいのですが、Dense Curveというフォーマットと、Keyframe Reductionの2つの削減方法を比較し、データが小さくなる方を採用します。 +押さえておくべきポイントとしては、DenseはKeyframe Reductionと比べるとサイズは小さくなります。 +ただしノイジーになりやすいためアニメーションクオリティが低下する可能性があります。 +この特性を把握した上で、あとは実際のアニメーションを目視して許容できるか確認していきましょう。 + +==={practice_asset_animation_update_reduction} 更新頻度の削減 +Animatorはデフォルト設定では画面に映っていなくても毎フレーム更新を行います。 +この更新方法を変更できるカリングモードというオプションがあります。 +//image[practice_asset_animator_cull][カリングモード] + +それぞれのオプションの意味は次のようになります。 +//table[animator_culling_mode][カリングモードの説明]{ +種類 意味 +-------------------- +Always Animate 画面外にいても常に更新を行います。(デフォルト設定) +Cull Update Transform 画面外にいるときにIKやTransformの書き込みを行ないません。@
{}ステートマシンの更新は行います。 +Cull Completely 画面外にいるとステートマシンの更新を行ないません。@
{}アニメーションが完全に止まります。 +//} +それぞれのオプションについて注意点があります。 +まずCull Completelyを設定する場合、Rootモーションを利用している際は注意が必要です。 +たとえば画面外からフレームインするようなアニメーションの場合、画面外にいるためアニメーションは即座に停止されます。 +その結果いつまでたってもフレームインしなくなります。 + +次にCull Update Transformです。 +これはTransformの更新がスキップされるだけなので、とても使い勝手のよいオプションのように感じます。 +しかし揺れものといったTransformに依存した処理がある場合は注意が必要です。 +たとえばキャラクターがフレームアウトすると、そのタイミングのポーズから更新がされなくなります。 +そして再びフレームインした際に新たなポーズに更新されるため、その弾みで揺れものが大きく動く可能性があります。 +各オプションの一長一短を把握した上で設定を変更するとよいでしょう。 + +また、これらの設定を用いても動的にアニメーションの更新頻度を細かく変更することはできません。 +たとえばカメラから距離が離れているオブジェクトのアニメーションの更新頻度を半分にするなどの最適化です。 +その場合はAnimationClipPlayableを利用するか、Animatorを非アクティブにしたうえで自身でAnimator.Updateを呼ぶ必要があります。 +どちらも自前でスクリプトを書く必要がありますが、前者に比べ後者の方が簡単に導入できるでしょう。 + +=={practice_asset_particle} Particle System +ゲームエフェクトはゲーム演出に欠かせません。UnityではParticle Systemをよく使います。 +本章ではパフォーマンスチューニング観点で、Particle Systemの失敗しない使い方、注意点について紹介します。 + +大事なことは次の2点です。 + + * パーティクルの個数を抑える + * ノイズは重いので注意する + +==={practice_asset_emit_count} パーティクルの個数を抑える +パーティクルの個数は負荷につながります。Particle SystemはCPUで動作するパーティクル(CPUパーティクル)のため、パーティクルの個数が多ければ多いほどCPU負荷は上がります。 +基本方針として必要最低限のパーティクル数に設定しましょう。必要に応じて個数を調整してみてください。 + +パーティクルの個数を制限する方法は2つです。 + + * Emissionモジュールの放出個数の制限 + * メインモジュールのMax Particlesで最大放出個数の制限 + +//image[practice_asset_emit_count][Emissionモジュールで放出個数の制限][scale=1.0] + + * Rate over Time: 毎秒放出する個数 + * Bursts > Count: バーストタイミングで放出する個数 + +これらの設定を調整して、必要最低限のパーティクル数になるよう設定してください。 + +//image[practice_asset_max_particles][Max Particlesで放出個数の制限][scale=1.0] +もう1つの方法はメインモジュールのMax Particlesです。上の例では1000個以上のパーティクルは放出されなくなります。 + +===={practice_asset_subemitters} Sub Emittersにも注意 +パーティクルの個数を抑える上で、Sub Emittersモジュールも注意が必要です。 +//image[practice_asset_subemitters][Sub Emittersモジュール][scale=1.0] +Sub Emittersモジュールは特定のタイミング(生成時、寿命が尽きた時など)で任意のParticle Systemを生成します。Sub Emittersの設定によっては、一気にピーク数まで到達してしまうため使用の際には注意しましょう。 + +==={practice_asset_noise} ノイズは重いので注意 +//image[practice_asset_noise][Noiseモジュール][scale=1.0] + +Noise(ノイズ)モジュールのQualityは負荷が上がりやすいです。 +ノイズは有機的なパーティクルを表現可能で、お手軽にエフェクトのクオリティをあげられるためよく使われます。 +よく使う機能だからこそパフォーマンスには気をつかいたい所です。 + +//image[practice_asset_noise_quality][NoiseモジュールのQuality][scale=1.0] + + * Low(1D) + * Midium(2D) + * High(3D) + +Qualityは次元が上がるほど負荷は上がります。もしNoiseが不要であればNoiseモジュールをオフにしましょう。また、ノイズを使う必要があれば、Qualityの設定はLowを優先し、要求に応じてQualityをあげていきましょう。 + +=={practice_asset_audio} Audio +サウンドファイルをインポートしたデフォルト状態はパフォーマンス的には改善ポイントがあります。設定項目は次の3点です。 + + * Load Type + * Compression Format + * Force To Mono + +これらをゲーム開発でよく使うBGM、効果音、ボイスで適切な設定にしましょう。 + +==={practice_asset_audio_load_type} Load Type +サウンドファイル(AudioClip)をロードする方法は3種類あります。 +//image[practice_asset_audio_load_type][AudioClip LoadType][scale=1.0] + + * Decompress On Load + * Compressed In Memory + * Streaming + +===={practice_asset_audio_decomplress_on_load} Decompress On Load +Decompress On Loadは、非圧縮でメモリにロードします。 +CPU負荷が低いため待機時間が小さく再生されます。 +その反面、メモリを多く使用します。 + +尺が短くすぐに再生してほしい効果音にオススメです。 +BGMや尺の長いボイスファイルでの使用は、メモリを多く使用してしまうため注意が必要です。 + +===={practice_asset_audio_complress_in_memory} Compressed In Memory +Compressed In Memoryは、AudioClipを圧縮した状態でメモリにロードします。 +再生するタイミングで展開するということです。 +つまりCPU負荷が高く、再生遅延が起きやすいです。 + +ファイルサイズが大きく、メモリにそのまま展開したくないサウンドや、多少の再生遅延に問題ないサウンドが適しています。 +ボイスで使うことが多いです。 + +===={practice_asset_audiostreaming} Streaming +Streamingは、その名の通りロードしながら再生する方式です。 +メモリ使用量は少ない代わりにCPU負荷が高くなります。 +尺の長いBGMでの使用がオススメです。 + + +//table[loadtype][ロード方法と主な使用用途まとめ]{ +種類 用途 +-------------------- +Decompress On Load 効果音 +Compressed In Memory ボイス +Streaming BGM +//} + +==={practice_asset_audio_compression_format} Compression Format +Compression formatとは、AudioClip自体の圧縮フォーマットです。 + +//image[practice_asset_audio_compression_format][AudioClip Compression Format][scale=1.0] + +===={practice_asset_pcm} PCM +非圧縮で、メモリを大量に消費します。音質によほどクオリティを求めない限り設定することはありません。 + +===={practice_asset_adpcm} ADPCM +PCMに比べてメモリ使用量は70%減りますが、クオリティは低くなります。CPU負荷がVorbisと比較して格段に小さいのが特徴です。 +つまり展開スピードが速いため、即時再生や大量に再生するサウンドに適しています。 +具体的には、足音や衝突音、武器などのノイズを多く含む且つ、大量に再生する必要のあるサウンドです。 + +===={practice_asset_vorbis} Vorbis +非可逆圧縮フォーマットのため、PCMよりクオリティは下がりますが、ファイルサイズは小さくなります。唯一Qualityを設定できるため、微調整も可能です。 +全サウンド(BGM、効果音、ボイス)でもっとも使われる圧縮形式です。 + +//table[compressionformat][圧縮方法と主な使用用途まとめ]{ +種類 用途 +-------------------- +PCM 使用しない +ADPCM 効果音 +Vorbis BGM、効果音、ボイス +//} + +==={practice_asset_sample_rate} サンプルレートの指定 +サンプルレートを指定してクオリティの調節できます。全圧縮フォーマットに対応しています。 +Sample Rate Settingから3種類の方法を選べます。 + +//image[practice_asset_audio_sample_rate_setting][Sample Rate Settings][scale=1.0] + +===={practice_asset_preserve_sample_rate} Preserve Sample Rate +デフォルト設定です。元音源のサンプルレートが採用されます。 + +===={practice_asset_optimize_sample_rate} Optimize Sample Rate +Unity側で解析され、最高周波数の成分に基づいて自動で最適化されます。 + +===={practice_asset_override_sample_rate} Override Sample Rate +元音源のサンプルレートを上書きします。8,000〜192,000Hzまで指定可能です。元の音源より高くしても品質は上がりません。元音源よりサンプルレートを下げたい場合に使用します。 + + +==={practice_asset_audio_force_to_mono} 効果音はForce To Monoを設定 +Unityはデフォルト状態でステレオ再生しますが、Force To Monoを有効にすることでモノラル再生になります。モノラル再生を強制することで左右それぞれのデータを持たなくて良くなるため、ファイルサイズとメモリサイズは半分になります。 +//image[practice_asset_audio_force_to_mono][AudioClip Force To Mono][scale=1.0] + +効果音はモノラル再生でも問題ないことが多いです。また3Dサウンドもモノラル再生の方が良い場合もあります。検討した上でForce To Monoを有効にするとよいでしょう。 +パフォーマンスチューニング効果も塵も積もれば山となります。モノラル再生で問題ない場合は積極的にForce To Monoを活用しましょう。 + +//info{ +パフォーマンスチューニングとは話がずれますが音声ファイルは無圧縮のものをUnityに取り込みましょう。 +圧縮済みのものをインポートした場合、Unity側でデコード & 再圧縮を行うので品質の低下が発生します。 +//} + +=={practice_asset_special_folder} Resources / StreamingAssets +Unityには特別なフォルダーが存在します。とくに次の3つはパフォーマンス観点で注意が必要です。 + + * Resourcesフォルダー + * StreamingAssetsフォルダー + +通常Unityは、シーンやマテリアル、スクリプトなどから参照されたオブジェクトがビルドに含まれます。 + +//listnum[practice_asset_special_folder_script_reference][スクリプトで参照されたオブジェクトの例][csharp]{ +[SerializeField] GameObject sample; // 参照されたオブジェクトはビルドに含まれる +//} + +先の特別なフォルダーはルールが違います。格納したファイルはビルドに含まれます。つまり、リリース時に不要な不要なファイルも格納されていればビルドに含まれ、ビルドサイズの膨張につながります。 + +問題はプログラムから確認することができないということです。不要なファイルを目視で確認しなければならないので時間がかかります。これらのフォルダーには注意してファイル追加しましょう。 + +しかし、プロジェクトが進行する中で格納ファイルはどうしても増えていきます。中には使用しなくなった不要なファイルが混入することもあるでしょう。結論として、定期的な格納ファイルの見直しをオススメします。 + +==={practice_asset_resources} 起動時間を遅くするResourcesフォルダー +Resourcesフォルダーに大量のオブジェクトを格納すると、アプリの起動時間が伸びてしまいます。 +Resourcesフォルダーは文字列参照でオブジェクトをロードできる昔ながらの便利機能です。 + +//listnum[practice_asset_special_folder_resources][スクリプトで参照されたオブジェクトの例][csharp]{ +var object = Resources.Load("aa/bb/cc/obj"); +//} +このようなコードでオブジェクトをロードできます。 +Resourcesフォルダーに格納しておけば、スクリプトからオブジェクトにアクセスできるため、多用してしまいがちです。 +しかし、Resourcesフォルダーに詰め込みすぎると前述の通りアプリの起動時間が伸びます。 +原因はUnity起動時に、全Resourcesフォルダー内の構造を解析し、ルックアップテーブルを作成するからです。 +できる限りResourcesフォルダーの使用は最小限にした方がよいでしょう。 + +=={practice_asset_scriptable_object} ScriptableObject + +ScriptableObjectはYAMLのアセットで、テキスト形式としてファイル管理しているプロジェクトが多いと思われます。 +明示的に@{[PreferBinarySerialization]}Attributeを指定することで保存形式をバイナリ形式に変更できます。 +おもにデータが大量になるようなアセットの場合、バイナリ形式にすることで書き込み・読み込みのパフォーマンスが向上します。 + +ただし、当然ながらバイナリ形式の場合マージツールなどでは扱いにくくなります。アセット更新が上書きだけで済む、 +変更差分をテキストで確認する必要がないようなアセットや、ゲーム開発が完了しデータの変更が行われなくなったアセットについては +積極的に@{[PreferBinarySerialization]}を指定するとよいでしょう。 + +//info{ +ScriptableObjectを使用するにあたって陥りやすいミスは、クラス名とソースコードのファイル名の不一致です。 +クラスとファイルは同名にする必要があります。 +クラス作成時には命名に注意しつつ、@{.asset}ファイルが正しくシリアライズされ、 +バイナリ形式で保存されていることを確認しましょう。 +//} + +//listnum[scriptable_object_sample][ScriptableObjectの実装例][csharp]{ +/* +* ソースコードのファイル名がScriptableObjectSample.csのとき +*/ + +// シリアライズ成功 +[PreferBinarySerialization] +public sealed class ScriptableObjectSample : ScriptableObject +{ + ... +} + +// シリアライズ失敗 +[PreferBinarySerialization] +public sealed class MyScriptableObject : ScriptableObject +{ + ... +} +//} diff --git a/articles/text/tuning_practice_assetbundle.re b/articles/text/tuning_practice_assetbundle.re new file mode 100644 index 0000000..b99a42a --- /dev/null +++ b/articles/text/tuning_practice_assetbundle.re @@ -0,0 +1,76 @@ +={practice_assetbundle} Tuning Practice - AssetBundle + +AssetBundleの設定に問題があると、ユーザーの貴重な通信容量や記憶領域を浪費してしまうだけでなく、 +快適なゲームプレイを妨げてしまうなど多くの問題が生じてしまいます。 +この章ではAssetBundleに関する設定や実装の方針について説明します。 + +=={practice_assetbundle_granularity} AssetBundleの粒度 + +依存関係の問題で、AssetBundleをどの程度細かくするかという粒度に関しては、慎重に検討する必要があります。 +極端に言えば、すべてのアセットを1つのAssetBundleに入れる方法と、それぞれのアセットを1つずつAssetBundleにする方法があります。 +どちらもシンプルな方法ですが、前者の方法は致命的な問題があります。 +それはアセットを追加したり、1つのアセットを更新するだけだとしても、ファイル全体を作り直して配布する必要があるためです。 +アセットの総量がGB単位になる場合は、更新の負荷が非常に高くなります。 + +そのためAssetBundleをなるべく分割する方法を選択することになりますが、細かすぎてもさまざまな部分でオーバーヘッドが生じてしまいます。 +そこで基本的には以下の方針でAssetBundle化することをオススメします。 + + * 同時に使用される前提のアセットは1つのAssetBundleにまとめる + * 複数のアセットから参照されるアセットは別のAssetBundleにする + +完璧にコントロールするのは難しいですが、プロジェクト内で粒度に関する何かしらのルールを決めるとよいでしょう。 + +=={practice_assetbundle_load} AssetBundleのロードAPI + +AssetBundleからアセットをロードする際のAPIとして3種類あります。 + + : @{AssetBundle.LoadFromFile} + ストレージに存在するファイルパスを指定してロードします。 + 最速かつもっとも省メモリなため、通常はこちらを使います。 + : @{AssetBundle.LoadFromMemory} + メモリにロード済みのAssetBundleデータを指定してロードします。 + AssetBundleを使っている間は非常に大きいデータをメモリに維持する必要があり、メモリ負荷が非常に大きいです。 + そのため通常は使用しません。 + : @{AssetBundle.LoadFromStream} + AssetBundleデータを返す@{Stream}を指定してロードします。 + 暗号化されたAssetBundleの復号処理をしながらロードする場合はメモリ負荷を考慮してこちらのAPIを使います。 + ただし@{Stream}はシークできる必要があるため、シークに対応できない暗号アルゴリズムを使わないように注意する必要があります。 + +=={practice_assetbundle_unload} AssetBundleのアンロード戦略 + +AssetBundleが不要になった時点でアンロードしないとメモリを圧迫してしまいますが、 +その際に使用するAPIである@{AssetBundle.Unload(bool unloadAllLoadedObjects)}の引数@{unloadAllLoadedObjects}は、 +非常に重要なので開発の初期にどう設定するか決定する必要があります。 +この引数がtrueの場合、AssetBundleをアンロードする際に、そのAssetBundleからロードされたすべてのアセットもアンロードします。 +falseの場合はアセットはアンロードされません。 + +つまり、アセットを使っているあいだAssetBundleもロードし続けないといけないtrueの方はメモリ負荷が高い一方で、アセットの破棄も確実に行えるので安全性は高いです。 +一方でfalseの場合はアセットをロードし終わったタイミングでAssetBundleをアンロードできるのでその場のメモリ負荷は低いのですが、 +使い終わったアセットのアンロードを忘れるとメモリリークに繋がったり、同じアセットがメモリ上で複数ロードされたりするため、適切なメモリ管理が求められます。 +一般に厳密なメモリ管理はシビアになるため、メモリ負荷に余裕がある場合は@{AssetBundle.Unload(true)}を推奨します。 + +=={practice_assetbundle_loadcount} 同時にロードされているAssetBundle数の最適化 + +@{AssetBundle.Unload(true)}の場合はアセットを使用している間はAssetBundleをアンロードできません。 +そのため実際の場面では、AssetBundleの粒度次第では100以上のAssetBundleを同時にロードしている状態といった場面も出てくるかもしれません。 +この場合気をつけたいのは、ファイルディスクリプターの制限と、@{PersistentManager.Remapper}のメモリ使用量です。 + +ファイルディスクリプターとは、ファイルを読み書きする際にOS側から割り当てられる操作用のIDです。 +1つのファイルを読み書きするために1つのファイルディスクリプターが必要で、ファイル操作が完了したタイミングでファイルディスクリプターを解放します。 +このファイルディスクリプターを1つのプロセスが持てる数の上限が決まっているので、その上限以上のファイルを同時に開くことはできません。 +"Too many open files" というエラーを見かけた場合は、この上限に引っかかったことを示しています。 +そのためAssetBundleの同時にロードできる数がこの制限に影響を受け、またUnity側もある程度はファイルを開いているので制限に対して余裕を保つ必要があります。 +この制限値はOSやバージョンなどによって変化するので、ターゲットとするプラットフォームの値を事前に調査する必要がありますが、一例としてiOSやmacOSでは制限値が256のバージョンもあります。 +また仮に上限に当たったとしても、OSによっては一時的に上限を引き上げることも可能@{setrlimit}なので、必要な場合は実装を検討しましょう。 + +#@# 各プラットフォームでのデフォルトのファイルディスクリプタの制限値を調べて書く + +//footnote[setrlimit][Linux/Unix環境では、setrlimit関数を用いて実行時に制限値を変更することが可能] + +同時にロードするAssetBundleが多いことの2つ目の問題として、Unityの@{PersistentManager.Remapper}の存在があります。 +PersistentManagerは簡単に言えば、Unity内部でオブジェクトとデータのマッピング関係を管理している機能です。 +つまり同時にロードするAssetBundleの数に応じてメモリを使うことが想像できると思いますが、問題はAssetBundleを解放しても使用したメモリ領域は解放されずにプールされるということにあります。 +この性質のために同時ロード数に比例してメモリを圧迫するようになるため、同時ロード数を削減することが重要となってきます。 + +以上のことから、@{AssetBundle.Unload(true)}の方針で運用する場合は、同時にロードするAssetBundleを最大でも150〜200程度を目安に、 +@{AssetBundle.Unload(false)}の方針で運用する場合は最大でも150以下を目安とするとよいでしょう。 diff --git a/articles/text/tuning_practice_graphics.re b/articles/text/tuning_practice_graphics.re new file mode 100644 index 0000000..fbd9363 --- /dev/null +++ b/articles/text/tuning_practice_graphics.re @@ -0,0 +1,588 @@ +={practice_graphics} Tuning Practice - Graphics + +本章では、Unityのグラフィックス機能周辺のチューニング手法について紹介します。 + +=={practice_graphics_resolution} 解像度の調整 + +レンダリングパイプラインの中でもフラグメントシェーダーのコストはレンダリングする解像度に比例して増加します。 +とくに昨今のモバイルデバイスはディスプレイの解像度が高く、描画解像度を適切な値に調整する必要があります。 + +==={practice_graphics_resolution_dpi} DPIの設定 + +モバイルプラットフォームのPlayer Settingsの解像度関連の項目に含まれているResolution Scaling Modeを@{Fixed DPI}に設定すると、 +特定の@{DPI(dots per inch)}をターゲットに解像度を落とすことができます。 + +//image[fixed_dpi][Resolution Scaling Mode] + +最終的な描画解像度は、Target DPIの値とQuality Settingsに含まれるResolution Scaling DPI Scale Factorの値が乗算されて決定します。 + +//image[dpi_factor][Resolution Scaling DPI Scale Factor] + +==={practice_graphics_resolution_code} スクリプトによる解像度設定 + +スクリプトから描画解像度を動的に変更するには、@{Screen.SetResolution}を呼び出します。 + +現在の解像度は@{Screen.width}や@{Screen.height}で取得することができ、DPIは@{Screen.dpi}で取得できます。 + +//listnum[screen_setresolution][Screen.SetResolution][csharp]{ +public void SetupResolution() +{ + var factor = 0.8f; + + // Screen.width, Screen.heightで現在の解像度を取得 + var width = (int)(Screen.width * factor); + var height = (int)(Screen.height * factor); + + // 解像度を設定 + Screen.SetResolution(width, height, true); +} +//} + +//info{ + @{Screen.SetResolution}での解像度設定は実機でのみ反映されます。 + + Editorでは変更が反映されないため注意しましょう。 +//} + + +=={practice_graphics_overdraw} 半透明とオーバードロー + +半透明マテリアルの使用は@{オーバードロー}の増加の大きな原因となります。 +オーバードローとは画面の1ピクセルに対して複数回フラグメントの描画が行われることで、フラグメントシェーダーの負荷に比例してパフォーマンスに影響を与えます。 + +とくにParticle Systemなどで大量に半透明のパーティクルを発生させる場合などにオーバードローが大量に発生することが多いです。 + +オーバードローによる描画負荷の増加を軽減するには、以下のような方法が考えられます。 + + * 不要な描画領域を削減する + ** テクスチャが完全に透明な領域も描画対象になるためなるべく減らす + * オーバードローが発生する可能性のあるオブジェクトにはなるべく軽量なシェーダーを使用する + * 半透明マテリアルはなるべく使用しない + ** 不透明マテリアルで擬似的に半透明のような見た目を再現できる@{ディザリング}という手法も検討する + +Built-in Render PipelineのEditorではSceneビューのモードを@{Overdraw}に変更することでオーバードローを可視化することができ、オーバードローの調整の基準として有用です。 + +//image[overdraw][Overdrawモード] + + +//info{ + Universal Render Pipelineでは、Unity 2021.2から実装されている@{Scene Debug View Modes}によってオーバードローを可視化できます。 +//} + +=={practice_graphics_draw_call} ドローコールの削減 + +ドローコールの増加はCPU負荷にしばしば影響を与えますが、 +Unityにはドローコールの数を削減するための機能がいくつか存在します。 + +==={practice_graphics_dynamic_batching} 動的バッチング + +@{動的バッチング}は、動的なオブジェクトをランタイムでバッチングするための機能です。 +この機能を使用することで、同じマテリアルを使用している動的なオブジェクトのドローコールを統合して削減できます。 + +使用するには、Player Settingsから@{Dynamic Batching}の項目を有効にします。 + +また、Universal Render PipelineではUniversal Render Pipeline Asset内の@{Dynamic Batching}の項目を有効にする必要があります。 +ただUniversal Render PipelineではDynamic Batchingの使用は非推奨となっています。 + +//image[dynamic_batching][Dynamic Batchingの設定] + +動的バッチングはCPUコストを使用する処理であるため、オブジェクトに適用させるには多くの条件をクリアする必要があります。 +以下に主な条件を記載します。 + + * 同一のマテリアルを参照している + * MeshRendererかParticle Systemで描画を行っている + ** SkinnedMeshRendererなどの他のコンポーネントでは動的バッチングの対象とならない + * メッシュの頂点数が300以下である + * マルチパスを使用していない + * リアルタイムシャドウの影響を受けていない + +//info{ + + 動的バッチングはCPUの定常負荷に影響を与えるため、推奨されない場合があります。 + 後述する@{SRP Batcher}を使用すると動的バッチングに近い効果を得ることができます。 + +//} + +==={practice_graphics_static_batching} 静的バッチング + +@{静的バッチング}は、シーン内で動かないオブジェクトをバッチングするための機能です。 +この機能を使用することで、同一マテリアルを使用している静的なオブジェクトのドローコールを削減できます。 + +動的バッチングと同様にPlayer Settingsから@{Static Batching}を有効にすることで使用できます。 + +//image[static_batching][Static Batchingの設定] + +オブジェクトを静的バッチングの対象とするには、オブジェクトの@{staticフラグ}を有効にする必要があります。 +具体的にはstaticフラグの中の@{Batching Static}というサブフラグを有効にする必要があります。 + +//image[batching_static][Batching Static] + +静的バッチングは動的バッチングとは違いランタイムでの頂点の変換処理を行わないため低負荷で実行できます。 +しかし、バッチ処理によって結合されたメッシュ情報を保存するためメモリを多く消費することに注意が必要です。 + + +==={practice_graphics_instancing} GPUインスタンシング + +@{GPUインスタンシング}とは同一メッシュ・同一マテリアルのオブジェクトを効率的に描画するための機能です。 +草や木のように同じメッシュを複数回描画する場合のドローコールの削減が期待できます。 + +GPUインスタンシングを使用するには、マテリアルのInspectorから@{Enable Instancing}を有効にします。 + +//image[enable_instancing][Enable Instancing] + +GPUインスタンシングを使用できるシェーダーを作成するためにはいくつか専用の対応が必要となります。 +以下にBuilt-in Render PipelineでGPUインスタンシングを使用するための最低限の実装を行ったシェーダーコードの例を記載します。 + +//listnum[instancing_shader][GPUインスタンシングに対応したシェーダー]{ +Shader "SimpleInstancing" +{ + Properties + { + _Color ("Color", Color) = (1, 1, 1, 1) + } + + CGINCLUDE + + #include "UnityCG.cginc" + + struct appdata + { + float4 vertex : POSITION; + UNITY_VERTEX_INPUT_INSTANCE_ID + }; + + struct v2f + { + float4 vertex : SV_POSITION; + // フラグメントシェーダーでINSTANCED_PROPにアクセスしたい場合にのみ必要 + UNITY_VERTEX_INPUT_INSTANCE_ID + }; + + UNITY_INSTANCING_BUFFER_START(Props) + UNITY_DEFINE_INSTANCED_PROP(float4, _Color) + UNITY_INSTANCING_BUFFER_END(Props) + + v2f vert(appdata v) + { + v2f o; + + UNITY_SETUP_INSTANCE_ID(v); + + // フラグメントシェーダーでINSTANCED_PROPにアクセスしたい場合にのみ必要 + UNITY_TRANSFER_INSTANCE_ID(v, o); + + o.vertex = UnityObjectToClipPos(v.vertex); + return o; + } + + fixed4 frag(v2f i) : SV_Target + { + // フラグメントシェーダーでINSTANCED_PROPにアクセスしたい場合にのみ必要 + UNITY_SETUP_INSTANCE_ID(i); + + float4 color = UNITY_ACCESS_INSTANCED_PROP(Props, _Color); + return color; + } + + ENDCG + + SubShader + { + Tags { "RenderType"="Opaque" } + LOD 100 + + Pass + { + CGPROGRAM + #pragma vertex vert + #pragma fragment frag + #pragma multi_compile_instancing + ENDCG + } + } +} +//} + +GPUインスタンシングは同一のマテリアルを参照しているオブジェクトにのみ作用しますが、各インスタンスごとのプロパティを設定できます。 +上記のシェーダーコードのように対象のプロパティを@{UNITY_INSTANCING_BUFFER_START(Props)}と@{UNITY_INSTANCING_BUFFER_END(Props)}で囲むことで個別に変更するプロパティとして設定できます。 + +このプロパティをC#で@{MaterialPropertyBlock}のAPIを使用して変更することで個別のカラーなどのプロパティを設定できます。 +ただ、あまりに大量のインスタンスに対してMaterialPropertyBlockを使用すると、MaterialPropertyBlockでのアクセスがCPUのパフォーマンスに影響を与えることがあるので気をつけましょう。 + +==={practice_graphics_srp_batcher} SRP Batcher + +@{SRP Batcher}とは、@{Scriptable Render Pipeline}でのみ使用できる描画のCPUコストを削減するための機能です。 +この機能を使用することで、同一のシェーダーバリアントを使用する複数のシェーダーのセットパスコールをまとめて処理することができるようになります。 + +SRP Batcherを使用するには、@{Scriptable Render Pipeline Asset}のInspectorから@{SRP Batcher}の項目を有効にします。 + +//image[enable_srp_batcher][SRP Batcherの有効化] + +また、ランタイムでは以下のC#コードでSRP Batcherの有効・無効を変更できます。 + +//listnum[srp_batcher_enable][SRP Batcherの有効化][csharp]{ +GraphicsSettings.useScriptableRenderPipelineBatching = true; +//} + +シェーダーをSRP Batcherに対応させるには以下の2点の条件をクリアする必要があります。 + + 1. オブジェクトごとに定義されるビルトインのプロパティを@{UnityPerDraw}という1つのCBUFFERに定義する + 2. マテリアルごとのプロパティを@{UnityPerMaterial}という1つのCBUFFERに定義する + +UnityPerDrawに関しては、Universal Render Pipelineなどのシェーダーでは基本的にデフォルトで対応されていますが、 +UnityPerMaterialのCBUFFERの設定は自分で行う必要があります。 + +以下のように、マテリアルごとのプロパティを@{CBUFFER_START(UnityPerMaterial)}と@{CBUFFER_END}で囲みます。 + +//listnum[shader_unitypermaterial][UnityPerMaterial]{ +Properties +{ + _Color1 ("Color 1", Color) = (1,1,1,1) + _Color2 ("Color 2", Color) = (1,1,1,1) +} + +CBUFFER_START(UnityPerMaterial) + +float4 _Color1; +float4 _Color2; + +CBUFFER_END +//} + +以上の対応でSRP Batcherに対応したシェーダーを作成できますが、 +Inspectorから該当のシェーダーがSRP Batcherに対応しているかどうか確認することもできます。 + +シェーダーのInspectorの@{SRP Batcher}の項目が@{compatible}となっていたらSRP Batcherに対応しており、 +@{not compatible}となっていたら対応していないことがわかります。 + +//image[srp_batcher_compatible][SRP Batcherに対応しているシェーダー] + +=={practice_graphis_atlas} SpriteAtlas + +2DゲームやUIでは多くのスプライトを使用して画面を構築することがしばしばあります。 +そういった場合に大量のドローコールを発生させないための機能が@{SpriteAtlas}です。 + +SpriteAtlasを使用すると、複数のスプライトを1つのテクスチャとしてまとめることでドローコールが削減できます。 + +SpriteAtlasを作成するには、まずPackage Managerから@{2D Sprite}をプロジェクトにインストールする必要があります。 + +//image[2d_sprite][2D Sprite] + +インストール後、Projectビューで右クリックして「Create -> 2D -> Sprite Atlas」を選択し、SpriteAtlasのアセットを作成します。 + +//image[create_sprite_atlas][SpriteAtlasの作成] + +アトラス化するスプライトを指定するには、SpriteAtlasのInspectorから@{Objects for Packing}の項目に@{スプライトかスプライトを含むフォルダー}を設定します。 + +//image[atlas_packing][Objects for Packingの設定] + +以上の設定を行うことでビルド時やUnity Editorの再生時にアトラス化の処理が行われ、対象のスプライトの描画の際はSpriteAtlasで統合されたテクスチャが参照されるようになります。 + +以下のようなコードでSpriteAtlasから直接スプライトを取得することも可能です。 + +//listnum[load_atlas_sprite][SpriteAtlasからSpriteをロードする][csharp]{ +[SerializeField] +private SpriteAtlas atlas; + +public Sprite LoadSprite(string spriteName) +{ + // Spriteの名前を引数にしてSpriteAtlasからSpriteを取得する + var sprite = atlas.GetSprite(spriteName); + return sprite; +} +//} + +SpriteAtlasに含まれるSpriteを1つロードするとアトラス全体のテクスチャが読み込まれるため、1つだけロードするよりも多くのメモリを消費します。 +そのため、SpriteAtlasを適切に分割するなど注意して使用する必要があります。 + +//info{ + この節はSpriteAtlas V1をターゲットにして記載しています。 + SpriteAtlas V2ではアトラス化する対象のスプライトのフォルダー指定ができなかったりなど動作に大きく変更が加わる可能性があります。 +//} + +=={practice_graphics_culling} カリング + +Unityでは、最終的に画面に表示されない部分の処理を事前に省くための@{カリング}という処理が行われます。 + +==={practice_graphics_frustum_culling} 視錐台カリング + +@{視錐台カリング}とは、カメラの描画範囲となる視錐台の外側にあるオブジェクトを描画対象から省くための処理です。 +これによりカメラの範囲外のオブジェクトは描画の計算が行われないようになります。 + +視錐台カリングは何も設定せずともデフォルトで行われています。 +頂点シェーダーの負荷が高いオブジェクトなどの場合は、メッシュを適切に分割することでカリングの対象とし描画コストを下げる手法も有効です。 + +==={practice_graphics_shader_culling} 背面カリング + +@{背面カリング}とは、カメラから見えない(はずの)ポリゴンの裏側を描画から省く処理です。 +ほとんどのメッシュは閉じている(表側のポリゴンのみがカメラに映る)ため裏側は描画する必要がありません。 + +Unityではシェーダーに明記しない場合ポリゴンの背面がカリングの対象となっていますが、明記することでカリングの設定を切り替えることが可能です。 +SubShader内に以下のように記述します。 + +//listnum[shader_cull][カリングの設定]{ +SubShader +{ + Tags { "RenderType"="Opaque" } + LOD 100 + + Cull Back // Front, Off + + Pass + { + CGPROGRAM + #pragma vertex vert + #pragma fragment frag + ENDCG + } +} +//} + +@{Back}、@{Front}、@{Off}の3つの設定がありますが、それぞれの効果は以下のようになっています。 + + * @{Back} - 視点と反対側のポリゴンを描画しない + * @{Front} - 視点と同じ方向のポリゴンを描画しない + * @{Off} - 背面カリングを無効化し、すべての面を描画する + +==={practice_graphics_occlusion_culling} オクルージョンカリング + +@{オクルージョンカリング}とは、オブジェクトに遮蔽されてカメラに映らないオブジェクトを描画対象から省く処理です。 +この機能は事前にベイクした遮蔽判定用のデータをもとにランタイムでオブジェクトが遮蔽されているか判定し、遮蔽されているオブジェクトを描画対象から外します。 + +オブジェクトをオクルージョンカリングの対象とするには、Inspectorのstaticフラグから@{Occluder Static}または@{Occludee Static}を有効にします。 +Occluder Staticを無効にしOccludee Staticを有効にした場合はオブジェクトを遮蔽する側としては判定されなくなり、@{遮蔽される側のみ}として処理が行われるようになります。 +逆の場合は遮蔽される側として判定されなくなり、 @{遮蔽する側のみ}として処理されます。 + +//image[occlusion_static][オクルージョンカリングのstaticフラグ] + +オクルージョンカリング用の事前ベイクを行うため、@{Occlusion Cullingウィンドウ}を表示します。 +このウィンドウでは各オブジェクトのstaticフラグの変更やベイクの設定変更などを行うことができ、@{Bakeボタン}を押すことでベイクを行うことができます。 + +//image[occlusion_window][Occlusion Cullingウィンドウ] + +オクルージョンカリングは描画コストを削減する代わりにカリング処理のためにCPUへ負荷をかけるため、各負荷のバランスを見て適切に設定を行う必要があります。 + +//info{ + オクルージョンカリングで削減されるのはオブジェクトの描画処理のみで、リアルタイムシャドウの描画などの処理はそのまま行われます。 +//} + + +=={practice_graphics_shader} シェーダー +シェーダーはグラフィックス表現に非常に有効ですが、しばしばパフォーマンスの問題を引き起こします。 + +==={practice_graphics_shader_float} 浮動小数点数型の精度を下げる +GPU(とくにモバイルプラットフォーム)の計算速度は大きいデータ型より小さいデータ型のほうが速くなります。 +そのため、置き換え可能な場合は浮動小数点数型を@{float型(32bit)}から@{half型(16bit)}に置き換えることが有効です。 + +深度計算など精度が必要な場合はfloat型を使うべきですが、Colorの計算などでは精度を落としてしまっても結果的な見た目に大きな差異は起こりづらいです。 + +==={practice_graphics_shader_vertex} 頂点シェーダーで計算を行う +頂点シェーダーの処理はメッシュの頂点数分だけ実行され、フラグメントシェーダーの処理は最終的に書き込まれるピクセル数分実行されます。 +一般的に頂点シェーダーの実行回数はフラグメントシェーダーよりも少ないことが多く、そのため複雑な計算は可能な限り頂点シェーダーで行うのが良いでしょう。 + +頂点シェーダーの計算結果はシェーダーセマンティクスを介してフラグメントシェーダーに渡されますが、ここで渡される値は補間されたものであり、フラグメントシェーダーで計算した場合と見た目が違う可能性に注意する必要があります。 + +//listnum[shader_vertex_factor][頂点シェーダーによる事前計算]{ +CGPROGRAM +#pragma vertex vert +#pragma fragment frag + +#include "UnityCG.cginc" + +struct appdata +{ + float4 vertex : POSITION; + float2 uv : TEXCOORD0; +}; + +struct v2f +{ + float2 uv : TEXCOORD0; + float3 factor : TEXCOORD1; + float4 vertex : SV_POSITION; +}; + +sampler2D _MainTex; +float4 _MainTex_ST; + +v2f vert (appdata v) +{ + v2f o; + o.vertex = UnityObjectToClipPos(v.vertex); + o.uv = TRANSFORM_TEX(v.uv, _MainTex); + + // 複雑な事前計算を行う + o.factor = CalculateFactor(); + + return o; +} + +fixed4 frag (v2f i) : SV_Target +{ + fixed4 col = tex2D(_MainTex, i.uv); + + // 頂点シェーダーで計算した値をフラグメントシェーダーで使用する + col *= i.factor; + + return col; +} +ENDCG +//} + +==={practice_graphis_shader_tex} テクスチャに情報を事前に仕込む +シェーダー内の複雑な計算の結果がもし外部の値によって変化しないのであれば、テクスチャの要素として事前計算した結果を格納しておくことも有効な手段です。 + +方法としては、Unityで専用のテクスチャを生成するツールを実装したり各種DCCツールの拡張機能として実装するなどが考えられます。 +すでに使用しているテクスチャのアルファチャンネルがもし使用されていなければそこに書き込んだり、専用のテクスチャを用意するのが良いでしょう。 + +たとえば、カラーグレーディングに使用される@{LUT(色対応表)}は、各ピクセルの座標が各カラーに対応するテクスチャに対して事前に色調補正をかけます。 +そのテクスチャをシェーダー内で元のカラーをもとにサンプリングすることで、事前にかけた色調補正をもとのカラーに対してかけたのとほぼ同一の結果を得ることができます。 + +//image[lut-texture-1024][色調補正前のLUTテクスチャ(1024x32)] + +==={practice_graphics_variant_collection} ShaderVariantCollection +@{ShaderVariantCollection}を使用すると、シェーダーを使用する前にコンパイルしスパイクを防ぐことができます。 + +ShaderVariantColletionではゲーム内で使用するシェーダーバリアントのリストをアセットとして保持できます。 +Projectビューから「Create -> Shader -> Shader Variant Collection」を選択して作成します。 + +//image[create-shadervariant][ShaderVariantCollectionの作成] + +作成したShaderVariantCollectionのInspectorビューからAdd Shaderを押下して対象のシェーダーを追加し、さらにシェーダーに対してどのバリアントを追加するかを選択します。 + +//image[shadervariant-inspector][ShaderVariantCollectionのInspector] + +ShaderVariantCollectionをGraphics Settingsの@{Shader preloading}の項目内の@{Preloaded Shaders}に追加することで、 +アプリケーションの起動時にコンパイルするシェーダーバリアントを設定できます。 + +//image[preloaded-shaders][Preloaded Shaders] + +また、スクリプトから@{ShaderVariantCollection.WarmUp()}を呼び出すことで該当のShaderVariantCollectionに含まれるシェーダーバリアントを明示的に事前コンパイルすることも可能です。 + +//listnum[warmup_shader_variant][ShaderVariantCollection.WarmUp]{ +public void PreloadShaderVariants(ShaderVariantCollection collection) +{ + // 明示的にシェーダーバリアントを事前コンパイルする + if (!collection.isWarmedUp) + { + collection.WarmUp(); + } +} +//} + +=={practice_graphics_lighting} ライティング +ライティングはゲームのアート表現において非常に重要な要素の1つですが、パフォーマンスに大きく影響を与えることが多いです。 + +==={practice_graphics_shadow} リアルタイムシャドウ +リアルタイムシャドウの生成はドローコールやフィルレートを大きく消費します。 +そのため、リアルタイムシャドウを使用する際は慎重に設定を検討する必要があります。 + +===={practice_graphics_shadow_call} ドローコールの削減 +シャドウ生成のドローコールを削減するためには、以下のような方針が考えられます。 + + * シャドウを落とすオブジェクトの数を減らす + * バッチングによりドローコールをまとめる + +シャドウを落とすオブジェクトを減らす方法はいくつかありますが、シンプルな方法はMeshRendererの@{Cast Shadows}の設定をオフにすることです。 +これによりオブジェクトをシャドウの描画対象から外すことができます。 +この設定は通常Unityではオンになっているため、シャドウを使用しているプロジェクトでは注意するべきでしょう。 + +//image[mesh-castshadow][Cast Shadows] + +また、オブジェクトがシャドウマップに描画される最大距離を短くすることも有効です。 +Quality Settingsの@{Shadow Distance}の項目でシャドウマップの最大描画距離を変更することができ、この設定によりシャドウを落とすオブジェクトの数を必要最低限に減らすことができます。 +この設定を調整することでシャドウマップの解像度に対して最低限の範囲でシャドウを描画することができるため、シャドウの解像感の低下を抑えることにも繋がります。 + +//image[shadow-distance][Shadow Distance] + +通常の描画と同様、シャドウ描画でもバッチング処理の対象にすることでドローコールを削減できます。 +バッチングの手法については@{practice_graphics_draw_call}を参照してください。 + + +===={practice_graphics_shadow_fill} フィルレートの節約 +シャドウによるフィルレートはシャドウマップの描画とシャドウの影響を受けるオブジェクトの描画の両方に左右されます。 + +Quality SettingsのShadowsの項目にあるいくつかの設定を調整することでそれぞれのフィルレートを節約できます。 + +//image[quality-shadow][Quality Settings -> Shadows] + +@{Shadows}の項目ではシャドウの形式を変更することができ、@{Hard Shadows}は影の境界線がはっきりと出ますが比較的負荷が低く、@{Soft Shadows}は影の境界線をぼかしたような表現ができますが負荷が高いです。 + +@{Shadow Resolution}と@{Shadow Cascades}の項目はシャドウマップの解像度に影響する項目で、大きく設定するとシャドウマップの解像度が大きくなりフィルレートの消費が大きくなります。 +ただ、この設定はシャドウの品質に大きく関係するところでもあるため、パフォーマンスと品質のバランスを見ながら慎重に調整する必要があります。 + +一部の設定は@{Light}コンポーネントのInspectorから変更できるため、個別のライトごとに設定を変えることも可能です。 + +//image[light-component][Lightコンポーネントのシャドウ設定] + +===={practice_graphics_fake_shadow} 疑似シャドウ +ゲームジャンルやアートスタイルによっては、板ポリゴンなどでオブジェクトの影を擬似的に表現する手法も有効です。 +この手法は使用上の制約が強く自由度が高いものではありませんが、通常のリアルタイムシャドウの描画手法に比べて圧倒的に軽量に描画できます。 + +//image[fake-shadow][板ポリゴンによる擬似シャドウ] + +==={practice_graphics_lightmap} ライトマッピング +事前にライティングの効果とシャドウをテクスチャにベイクしておくことで、リアルタイム生成よりもかなり低負荷に品質の高いライティング表現を実現できます。 + +ライトマップをベイクするには、まずシーンに配置したLightコンポーネントの@{Mode}の項目を@{Mixed}か@{Baked}に変更します。 + +//image[light-mixed][LightのMode設定] + +また、ベイク対象となるオブジェクトのstaticフラグを有効化します。 + +//image[object-static][staticの有効化] + +この状態で、メニューから「Window -> Rendering -> Lighting」を選択しLightingビューを表示します。 + +デフォルトの状態だと@{Lighting Settings}アセットが指定されていないため、@{New Lighting Settings}ボタンを押下して新規作成を行います。 + +//image[new-lighting-settings][New Lighting Settings] + +ライトマップについての設定は主に@{Lightmapping Settings}タブで行います。 + +//image[lightmapping-settings][Lightmapping Settings] + +多くの設定項目がありますが、これらの値を調整することでライトマップのベイクの速度や品質が変わります。 +そのため、求める速度や品質に合わせて適切に設定する必要があります。 + +これらの設定の中でもっともパフォーマンスへの影響が大きいのは@{Lightmap Resolution}です。 +この設定はUnityにおける1unitあたりにライトマップのテクセルをどれだけ割り当てるかという設定で、この値により最終的なライトマップのサイズが変動するため、ストレージやメモリの容量、テクスチャへのアクセス速度などに大きな影響を与えます。 + +//image[lightmap-resolution][Lightmap Resolution] + +最後に、Inspectorビューの下部にある@{Generate Lighting}ボタンを押下することでライトマップのベイクを行うことができます。 +ベイクが完了すると、シーンと同名のフォルダーにベイクされたライトマップが格納されていることを確認できます。 + +//image[generate-lighting][Generate Lighting] + +//image[baked-lightmaps][ベイクされたライトマップ] + +=={practice_graphics_lod} Level of Detail +カメラから遠い距離にあるオブジェクトをハイポリゴン・高精細に描画するのは非効率です。 +@{Level of Detail(LOD)}という手法を利用することでカメラからの距離に応じてオブジェクトの詳細度を削減できます。 + +Unityでは、オブジェクトに@{LOD Group}コンポーネントを追加することでLODの制御を行うことができます。 + +//image[lod-group][LOD Group] + +LOD GroupがアタッチされたGameObjectの子に各LODレベルのメッシュを持ったRendererを配置し、LOD Groupの各LODレベルに設定することでカメラに応じてLODレベルが切り替えられるようになります。 +カメラの距離に対してどのLODレベルを割り当てるかをLOD Groupごとに設定することも可能です。 + +LODを使用することで一般に描画の負荷を削減できますが、各LODレベルのメッシュがすべてロードされるためメモリやストレージの圧迫には注意が必要です。 + +=={practice_graphics_texture_sreaming} テクスチャストリーミング +Unityの@{テクスチャストリーミング}を利用することで、テクスチャのために必要なメモリ容量やロード時間を削減できます。 +テクスチャストリーミングとは、シーンのカメラ位置に応じてミップマップをロードすることでGPUメモリを節約するための機能です。 + +この機能を有効にするには、Quality Settingsの@{Texture Streaming}を有効化します。 + +//image[texture-streaming][Texture Streaming] + +さらに、テクスチャのミップマップをストリーミングできるようにするためテクスチャのインポート設定を変更する必要があります。 +テクスチャのInspectorを開き、@{Advanced}設定内の@{Streaming Mipmaps}を有効化します。 + +//image[streaming-mip][Streaming Mipmaps] + +これらの設定により指定されたテクスチャのミップマップがストリーミングされるようになります。 +またQuality Settingsの@{Memory Budget}の項目を調整することでロードするテクスチャの合計メモリ使用量を制限できます。 +テクスチャストリーミングシステムはここで設定したメモリ量を超えないようにミップマップをロードします。 diff --git a/articles/text/tuning_practice_physics.re b/articles/text/tuning_practice_physics.re new file mode 100644 index 0000000..1d22389 --- /dev/null +++ b/articles/text/tuning_practice_physics.re @@ -0,0 +1,246 @@ +={practice_physics} Tuning Practice - Physics + +本章では、Physics(物理演算)の最適化について紹介します。 + +ここでPhysicsとはPhysXによる物理演算を指します。ECSのUnity Physicsは扱っていません。 + +また本章では、3D Physicsをメインで取り上げていますが、2D Physicsでも参考になる箇所は多いでしょう。 + +=={practice_physics_you_need_physics} 物理演算のオン・オフ + +Unity標準では、たとえシーン上に1つも物理演算に関するコンポーネントが配置されていなかったとしても、物理エンジンによる物理演算の処理は毎フレーム必ず実行されます。 +そのため、ゲーム内で物理演算を必要としない場合は、物理エンジンをオフにしておくとよいでしょう。 + +物理エンジンの処理は、@{Physics.autoSimulation}に値を設定することでオン・オフを切り替えることができます。 +たとえば、インゲーム中のみ物理演算を用いて、それ以外では利用しない場合は、インゲーム中のみこの値を@{true}に設定しておくとよいでしょう。 + +=={practice_physics_fixed_timestep} Fixed TimestepとFixed Updateの頻度の最適化 + +MonoBehaviourの@{FixedUpdate}は@{Update}とは違い、固定時間で実行されます。 + +物理エンジンは前フレームの経過時間に対して、1フレーム内でFixed Updateを複数回呼び出すことで、ゲームの世界の経過時間と物理エンジンの世界の時間をあわせます。 +そのためFixed Timestepの値が小さいと、@{より多くの回数Fixed Updateが呼び出され、負荷の原因となります}。 + +この時間は、@{projectsetting_time_fixed_timestep}の示すように、Project Settingsの@{Fixed Timestep}で設定できます。この値の単位は秒となります。デフォルトでは0.02、つまり20ミリ秒が指定されています。 + +//image[projectsetting_time_fixed_timestep][Project SettingsのFixed Timestep項目][scale=1.0] + +また、スクリプト中からは@{Time.fixedDeltaTime}を操作することで変更できます。 + +@{Fixed Timestep}は一般に、小さいほど物理演算の精度が上がり、コリジョン抜けなどの問題が発生しにくくなります。 +そのため、精度と負荷のトレードオフにはなりますが、@{ゲームの挙動に不備が発生しない範囲でこの値をターゲットFPSに近い値にする}ことが望ましいです。 + +==={practice_physics_maximum_allowed_timestep} Maximum Allowed Timestep + +前節のとおり、Fixed Updateは前フレームからの経過時間をもとに複数回呼び出されます。 + +「あるフレームでレンダリング処理が重たい」などの理由で前フレームの経過時間が大きくなった場合、そのフレームでは通常より多くの回数Fixed Updateが呼び出されることになります。 + +たとえば、@{Fixed Timestep}が20ミリ秒で前フレームに200ミリ秒かかったとき、Fixed Updateは10回呼び出されることになります。 + +つまり、あるフレームで処理落ちした場合は次のフレームでの物理演算のコストが高くなります。 +それが原因でそのフレームも処理落ちするリスクが高くなることで、次フレームでの物理演算も重くなる、といった負のスパイラルに陥る現象が物理エンジンの世界では知られています。 + +この問題を解決するためにUnityでは@{projectsetting_time_maximum_allowed_timestep}に示すように、Project Settingsから@{Maximum Allowd Timestep}という、1フレーム内で物理演算が利用する時間の最大値が設定できます。 +この値はデフォルトで0.33秒が設定されていますが、ターゲットFPSに近い値にしてFixed Updateの呼び出し回数を制限し、フレームレートを安定させたほうがよいでしょう。 + +//image[projectsetting_time_maximum_allowed_timestep][Project SettingsのMaximum Allowed Timestep項目][scale=1.0] + +=={practice_physics_collision} コリジョン形状の選定 + +一般的に複雑なコリジョン形状は、当たり判定にかかるコストが高くなります。 +そのため物理演算のパフォーマンスを最適化するときに、コライダーの形状は@{可能な限りシンプルな形状を採用する}ことが重要です。 + +たとえば、人型のキャラクター形状の近似にカプセルコライダーをよく利用しますが、ゲームとして身長が仕様に影響しない場合、スフィアコライダーに置き換えたほうが当たり判定のコストは小さくなります。 + +形状の選定は、当たり判定の対象や状況によってコストが異なるので一概に言えないですが、判定コストを低い順に並べるとスフィアコライダー、カプセルコライダー、ボックスコライダー、メッシュコライダーと覚えておくとよいでしょう。 + +=={practice_physics_collision_mesh} 衝突マトリックスとレイヤーの最適化 + +Physicsには、どのゲームオブジェクトのレイヤー同士が衝突可能かを定義する「衝突マトリックス」という設定があります。 +この設定は@{projectsetting_layer_collision_matrix}に示すように、Project SettingsのPhysics > Layer Collision Matrixから変更できます。 + +//image[projectsetting_layer_collision_matrix][Project SettingsのLayer Collision Matrix項目] + +衝突マトリックスは、2つのレイヤーが交わる位置のチェックボックスにチェックが入っていれば、それらのレイヤーは衝突することを示します。 + +衝突しないレイヤー同士は@{ブロードフェーズと呼ばれるオブジェクトの大雑把な当たり判定を取る前計算からも除外される}ため、この設定を適切に行うのが、@{衝突する必要がないオブジェクト同士の計算を省くのに最も効率的}です。 + +パフォーマンスを考慮すると、@{物理演算には専用のレイヤーを用意}し、@{衝突する必要のないレイヤー間のチェックボックスはすべてオフ}にすることが望ましいです。 + +=={practice_physics_raycast} レイキャストの最適化 + +レイキャストは、飛ばしたレイと衝突したコライダーの衝突情報を取得できる便利な機能ですが、その反面、負荷の原因にもなるので注意が必要です。 + +==={practice_physics_raycast_type} レイキャストの種類 + +レイキャストは、線分との衝突判定を取る@{Physics.Raycast}以外にも、@{Physics.SphereCast}などの、その他の形状で判定を取るメソッドが用意されています。 + +ただし判定を取る形状が複雑になるほど、その負荷が高くなります。パフォーマンスを考慮すると、可能な限り@{Physics.Raycast}の利用のみに留めるのが望ましいです。 + +==={practice_physics_raycast_parameter} レイキャストのパラメーターの最適化 + +@{Physics.Raycast}は、レイキャストの始点と向きの2つのパラメーター以外に、パフォーマンスの最適化に関わるパラメーターとして、@{maxDistance}と@{layerMask}があります。 + +@{maxDistance}はレイキャストの判定を行う最大の長さ、つまりレイの長さを指定します。 + +このパラメーターを省略すると既定値として@{Mathf.Infinity}が渡され、非常に長いレイで判定を取ろうとします。 +このようなレイは、ブロードフェーズに対して悪影響を与えたり、そもそも当たりを取る必要のないオブジェクトと当たり判定を取る可能性があるので、必要以上の距離を指定しないようにします。 + +また@{layerMask}も、当たりを取る必要のないレイヤーのビットは立てないようにします。 + +衝突マトリックスと同様に、ビットが立っていないレイヤーとはブロードフェーズからも除外されるため、計算コストを抑えることができます。 +このパラメーターを省略すると、既定値として@{Physics.DefaultRaycastLayers}という、Ignore Raycast以外のすべてのレイヤーと衝突する値が指定されるため、こちらも必ず指定します。 + +==={practice_physics_raycastall} RaycastAllとRaycastNonAlloc + +@{Physics.Raycast}では、衝突したコライダーのうち1つの衝突情報が返却されますが、@{Physics.RaycastAll}メソッドを利用すると、複数の衝突情報を取得できます。 + +@{Physics.RaycastAll}は衝突情報を、@{RaycastHit}構造体の配列を動的に確保して返却します。 +そのため、このメソッドを呼び出すたびにGC Allocが発生し、GCによるスパイクの原因になります。 + +この問題を回避するために、確保済みの配列を引数に渡すと、結果をその配列に書き込んで返却する@{Physics.RaycastNonAlloc}というメソッドが存在します。 + +パフォーマンスを考慮すると@{FixedUpdate}内では、可能な限りGC Allocを発生させないようにすべきです。 + +@{how_to_use_raycast_all_nonalloc}に示すように、結果を書き込む配列をクラスのフィールドやプーリングなどの機構で保持し、その配列を@{Physics.RaycastNonAlloc}に渡すことで、配列の初期化時以外のGC.Allocを回避できます。 + +//listnum[how_to_use_raycast_all_nonalloc][@{Physics.RaycastAllNonAlloc}の利用方法][csharp]{ +// レイを飛ばす始点 +var origin = transform.origin; +// レイを飛ばす方向 +var direction = Vector3.forward; +// レイの長さ +var maxDistance = 3.0f; +// レイが衝突する対象のレイヤー +var layerMask = 1 << LayerMask.NameToLayer("Player"); + +// レイキャストの衝突結果を格納する配列 +// この配列を初期化時に事前に確保したり、 +// プールに確保されているものを利用する +// 事前にレイキャストの結果の最大数を決める必要がある +// private const int kMaxResultCount = 100; +// private readonly RaycastHit[] _results = new RaycastHit[kMaxResultCount]; + +// すべての衝突情報が配列で返ってくる +// 戻り値に衝突個数が返されるので合わせて利用する +var hitCount = Physics.RaycastNonAlloc( + origin, + direction, + _results, + layerMask, + query +); +if (hitCount > 0) +{ + Debug.Log($"{hitCount}人のプレイヤーとの衝突しました"); + + // _resultsには配列の0番目から順に衝突情報がはいる + var firstHit = _results[0]; + + // 個数以上のインデックスを指定するとそれは無効な衝突情報なので注意 +} +//} + +=={practice_physics_collider_and_rigidbody} コライダーとRigidbody + +UnityのPhysicsには、スフィアコライダーやメッシュコライダーなどの衝突について扱う@{Collider}コンポーネントと、物理シミュレーションを剛体ベースで行うための@{Rigidbody}コンポーネントがあります。 +これらのコンポーネントの組み合わせとその設定によって、3つのコライダーに分類されます。 + +@{Collider}コンポーネントがアタッチされ、@{Rigidbody}コンポーネントがアタッチされていないオブジェクトは、@{静的コライダー}(Static Collider)と呼ばれます。 + +このコライダーは@{常に同じ場所に留まる、動くことのないジオメトリにのみ使用することを前提}に最適化が行われます。 + +そのため、ゲーム中に静的コライダーの@{有効・無効を切り替えたり、移動やスケーリングを行なうべきではありません}。 +これらの処理を行うと、@{内部のデータ構造の変更に伴う再計算が行われ、パフォーマンスを著しく低下させる原因となります}。 + +@{Collider}コンポーネントと@{Rigidbody}コンポーネントの両方がアタッチされているオブジェクトは、@{動的コライダー}(Dynamic Collider)と呼ばれます。 + +このコライダーは、物理エンジンによって他のオブジェクトと衝突できます。また、スクリプトから@{Rigidbody}コンポーネントを操作することによって適用される衝突や力に反応できます。 + +そのため、物理演算が必要なゲームでは、もっともよく利用されるコライダーになります。 + +@{Collider}コンポーネントと@{Rigidbody}コンポーネントの両方がアタッチして、かつ@{Rigidbody}の@{isKinematic}プロパティを有効にしたコンポーネントは、@{キネマティックな動的コライダー}(Kinematic Dynamic Collider)として分類されます。 + +キネマティックな動的コライダーは、@{Transform}コンポーネントを直接操作することで動かせますが、通常の動的コライダーのように@{Rigidbody}コンポーネントを操作することで衝突や力を加えて動かせません。 + +@{物理演算の実行を切り替えたい場合}や、ドアなどの@{たまに動かしたいが大半は動かない障害物}などにこのコライダーを利用することで、物理演算を最適化できます。 + +==={practice_physics_rigidbody_and_sleep} Rigidbodyとスリープ状態 + +物理エンジンでは最適化の一環として、@{Rigidbody}コンポーネントをアタッチしたオブジェクトが一定時間動かない場合、そのオブジェクトは休止中と判断して、そのオブジェクトの内部状態をスリープ状態に変更します。 +スリープ状態に移行すると、外力や衝突などのイベントによって動かない限りは、そのオブジェクトにかかる計算コストが最小限に抑えられます。 + +そのため、@{Rigidbody}コンポーネントがアタッチされたオブジェクトのうち動く必要のないものは、可能な限りスリープ状態に遷移させておくことで物理演算の計算コストを抑えることができます。 + +@{Rigidbody}コンポーネントがスリープ状態へ移行すべきかを判定する際に利用されるしきい値は@{projectsetting_sleep_threshold}に示すように、Project SettingsのPhysics内部の@{Sleep Threshold}で設定できます。 +また、個別のオブジェクトに対してしきい値を指定したい場合は、@{Rigidbody.sleepThreshold}プロパティから設定できます。 + +//image[projectsetting_sleep_threshold][Project SettingsのSleep Threshold項目][scale=1.0] + +@{Sleep Threshold}はスリープ状態に移行する際の質量で正規化された運動エネルギーを表します。 + +この値を大きくすると、オブジェクトはより早くスリープ状態へ移行するため、計算コストを低く抑えられます。しかし、ゆっくり動いている場合にもスリープ状態へ移行する傾向にあるため、オブジェクトが急停止したように見える場合があります。 +この値を小さくすると、上記の現象は発生しにくくなりますが、一方でスリープ状態へは移行しづらくなるため、計算コストを抑えられにくい傾向になります。 + +@{Rigidbody}がスリープ状態かどうかは、@{Rigidbody.IsSleeping}プロパティで確認できます。シーン上でアクティブな@{Rigidbody}コンポーネントの総数は@{profiler_physics}に示すように、プロファイラーのPhysics項目から確認できます。 + +//image[profiler_physics][ProfilerのPhysics項目。アクティブな@{Rigidbody}の個数だけでなく、物理エンジン上のそれぞれの要素数が確認できる。][scale=1.0] + +また、@{Physics Debugger}を用いると、シーン上のどのオブジェクトがアクティブ状態なのかを確認できます。 + +//image[physics_debugger][Physics Debugger。シーン上のオブジェクトが物理エンジン上どのような状態か、色分けされて表示される。][scale=1.0] + +=={practice_physics_rigidbody_collision_detecion} 衝突検出の最適化 + +@{Rigidbody}コンポーネントはCollision Detection項目にて、衝突検出で利用するアルゴリズムが選択できます。 + +Unity 2020.3時点で、衝突判定には下記の4つがあります。 + + * Discrete + * Continuous + * Continuous Dynamic + * Continuous Speculative + +これらのアルゴリズムは大きく分けて@{離散的衝突判定}と@{連続的衝突判定}の2つに分類されます。@{Discrete}は離散的衝突判定で、それ以外は連続的衝突判定に属します。 + +離散的衝突判定は名前のとおり、1シミュレーションごとにオブジェクトが離散的にテレポート移動し、すべてのオブジェクトが移動後に衝突判定を行います。 +そのため、とくにオブジェクトが@{高速に移動している場合に、衝突を見逃してすり抜けを起こす可能性}があります。 + +一方で連続的衝突判定は、移動前後のオブジェクトの衝突を考慮するために、@{高速に動くオブジェクトのすり抜けを防ぎます}。その分、計算コストは離散的衝突判定と比べて高くなります。 +パフォーマンスを最適化するには、@{可能な限りDiscreteを選択できるようにゲームの挙動を作ります}。 + +もし不都合がある場合は、連続的衝突判定を検討します。 +@{Continuous}は@{動的コライダー}と@{静的コライダー}の組み合わせにのみ連続的衝突判定が有効になり、@{Conitnuous Dynamic}は動的コライダー同士でも連続的衝突判定が有効になります。 +計算コストはContinuous Dynamicのほうが高くなります。 + +そのためキャラクターがフィールドを走り回る、つまり動的コライダーと静的コライダーの衝突判定のみを考慮する場合はContinuousを選択し、動くコライダー同士のすり抜けも考慮したい場合はContinuous Dynamicを選択します。 + +@{Continuous Speculative}は動的コライダー同士で連続的衝突判定が有効にもかかわらずContinuous Dynamicより計算コストが低いですが、複数のコライダーが密接している箇所で誤衝突してしまうゴースト衝突(@{Ghost Collision})と呼ばれる現象が発生するため、導入には注意が必要です。 + +=={practice_physics_settings} その他プロジェクト設定の最適化 + +これまでに紹介した設定以外で、とくにパフォーマンスの最適化に影響するプロジェクト設定の項目を紹介します。 + +==={practice_physics_settings_auto_sync_transform} Physics.autoSyncTransforms + +Unity 2018.3より前のバージョンでは、@{Physics.Raycast}などの物理演算に関するAPIを呼び出すたびに、@{Transform}と物理エンジンの位置が自動的に同期されていました。 + +この処理は比較的重たいので、物理演算のAPIを呼び出したときにスパイクの原因になります。 + +この問題を回避するために、Unity 2018.3以降、@{Physics.autoSyncTransforms}という設定が追加されています。 +この値に@{false}を設定すると、上記で説明した、物理演算のAPIを呼び出したときの@{Transform}の同期処理が行われなくなります。 + +@{Transform}の同期は物理演算のシミュレーション時の、@{FixedUpdate}が呼び出された後になります。 +つまり、コライダーを移動してから、そのコライダーの新しい位置に対してレイキャストを実行しても、レイキャストがコライダーに当たらないことを意味します。 + +==={practice_physics_settings_reuse_collision_callback} Physics.reuseCollisionCallbacks + +Unity 2018.3より前のバージョンでは、@{OnCollisionEnter}などの@{Collider}コンポーネントの衝突判定を受け取るイベントが呼び出されるたびに、引数の@{Collision}インスタンスを新たに生成して渡されるため、GC Allocが発生していました。 + +この挙動は、イベントの呼び出し頻度によってはゲームのパフォーマンスに悪影響を及ぼすため、2018.3以降では新たに@{Physics.reuseCollisionCallbacks}というプロパティが公開されました。 + +この値に@{true}を設定すると、イベント呼び出し時に渡される@{Collision}インスタンスを内部で使い回すため、GC Allocが抑えられます。 + +この設定は2018.3以降ではデフォルト値として@{true}が設定されているため、比較的新しいUnityでプロジェクトを作成した場合には問題ないですが、2018.3より前のバージョンでプロジェクトを作成した場合、この値が@{false}になっている場合があります。 +もしこの設定が無効になっている場合は、@{この設定を有効にしたうえでゲームが正常に動作するようコードを修正すべきです}。 diff --git a/articles/text/tuning_practice_player_settings.re b/articles/text/tuning_practice_player_settings.re new file mode 100644 index 0000000..a3066ea --- /dev/null +++ b/articles/text/tuning_practice_player_settings.re @@ -0,0 +1,51 @@ +={practice_project_settings} Tuning Practice - Player Settings + +この章では、パフォーマンスに影響を与えるProject SettingsにあるPlayerの項目について紹介します。 + +=={practice_player_settings_scripting_backend} Scripting Backend + +Unityでは、AndroidやStandalone (Windows、macOS、Linux) といったプラットフォームで、Scripting BackendをMonoかIL2CPPのどちらかから選択できます。 +@{basic}の@{basic|basic_unity_output_binary_il2cpp}で述べたとおりパフォーマンスが向上するため、IL2CPPを選択することを推奨します。 + +//image[scripting_backend][Scripting Backendの設定][scale=0.75] + +また、Scripting BackendをIL2CPPに変更すると、一部プラットフォームを除いて@{C++ Compiler Configuration}が選択できるようになります。 + +//image[compiler_configuration][C++ Compiler Configurationの設定][scale=0.75] + +ここではDebug、Release、Masterから選べますが、それぞれビルド時間と最適化度合いのトレードオフがありますので、ビルドの目的に応じて使い分けるとよいでしょう。 + +==={practice_player_settings_compiler_configuration_debug} Debug + +最適化が行われないためランタイムでのパフォーマンスは良くありませんが、ビルド時間は他の設定と比較してもっとも短くなります。 + +==={practice_player_settings_compiler_configuration_release} Release + +最適化によりランタイムのパフォーマンスが向上し、ビルドしたバイナリのサイズもより小さくなりますが、ビルドに要する時間は伸びます。 + +==={practice_player_settings_compiler_configuration_master} Master + +そのプラットフォームで利用可能なすべての最適化が有効化されます。 +たとえばWindows向けのビルドでは、リンク時コード生成 (LTCG) が使用されるなど、よりアグレッシブな最適化が行われます。 +その代わりとしてビルド時間はRelease設定よりもさらに伸びますが、それが許容できる場合、製品版のビルドにはMaster設定を使用することをUnityは推奨しています。 + + +=={practice_player_settings_code_stripping} Strip Engine Code / Managed Stripping Level + +@{Strip Engine Code}はUnityの機能から、@{Managed Stripping Level}はC#をコンパイルして生成されるCILバイトコードから、 +それぞれ使用されないコードを取り除くことにより、ビルドしたバイナリのサイズを削減する効果が期待できます。 + +しかしながら、あるコードが使用されているかどうかの判定は静的解析に強く依存しているため、 +コード中で直接参照されていない型や、リフレクションで動的に呼び出しているコードが誤って除去されてしまう場合があります。 + +その場合には@{link.xml}ファイルや、@{Preserve}属性を指定することにより除去されることを回避できます。@{managed_code_stripping} + +//footnote[managed_code_stripping][@{https://docs.unity3d.com/2020.3/Documentation/Manual/ManagedCodeStripping.html}] + +=={practice_player_settings_accelerometer} Accelerometer Frequency (iOS) + +iOS固有の設定で、加速度センサーのサンプリング周波数を変更できます。 +初期設定では60Hzになっているため、適切な周波数に設定しましょう。 +とくに加速度センサーを利用していない場合は、必ず設定を無効化しましょう。 + +//image[ios_accelerometer_frequency][サンプリング周波数の設定][scale=0.75] diff --git a/articles/text/tuning_practice_script_csharp.re b/articles/text/tuning_practice_script_csharp.re new file mode 100644 index 0000000..bc31e03 --- /dev/null +++ b/articles/text/tuning_practice_script_csharp.re @@ -0,0 +1,856 @@ +={practice_script_csharp} Tuning Practice - Script (C#) + +本章では、主にC#コードのパフォーマンスチューニング手法について実例を交えながら紹介します。 +C#の基礎的な記法についてはここでは扱わず、パフォーマンスを要求されるようなゲームの開発において +意識すべき設計や実装について解説します。 + +=={practice_script_csharp_sample} GC.Allocするケースと対処法 + +@{basic|basic_csharp_gc}で紹介しましたが、 +本節では具体的にどのような処理を行ったときにGC.Allocをするのか、まずは理解していきましょう。 + +==={practice_script_csharp_new} 参照型のnew + +まずはとてもわかりやすくGC.Allocが発生するケースです。 + +//listnum[simple_list_alloc][毎フレームGC.Allocするコード][csharp]{ +private void Update() +{ + const int listCapacity = 100; + // ListのnewでGC.Alloc + var list = new List(listCapacity); + for (var index = 0; index < listCapacity; index++) + { + // 特に意味はないけどindexをListに詰めていく + list.Add(index); + } + // listから値をランダムに取り出す + var random = UnityEngine.Random.Range(0, listCapacity); + var randomValue = list[random]; + // ... ランダムな値から何かする ... +} +//} +このコードの大きな問題点は、毎フレーム実行されるUpdateメソッドで、 +@{List}を@{new}している点です。 + +こちらを修正するには、@{List}を事前に生成して使い回すことで毎フレームのGC.Allocを +回避することが可能です。 + +//listnum[simple_list_nonalloc][毎フレームのGC.Allocを無くしたコード][csharp]{ +private static readonly int listCapacity = 100; +// 事前にListを生成しておく +private readonly List _list = new List(listCapacity); + +private void Update() +{ + _list.Clear(); + for (var index = 0; index < listCapacity; index++) + { + // 特に意味はないけどindexをListに詰めていく + _list.Add(index); + } + // listから値をランダムに取り出す + var random = UnityEngine.Random.Range(0, listCapacity); + var randomValue = _list[random]; + // ... ランダムな値から何かする ... +} +//} + +こちらのサンプルコードのような無意味なコードを書くことはないと思いますが、 +類似した例は想像よりも見つかるケースが多いです。 + +====[column] GC.Allocを無くしたら + +気付いた方も多いかと思いますが、上記@{simple_list_nonalloc}のサンプルコードはこれだけで事足ります。 +//listnum[simple_random][][csharp]{ +var randomValue = UnityEngine.Random.Range(0, listCapacity); +// ... ランダムな値から何かする ... +//} +パフォーマンスチューニングにおいてGC.Allocを無くすことを考えるのは重要ですが、 +無意味な計算を省くことを常に考えることが高速化の一歩に繋がります。 + +====[/column] + + +==={practice_script_csharp_lambda} ラムダ式 +ラムダ式も便利な機能ですが、こちらも使い方によってはGC.Allocが発生してしまうため、 +ゲームでは使用できる場面は限られます。 +ここでは、次のようなコードが定義されている前提とします。 +//listnum[lambda_define][ラムダ式サンプルの前提コード][csharp]{ +// メンバー変数 +private int _memberCount = 0; + +// static変数 +private static int _staticCount = 0; + +// メンバーメソッド +private void IncrementMemberCount() +{ + _memberCount++; +} + +// staticメソッド +private static void IncrementStaticCount() +{ + _staticCount++; +} + +// 受け取ったActionをInvokeするだけのメンバーメソッド +private void InvokeActionMethod(System.Action action) +{ + action.Invoke(); +} +//} + +このとき、次のようにラムダ式内で変数を参照した場合、GC.Allocが発生します。 + +//listnum[lambda_member_var][ラムダ式内で変数を参照してGC.Allocするケース][csharp]{ +// メンバー変数を参照した場合、Delegate Allocationが発生 +InvokeActionMethod(() => { _memberCount++; }); + +// ローカル変数を参照した場合、Closure Allocationが発生 +int count = 0; +// 上記と同じDelegate Allocationも発生 +InvokeActionMethod(() => { count++; }); +//} + +ただし、以下のようにstatic変数を参照すると、これらのGC.Allocを回避できます。 + +//listnum[lambda_static_var][ラムダ式内でstatic変数を参照してGC.Allocしないケース][csharp]{ +// static変数を参照した場合、GC Allocは発生せず +InvokeActionMethod(() => { _staticCount++; }); +//} + +//embed[latex]{ +\clearpage +//} + +ラムダ式内でのメソッド参照も記述方法によってGC.Allocのされ方が異なります。 + +//listnum[lambda_method][ラムダ式内でメソッドを参照してGC.Allocするケース][csharp]{ +// メンバーメソッドを参照した場合、Delegate Allocationが発生 +InvokeActionMethod(() => { IncrementMemberCount(); }); + +// メンバーメソッドを直接指定した場合、Delegate Allocationが発生 +InvokeActionMethod(IncrementMemberCount); + +// staticメソッドを直接指定した場合、Delegate Allocationが発生 +InvokeActionMethod(IncrementStaticCount); +//} + +これらを回避するためには、以下のようにステートメント形式でstaticメソッドを参照する必要があります。 + +//listnum[lambda_member_method_ref][ラムダ式内でメソッドを参照してGC.Allocしないケース][csharp]{ +// ラムダ式内でstaticメソッドを参照した場合、GC.Allocは発生せず +InvokeActionMethod(() => { IncrementStaticCount(); }); +//} +こうすることで初回のみActionがnewされますが、内部的にキャッシュされることによって2回目以降GC.Allocが回避されます。 + +しかし、すべての変数やメソッドをstaticにすることはコードの安全性や可読性の面から +とても採用できるものではありません。高速化が必要なコードでは、staticを多用してGC.Allocを無くすよりも +毎フレームもしくは不定なタイミングで発火するイベントなどはラムダ式を使わずに設計する方が安全と言えるでしょう。 + +==={practice_script_csharp_boxing_generic} ジェネリックを使用してボックス化するケース + +ジェネリックを使用した以下の場合、何が原因でボックス化する可能性があるでしょうか。 + +//listnum[generic_boxing][ジェネリックを使用してボックス化する可能性がある例][csharp]{ +public readonly struct GenericStruct : IEquatable +{ + private readonly T _value; + + public GenericStruct(T value) + { + _value = value; + } + + public bool Equals(T other) + { + var result = _value.Equals(other); + return result; + } +} +//} +このケースでは、プログラマーは@{GenericStruct}に@{IEquatable}インターフェイスの実装をしましたが、 +@{T}に制限を設けるのを忘れてしまいました。その結果、@{IEquatable}インターフェイスの実装がされていない型を +@{T}に指定できてしまい、@{Object}型へ暗黙的にキャストされて以下の@{Equals}が使われるケースが存在してしまいます。 + +//listnum[object_class_equals][Object.cs][csharp]{ +public virtual bool Equals(object obj); +//} + +たとえば@{IEquatable}インターフェイスの実装がされていない@{struct}を@{T}に指定すると、@{Equals}の引数で +@{object}にキャストされることになるため、ボックス化が発生します。これが起こらないように事前に対策するには、 +次のように変更します。 + +//listnum[generic_non_boxing][ボックス化しないように制限をかけた例][csharp]{ +public readonly struct GenericOnlyStruct : IEquatable + where T : IEquatable +{ + private readonly T _value; + + public GenericOnlyStruct(T value) + { + _value = value; + } + + public bool Equals(T other) + { + var result = _value.Equals(other); + return result; + } +} +//} + +@{where}句(ジェネリック型制約)を用いて、@{T}が受け入れられる型を@{IEquatable}を実装している型に制限してあげることで、 +こうした予期せぬボックス化を未然に防ぐことができます。 + +====[column] 本来の目的を見失わない + +@{basic|basic_csharp_gc}で紹介したようにゲームではランタイム中のGC.Allocを避けたい意図があるため、 +構造体が選択されるケースも多く存在します。ただし、GC.Allocを削減したいあまりにすべてを構造体にしたところで高速化できるわけではありません。 + +よくある失敗としては、GC.Allocを避ける目的で構造体を取り入れたところ、期待通りGCに関するコストが減ったものの、 +データサイズが大きいために値型のコピーコストがかかってしまい結果的に非効率な処理になってしまうようなケースが挙げられます。 + +また、これをさらに回避するためにメソッドの引数を参照渡しを利用することでコピーコストを削減する手法も存在します。 +結果的に高速化できる可能性はありますが、この場合は最初からクラスを選択し、インスタンスを事前に生成して使い回すような実装を検討すべきでしょう。 +GC.Allocを撲滅することが目的ではなく、あくまでも1フレームあたりの処理時間を短くすることが最終目的であることを忘れないようにしましょう。 + +====[/column] + +=={practice_script_csharp_loop} for/foreachについて + +@{basic|basic_algorithm}で紹介したようにループはデータ数に依存して時間がかかるようになります。 +また、一見同じような処理に見えるループもコードの書き方次第で効率が変わります。 + +ここでは、SharpLab@{performance_tips_sharplab}を利用して、@{foreach/for}を使用した +@{List}や配列の中身を1つずつ取得するだけのコードをILからC#にデコンパイルした結果を見てみましょう。 + +//footnote[performance_tips_sharplab][@{https://sharplab.io/}] + +まずは@{foreach}でループを回した場合を見てみましょう。@{List}への値の追加などは省略してます。 + +//listnum[simple_foreach_list][Listをforeachで回す例][csharp]{ +var list = new List(128); +foreach (var val in list) +{ +} +//} + +//listnum[simple_foreach_list_decompile][Listをforeachで回す例のデコンパイル結果][csharp]{ +List.Enumerator enumerator = new List(128).GetEnumerator(); +try +{ + while (enumerator.MoveNext()) + { + int current = enumerator.Current; + } +} +finally +{ + ((IDisposable)enumerator).Dispose(); +} +//} + +@{foreach}で回した場合は、列挙子を取得して@{MoveNext()}で次へ進めて@{Current}で値を参照する実装になっていることがわかります。 +さらに、list.cs@{performance_tips_ms_list}の +@{MoveNext()}の実装を見ると、サイズのチェックなど各種プロパティアクセス回数が多くなっており、インデクサーによる直アクセスより +処理が多くなるように見えます。 + +//footnote[performance_tips_ms_list][https://referencesource.microsoft.com/#mscorlib/system/collections/generic/list.cs] + + +次に、@{for}で回したときを見てみましょう。 + +//listnum[simple_for_list][Listをforで回す例][csharp]{ +var list = new List(128); +for (var i = 0; i < list.Count; i++) +{ + var val = list[i]; +} +//} + +//listnum[simple_for_list_decompile][Listをforで回した際のデコンパイル結果][csharp]{ +List list = new List(128); +int num = 0; +while (num < list.Count) +{ + int num2 = list[num]; + num++; +} +//} + +C#では@{for}文は@{while}文の糖衣構文であり、インデクサー(@{public T this[int index]})による参照で取得されていることがわかります。 +また、この@{while}文をよく見ると、条件式に@{list.Count}が入っています。つまり、@{Count}プロパティへの +アクセスがループを繰り返すたびに行われることになります。@{Count}の数が多くなればなるほど、@{Count}プロパティへのアクセス回数が比例して増加し、 +数によっては無視できない負荷になっていきます。もし、ループ内で@{Count}が変わらないのであれば、ループの前でキャッシュしておくことでプロパティアクセスの負荷を削減できます。 + +//listnum[simple_for_improve_list][Listをforで回す例: 改良版][csharp]{ +var count = list.Count; +for (var i = 0; i < count; i++) +{ + var val = list[i]; +} +//} + +//listnum[simple_for_improve_list_decompile][Listをforで回す例: 改良版のデコンパイル結果][csharp]{ +List list = new List(128); +int count = list.Count; +int num = 0; +while (num < count) +{ + int num2 = list[num]; + num++; +} +//} + +@{Count}をキャッシュすることでプロパティアクセス回数が削減され、高速化されました。 +今回のループ中の比較はどちらもGC.Allocによる負荷はなく、実装内容の違いによる差分となります。 + +また、配列の場合は@{foreach}も最適化されており、@{for}で記述したものとほぼ変化はないものとなります。 + +//listnum[simple_foreach_array][配列をforeachで回す例][csharp]{ +var array = new int[128]; +foreach (var val in array) +{ +} +//} + +//listnum[simple_foreach_array_decompile][配列をforeachで回す例のデコンパイル結果][csharp]{ +int[] array = new int[128]; +int num = 0; +while (num < array.Length) +{ + int num2 = array[num]; + num++; +} +//} + + +検証のため、データ数10,000,000として事前にランダムな数値をアサインし、@{List} +データの和を計算するものとしました。検証環境はPixel 3a、Unity 2021.3.1f1で実施しました。 + +//table[loop_profile_diff_int][Listにおける記述方法ごとの計測結果]{ +種類 Time ms +List: foreach 66.43 +List: for 62.49 +List: for(Countキャッシュ) 55.11 +配列: for 30.53 +配列: foreach 23.75 +//} + +@{List}の場合は、条件を細かく揃えて比較してみると@{foreach}よりも@{for}、@{Count}の最適化を施した@{for}のほうが +さらに速くなったことがわかります。@{List}の@{foreach}は、@{Count}の最適化を施した@{for}に書き換えることで、 +@{foreach}の処理における@{MoveNext()}や@{Current}プロパティのオーバーヘッドを削減し、高速化が可能です。 + +また、@{List}と配列のそれぞれ最速同士で比較すると、@{List}より配列のほうが約2.3倍以上高速になりました。 +@{foreach}と@{for}でILが同じ結果になるように記述しても、@{foreach}が速い結果となり、 +配列の@{foreach}が十分に最適化されていると言えるでしょう。 + +以上の結果から、データ数が多くかつ処理速度を高速にしなければならない場面については@{List}ではなく配列を検討すべきでしょう。 +しかし、フィールドに定義した@{List}をローカルキャッシュせずそのまま参照してしまうなど、書き換えが不十分な場合は高速化できないこともありますので、 +@{foreach}から@{for}に変更する際には必ず計測をしつつ適切に書き換えましょう。 + +=={practice_script_csharp_reuse} オブジェクトプーリング + +随所で触れてきましたが、ゲーム開発では動的にオブジェクトを生成せずに、事前生成して使い回すことが重要です。 +これを@{オブジェクトプーリング}と呼びます。たとえば、ゲームフェーズで使用予定のオブジェクトをロードフェーズでまとめて生成してプーリングしておき、 +使用するときはプールしているオブジェクトへの代入と参照のみ行いながら扱うことで、ゲームフェーズ中のGC.Allocを避けることが可能です。 + +また、オブジェクトプーリングはアロケーションの削減の他にも、画面を構成するオブジェクトを都度作り直すことなく +画面遷移を可能にしておくことでロード時間の短縮を実現したり、計算コストが非常に高い処理の結果を保持しておいて重い計算を複数回実行することを避けたり、 +さまざまな場面で用いられます。 + +ここでは広義にオブジェクトと表現しましたが、これは最小単位のデータに留まらず、@{Coroutine}や@{Action}などにも +該当します。たとえば、事前に@{Coroutine}を想定される実行数分以上生成しておき、必要なタイミングで使用して使い潰していくようなことも検討しましょう。 +2分間で終わるゲームで最大で20回実行されるようなときは@{IEnumerator}をそれぞれ先に生成しておき、使用する時は@{StartCoroutine}するだけにすることで +生成コストは抑えられます。 + +=={practice_script_csharp_string} string + +@{string}オブジェクトは文字列を表す@{System.Char}オブジェクトのシーケンシャルコレクションです。@{string}は使い方1つでGC.Allocが簡単に起こります。 +たとえば、文字連結演算子@{+}を利用して2つの文字列の連結をすると、新しい@{string}オブジェクトを生成することになります。@{string}の値は生成後に +変更できない(イミュータブル)ため、値の変更が行われているように見える操作は新しい@{string}オブジェクトを生成して返しています。 + +//listnum[string_create][文字列結合でstringを作る場合][csharp]{ +private string CreatePath() +{ + var path = "root"; + path += "/"; + path += "Hoge"; + path += "/"; + path += "Fuga"; + return path; +} +//} + +上記例の場合は、各文字列結合でstringが生成されていき、合計で164Byteアロケーションが発生します。 + +文字列が頻繁に変更されるときは、値が変更可能な@{StringBuilder}を利用することで@{string}オブジェクトの大量生成を防ぐことができます。 +文字連結や削除などの操作を@{StringBuilder}オブジェクトで行い、最終的に値を取り出して@{string}オブジェクトに@{ToString()}することで、 +取得時のみのメモリアロケーションに抑えることができます。また、@{StringBuilder}を使用する際には、Capacityを必ず設定するようにしましょう。 +未指定のときは初期値が16になり、@{Append}などで文字数が増えバッファーが拡張されるときにメモリの確保と値のコピーが走るため、 +不用意な拡張が発生しない適切なCapacityを設定するようにしましょう。 + + +//listnum[string_create_builder][StringBuilderでstringを作る場合][csharp]{ +private readonly StringBuilder _stringBuilder = new StringBuilder(16); +private string CreatePathFromStringBuilder() +{ + _stringBuilder.Clear(); + _stringBuilder.Append("root"); + _stringBuilder.Append("/"); + _stringBuilder.Append("Hoge"); + _stringBuilder.Append("/"); + _stringBuilder.Append("Fuga"); + return _stringBuilder.ToString(); +} +//} + +@{StringBuilder}を用いた例では、事前に@{StringBuilder}を生成(上記例の場合、生成時に112Byteアロケーション)しておけば、 +以降は生成した文字列を取り出す@{ToString()}時に掛かる50Byteのアロケーションで済みます。 + +ただし、@{StringBuilder}も値の操作中にアロケーションが起こりにくいだけで、前述のように@{ToString()}実行時には@{string}オブジェクトを +生成することになるため、GC.Allocを避けたいときに使用するのは推奨されません。また、@{$""}構文は@{string.Format}に変換され、 +@{string.Format}の内部実装は@{StringBuilder}が使われているため、結局は@{ToString()}のコストは避けられません。 +前項のオブジェクトの使い回しをここでも応用し、あらかじめ使用される可能性のある文字列は@{string}オブジェクトを事前に生成し、それを使うようにしましょう。 + +しかしながらゲーム中に文字列操作と@{string}オブジェクトの生成をどうしても行わなければならない場合もあります。 +そういう場合には、文字列用のバッファーを事前に持っておいて、そのバッファーをそのまま使えるようにするような拡張を行う必要があります。 +@{unsafe}なコードを自前で実装するか、ZString@{string_zstring}のようなUnity向けの拡張機能 +(たとえば@{TextMeshPro}へのNonAllocな適用機能)を備えたライブラリの導入を検討しましょう。 + +//footnote[string_zstring][@{https://github.com/Cysharp/ZString}] + +=={practice_script_csharp_linq} LINQと遅延評価 +本節ではLINQの使用によるGC.Allocを軽減する方法と遅延評価のポイントについて解説します。 + +==={practice_script_csharp_linq_gc_cause}LINQの使用によるGC.Allocを軽減する +LINQの使用では、@{linq_heap_allocation_sample_code}のような場合にGC.Allocが発生します。 + +//listnum[linq_heap_allocation_sample_code][GC.Allocが発生する例][csharp]{ +var oneToTen = Enumerable.Range(1, 11).ToArray(); +var query = oneToTen.Where(i => i % 2 == 0).Select(i => i * i); +//} + +@{linq_heap_allocation_sample_code}でGC.Allocが発生する理由はLINQの内部実装に起因します。 +加えて、LINQの一部メソッドは呼び出し側の型に合わせた最適化を行うため、呼び出し元の型によってGC.Allocのサイズが変化します。 + +//listnum[linq_type_optimization_query_execute][型ごとの実行速度検証][csharp]{ +private int[] array; +private List list; +private IEnumerable ienumerable; + +public void GlobalSetup() +{ + array = Enumerable.Range(0, 1000).ToArray(); + list = Enumerable.Range(0, 1000).ToList(); + ienumerable = Enumerable.Range(0, 1000); +} + +public void RunAsArray() +{ + var query = array.Where(i => i % 2 == 0); + foreach (var i in query){} +} + +public void RunAsList() +{ + var query = list.Where(i => i % 2 == 0); + foreach (var i in query){} +} + +public void RunAsIEnumerable() +{ + var query = ienumerable.Where(i => i % 2 == 0); + foreach (var i in query){} +} +//} + +@{linq_type_optimization_query_execute}に定義した各メソッドのベンチマークを測定すると@{linq_type_optimizaiton_query_execute_result}のような結果が得られました。 +この結果から@{T[]} → @{List} → @{IEnumerable}の順番にヒープアロケーションのサイズが大きくなっていることがわかります。 + +このように、LINQを使用する場合は、実行時の型を意識することでGC.Allocのサイズを削減することができます。 + +//image[linq_type_optimizaiton_query_execute_result][型ごとの実行速度比較][scale=1.0] + +====[column] LINQのGC.Allocの原因 + +LINQの使用によるGC.Allocの原因の一部は、LINQの内部実装です。 +LINQのメソッドは@{IEnumerable}を受け取り、@{IEnumerable}を返すものが多く、このAPI設計によりメソッドチェーンを用いた直感的な記述ができるようになっています。 +このときメソッドが返す@{IEnumerable}の実体は、各機能に合わせたクラスのインスタンスとなっています。 +LINQは内部的に@{IEnumerable}を実装したクラスをインスタンス化してしまい、さらにループ処理を実現するために@{GetEnumerator()}の呼び出しなどが行われるため内部的にGC.Allocが発生します。 + +====[/column] + +==={practice_script_csharp_linq_lazy}LINQの遅延評価 +LINQの@{Where}や@{Select}といったメソッドは、実際に結果が必要になるまで評価を遅らせる遅延評価となっています。 +一方で、@{ToArray}のような即時評価となるメソッドも定義されています。 + +ここで、下記の@{linq_to_array_sample_code}のコードの場合を考えます。 + +//listnum[linq_to_array_sample_code][即時評価を挟んだメソッド][csharp]{ +private static void LazyExpression() +{ + var array = Enumerable.Range(0, 5).ToArray(); + var sw = Stopwatch.StartNew(); + var query = array.Where(i => i % 2 == 0).Select(HeavyProcess).ToArray(); + Console.WriteLine($"Query: {sw.ElapsedMilliseconds}"); + + foreach (var i in query) + { + Console.WriteLine($"diff: {sw.ElapsedMilliseconds}"); + } +} + +private static int HeavyProcess(int x) +{ + Thread.Sleep(1000); + return x; +} +//} + +@{linq_to_array_sample_code}の実行結果が@{linq_to_array_sample_code_result}になります。 +即時評価となる@{ToArray}を末尾に追加したことで、@{query}への代入時に@{Where}や@{Select}のメソッドを実行し値を評価した結果が返されます。 +そのため@{HeavyProcess}も呼び出されるので、@{query}を生成するタイミングで処理時間がかかっていることがわかります。 + +//listnum[linq_to_array_sample_code_result][即時評価のメソッドを追加した結果]{ +Query: 3013 +diff: 3032 +diff: 3032 +diff: 3032 +//} + +このようにLINQの即時評価のメソッドを意図せず呼び出してしまうとその箇所がボトルネックになってしまう可能性があります。@{ToArray}や@{OrderBy}, @{Count}などシーケンスすべてを一度見る必要があるメソッドは即時評価となるため、呼び出し時のコストを意識して使用しましょう。 + +==={practice_script_csharp_not_use_linq}「LINQの使用を避ける」という選択 +LINQを使用した際のGC.Allocの原因や軽減方法、遅延評価のポイントについて解説しました。 +本節ではLINQを使用する基準について解説します。 +前提としてLINQは便利な言語機能ではありますが、使用するとヒープアロケーションや実行速度は使用しない場合に比べて悪化します。 +実際にMicrosoftのUnityのパフォーマンスに関する推奨事項@{performance_tips_from_microsoft}では「Avoid use of LINQ」と明記されています。 +LINQを使用した場合と、使用しない場合で同じロジックを実装した場合のベンチマークを@{linq_vs_pure_benchmark}で比較してみます。 + +//footnote[performance_tips_from_microsoft][@{https://docs.microsoft.com/en-us/windows/mixed-reality/develop/unity/performance-recommendations-for-unity#avoid-expensive-operations}] + +//listnum[linq_vs_pure_benchmark][LINQの使用有無によるパフォーマンス比較][csharp]{ +private int[] array; + +public void GlobalSetup() +{ + array = Enumerable.Range(0, 100_000_000).ToArray(); +} + +public void Pure() +{ + foreach (var i in array) + { + if (i % 2 == 0) + { + var _ = i * i; + } + } +} + +public void UseLinq() +{ + var query = array.Where(i => i % 2 == 0).Select(i => i * i); + foreach (var i in query) + { + } +} +//} + +結果は@{linq_vs_pure_benchmark}になります。実行時間を比較するとLINQを使用しない場合に対してLINQを使った処理は19倍ほど時間がかかってしまっていることがわかります。 + +//image[linq_vs_pure_benchmark][LINQの使用有無によるパフォーマンス比較結果][scale=1.0] + +上記の結果からLINQを使用することによるパフォーマンスの悪化は明確ですが、LINQを使用することでコーディングの意図が伝わりやすい場合などもあります。 +これらの挙動を把握した上で、LINQを使用するか、使用する場合のルールなどはプロジェクト内で議論の余地があるかと思います。 + +=={practice_script_csharp_async_await} async/awaitのオーバーヘッドの避け方 +async/awaitはC#5.0で追加された言語機能であり、非同期処理をコールバックを使わず一筋の同期的処理のように記述できるものです。 + +==={practice_script_csharp_async_await_compiler_generated}不要な箇所でのasyncを避ける +asyncを定義されたメソッドは、コンパイラによって非同期処理を実現するためのコードが生成されます。 +そしてasyncキーワードがあれば、コンパイラによるコード生成は必ず行われます。 +そのため、@{possibly_not_async_sample}のように同期的に完了する可能性のあるメソッドも実際にはコンパイラによるコード生成が行われています。 + +//listnum[possibly_not_async_sample][同期的に完了する可能性のある非同期処理][csharp]{ +using System; +using System.Threading.Tasks; + +namespace A { + public class B { + public async Task HogeAsync(int i) { + if (i == 0) { + Console.WriteLine("i is 0"); + return; + } + await Task.Delay(TimeSpan.FromSeconds(1)); + } + + public void Main() { + int i = int.Parse(Console.ReadLine()); + Task.Run(() => HogeAsync(i)); + } + } +} +//} + +この@{possibly_not_async_sample}のような場合は、同期的に終了する可能性のある@{HogeAsync}を分割し、@{complete_async_method_sample}のように実装することで同期的に完了する場合に不要な@{IAsyncStateMachine}実装のステートマシン構造体を生成するコストを省略できます。 + +//embed[latex]{ +\clearpage +//} + +//listnum[complete_async_method_sample][同期処理と非同期処理を分割した実装][csharp]{ +using System; +using System.Threading.Tasks; + +namespace A { + public class B { + public async Task HogeAsync(int i) { + await Task.Delay(TimeSpan.FromSeconds(1)); + } + + public void Main() { + int i = int.Parse(Console.ReadLine()); + if (i == 0) { + Console.WriteLine("i is 0"); + } else { + Task.Run(() => HogeAsync(i)); + } + } + } +} +//} + +====[column] async/awaitの仕組み + +async/await構文はコンパイル時にコンパイラによるコード生成を用いて実現されています。 +asyncキーワードのついたメソッドはコンパイル時点で@{IAsyncStateMachine}を実装した構造体を生成する処理が追加され、await対象の処理が完了するとステートを進めるステートマシンを管理することでasync/awaitの機能を実現しています。 +また、この@{IAsyncStateMachine}は@{System.Runtime.CompilerServices}名前空間に定義されたインタフェースであり、コンパイラのみが使用可能なものとなっています。 + +====[/column] + +==={practice_script_csharp_async_await_thread_context_capture}同期コンテキストのキャプチャを避ける +別スレッドに退避させた非同期処理から、呼び出し元のスレッドに復帰する仕組みが同期コンテキストであり、@{await}を使用することで直前のコンテキストをキャプチャーできます。 +この同期コンテキストのキャプチャは@{await}のたびに行われるため、@{await}ごとのオーバーヘッドが発生します。 +そのため、Unityでの開発に広く利用されているUniTask@{unitask_reference}では、同期コンテキストのキャプチャによるオーバーヘッドを避けるために@{ExecutionContext}と@{SynchronizationContext}を使用しない実装となっています。 +Unityに関してはこのようなライブラリを導入することで、パフォーマンスの改善が見られる場合があります。 + +//footnote[unitask_reference][@{https://tech.cygames.co.jp/archives/3417/}] + +=={practice_script_csharp_stackalloc} stackallocによる最適化 + +配列の確保は通常ヒープ領域に確保されるため、ローカル変数として配列を確保すると、都度GC.Allocが発生してスパイクの原因となります。 +また、ヒープ領域への読み書きはスタック領域と比べると、少しですが効率が悪くなります。 + +そのためC#では、@{unsafe}コード限定で、スタック上に配列を確保するための構文が用意されています。 + +@{how_to_use_stackalloc}のように、@{new}キーワードを用いる代わりに、@{stackalloc}キーワードを用いて配列を確保すると、スタック上に配列が確保されます。 + +//listnum[how_to_use_stackalloc][@{stackalloc}を用いたスタック上への配列確保][csharp]{ +// stackallocはunsafe限定 +unsafe +{ + // スタック上にintの配列を確保 + byte* buffer = stackalloc byte[BufferSize]; +} +//} + +C# 7.2から@{Span}構造体を用いることで、@{how_to_use_safe_stackalloc}に示すように@{unsafe}なしで@{stackalloc}を利用できるようになりました。 + +//listnum[how_to_use_safe_stackalloc][@{Span}構造体を併用したスタック上への配列確保][csharp]{ +Span buffer = stackalloc byte[BufferSize]; +//} + +Unityの場合は2021.2から標準で利用できます。それ以前バージョンの場合は@{Span}が存在しないため、System.Memory.dllを導入する必要があります。 + +@{stackalloc}で確保した配列はスタック専用なため、クラスや構造体のフィールドに持てません。必ずローカル変数として使う必要があります。 + +スタック上への確保とはいえ、要素数が大きい配列の確保はそれなりに処理時間がかかります。 +もしUpdateループ内などのヒープアロケーションを避けたい箇所で要素数の大きい配列を利用したい場合は、初期化時の事前確保か、オブジェクトプールのようなデータ構造を用意して、利用時に貸し出すような実装のほうがよいでしょう。 + +また、@{stackalloc}で確保したスタック領域は、@{関数を抜けるまで解放されない}点に注意が必要です。 +たとえば@{stackalloc_with_loop}に示すコードは、ループ内で確保した配列はすべて保持され、@{Hoge}メソッドを抜けるときに解放されるので、ループを回しているうちにStack Overflowを起こす可能性があります。 + +//listnum[stackalloc_with_loop][@{stackalloc}を用いたスタック上への配列確保][csharp]{ +unsafe void Hoge() +{ + for (int i = 0; i < 10000; i++) + { + // ループ数分配列が蓄積される + byte* buffer = stackalloc byte[10000]; + } +} +//} + +=={practice_script_csharp_sealed} sealedによるIL2CPPバックエンド下でのメソッド呼び出しの最適化 + +UnityでIL2CPPをバックエンドとしてビルドをすると、クラスのvirtualなメソッド呼び出しを実現するために、C++のvtableのような仕組みを用いてメソッド呼び出しを行います@{il2cpp_internal_method_call}。 + +//footnote[il2cpp_internal_method_call][@{https://blog.unity.com/technology/il2cpp-internals-method-calls}] + +具体的には、クラスのメソッド呼び出しの定義ごとに、@{il2cpp_method_call}に示すようなコードが自動生成されます。 + +//listnum[il2cpp_method_call][IL2CPPが生成するメソッド呼び出しに関するC++コード][c++]{ +struct VirtActionInvoker0 +{ + typedef void (*Action)(void*, const RuntimeMethod*); + + static inline void Invoke ( + Il2CppMethodSlot slot, RuntimeObject* obj) + { + const VirtualInvokeData& invokeData = + il2cpp_codegen_get_virtual_invoke_data(slot, obj); + ((Action)invokeData.methodPtr)(obj, invokeData.method); + } +}; +//} + +これはvirutalなメソッドだけでなく、@{コンパイル時に継承をしていない、virtualでないメソッドであっても}同様なC++コードを生成します。 +このような自動生成の挙動によって、@{コードサイズが肥大化したり、メソッド呼び出しの処理時間が増大します}。 + +この問題は、クラスの定義に@{sealed}修飾子をつけることで回避できます@{il2cpp_devirtualization}。 + +//footnote[il2cpp_devirtualization][@{https://blog.unity.com/technology/il2cpp-optimizations-devirtualization}] + +@{il2cpp_method_call_non_sealed}のようなクラスを定義してメソッドを呼び出した場合、IL2CPPで生成されたC++コードでは@{il2cpp_method_call_non_sealed_cpp}のようなメソッド呼び出しが行われます。 + +//listnum[il2cpp_method_call_non_sealed][sealedを用いないクラス定義とメソッド呼び出し][csharp]{ +public abstract class Animal +{ + public abstract string Speak(); +} + +public class Cow : Animal +{ + public override string Speak() { + return "Moo"; + } +} + +var cow = new Cow(); +// Speakメソッドを呼び出す +Debug.LogFormat("The cow says '{0}'", cow.Speak()); +//} + +//listnum[il2cpp_method_call_non_sealed_cpp][@{il2cpp_method_call_non_sealed}のメソッド呼び出しに対応するC++コード][c++]{ +// var cow = new Cow(); +Cow_t1312235562 * L_14 = + (Cow_t1312235562 *)il2cpp_codegen_object_new( + Cow_t1312235562_il2cpp_TypeInfo_var); +Cow__ctor_m2285919473(L_14, /*hidden argument*/NULL); +V_4 = L_14; +Cow_t1312235562 * L_16 = V_4; + +// cow.Speak() +String_t* L_17 = VirtFuncInvoker0< String_t* >::Invoke( + 4 /* System.String AssemblyCSharp.Cow::Speak() */, L_16); +//} + +@{il2cpp_method_call_non_sealed_cpp}に示すように、virtualなメソッド呼び出しではないにもかかわらず@{VirtFuncInvoker0< String_t* >::Invoke}を呼び出しており、virtualメソッドのようなメソッド呼び出しが行われていることが確認できます。 + +一方で、@{il2cpp_method_call_non_sealed}の@{Cow}クラスを@{il2cpp_method_call_sealed}に示すように@{sealed}修飾子を用いて定義すると、@{il2cpp_method_call_sealed_cpp}のようなC++コードが生成されます。 + +//listnum[il2cpp_method_call_sealed][sealedを用いたクラス定義とメソッド呼び出し][csharp]{ +public sealed class Cow : Animal +{ + public override string Speak() { + return "Moo"; + } +} + +var cow = new Cow(); +// Speakメソッドを呼び出す +Debug.LogFormat("The cow says '{0}'", cow.Speak()); +//} + +//listnum[il2cpp_method_call_sealed_cpp][@{il2cpp_method_call_sealed}のメソッド呼び出しに対応するC++コード][c++]{ +// var cow = new Cow(); +Cow_t1312235562 * L_14 = + (Cow_t1312235562 *)il2cpp_codegen_object_new( + Cow_t1312235562_il2cpp_TypeInfo_var); +Cow__ctor_m2285919473(L_14, /*hidden argument*/NULL); +V_4 = L_14; +Cow_t1312235562 * L_16 = V_4; + +// cow.Speak() +String_t* L_17 = Cow_Speak_m1607867742(L_16, /*hidden argument*/NULL); +//} + +このように、メソッド呼び出しが@{Cow_Speak_m1607867742}を呼び出しており、直接メソッドを呼び出していることが確認できます。 + +ただし、比較的最近のUnityでは、このような最適化では一部自動で行われていることをUnity公式で明言しています@{improved_devirtualization_forum}。 + +//footnote[improved_devirtualization_forum][@{https://forum.unity.com/threads/il2cpp-is-sealed-not-worked-as-said-anymore-in-unity-2018-3.659017/#post-4412785}] + +つまり、@{sealed}を明示的に指定しない場合でも、このような最適化が自動で行われている可能性があります。 + +しかし、「[il2cpp] Is `sealed` Not Worked As Said Anymore In Unity 2018.3?」 +@{\footnotemark[8]}というフォーラムで言及している通り、2019年4月の段階で、この実装は完全というわけではありません。 + +このような現状から、IL2CPPの生成するコードを確認しながら、プロジェクトごとにsealed修飾子の設定を決めるとよいでしょう。 + +より確実に直接的なメソッド呼び出しを行うために、また、今後のIL2CPPの最適化を期待して、最適化可能なマークとして@{sealed}修飾子を設定するのもよいかもしれません。 + +=={practice_script_csharp_inline} インライン化による最適化 + +メソッド呼び出しには多少のコストがかかります。 +そのため、C#に限らず一般的な最適化として、比較的小さいメソッドの呼び出しは、コンパイラなどによってインライン化という最適化が行われます。 + +具体的には、@{inline_sample_no_inline}のようなコードに対して、インライン化によって@{inline_sample_inline}のようなコードが生成されます。 + +//listnum[inline_sample_no_inline][インライン化前のコード][csharp]{ +int F(int a, int b, int c) +{ + var d = Add(a, b); + var e = Add(b, c); + var f = Add(d, e); + + return f; +} + +int Add(int a, int b) => a + b; +//} + +//listnum[inline_sample_inline][@{inline_sample_no_inline}に対してインライン化を行ったコード][csharp]{ +int F(int a, int b, int c) +{ + var d = a + b; + var e = b + c; + var f = d + e; + + return f; +} +//} + +インライン化は@{inline_sample_inline}のように、@{inline_sample_no_inline}の@{Func}メソッド内での@{Add}メソッドの呼び出しを、メソッド内の内容をコピーして展開することで行われます。 + +IL2CPPでは、コード生成時にはとくにインライン化による最適化は行われません。 + +しかし、Unity 2020.2からメソッドに@{MethodImpl}属性を指定し、そのパラメーターに@{MethodOptions.AggressiveInlining}を指定することで、生成されるC++コードの対応する関数に@{inline}指定子が付与されるようになりました。 +つまり、C++のコードレベルでのインライン化が行えるようになりました。 + +インライン化のメリットは、メソッド呼び出しのコストが削減されるだけでなく、メソッド呼び出し時に指定した引数のコピーも省けることです。 + +たとえば算術系のメソッドは、@{Vector3}や@{Matrix}のような、比較的サイズの大きい構造体を複数個引数に取ります。 +構造体はそのまま引数として渡すと、すべて値渡しとしてコピーされてメソッドに渡されるため、引数の個数や渡す構造体のサイズが大きいと、メソッド呼び出しと引数のコピーでかなりの処理コストがかかる可能性があります。 +また、物理演算やアニメーションの実装など、定期処理などで利用されることが多いため、メソッド呼び出しが処理負荷として見逃せないケースになることがあります。 + +このようなケースでは、インライン化による最適化は有効です。実際に、Unityの新しい算術系ライブラリである@{Unity.Mathmatics}では、いたるメソッド呼び出しに@{MethodOptions.AggressiveInlining}が指定されています@{unity_mathmatics_inline}。 + +一方で、インライン化はメソッド内の処理を展開する処理のため、展開した分コードサイズが増大するというデメリットがあります。 + +そのため、とくに1フレームで頻繁に呼び出されホットパスとなるようなメソッドに対して、インライン化を検討するとよいでしょう。 +また、属性を指定すると必ずインライン化が行われるわけではない点にも注意が必要です。 + +インライン化されるメソッドは、その中身が小さいものに限定されるため、インライン化を行いたいメソッドは処理を小さく保つ必要があります。 + +また、Unity 2020.2以前では属性指定に対して@{inline}指定子がつかないのと、C++の@{inline}指定子を指定してもインライン化が確実に行われる保証はありません。 + +そのため確実にインライン化を行いたい場合、可読性は落ちますがホットパスとなるメソッドは手動でのインライン化も検討するとよいでしょう。 + +//footnote[unity_mathmatics_inline][@{https://github.com/Unity-Technologies/Unity.Mathematics/blob/f476dc88954697f71e5615b5f57462495bc973a7/src/Unity.Mathematics/math.cs#L1894}] diff --git a/articles/text/tuning_practice_script_unity.re b/articles/text/tuning_practice_script_unity.re new file mode 100644 index 0000000..a9f1fd7 --- /dev/null +++ b/articles/text/tuning_practice_script_unity.re @@ -0,0 +1,360 @@ +={practice_script_unity} Tuning Practice - Script (Unity) + +Unityで提供されている機能を何気なく使っていると思わぬ落とし穴にはまることがあります。 +本章では、Unityの内部実装に関連したパフォーマンスチューニング手法について実例を交えながら紹介します。 + +=={practice_script_unity_event} 空のUnityイベント関数 +@{Awake}、@{Start}、@{Update}などUnityが提供しているイベント関数が定義されている場合、 +実行時にUnity内部のリストにキャッシュされて、リストのイテレーションによって実行されます。 + +関数内で何も処理を行っていなくとも、定義されているだけでキャッシュ対象となるため、 +不要なイベント関数を残したままにするとリストが肥大化し、イテレーションのコストが増大します。 + +たとえば下記サンプルコードのようにUnity上で新規生成したスクリプトには@{Start}、@{Update}が最初から定義されていますが、 +これらの関数が不要であれば必ず削除しておきましょう。 + +//listnum[new_script][Unity上で新規生成したスクリプト][csharp]{ +public class NewBehaviourScript : MonoBehaviour +{ + // Start is called before the first frame update + void Start() + { + + } + + // Update is called once per frame + void Update() + { + + } +} +//} + +=={practice_script_unity_tag_name} tagやnameのアクセス +@{UnityEngine.Object}を継承したクラスには@{tag}プロパティと@{name}プロパティが提供されています。 +オブジェクトの識別に便利なこれらプロパティですが、実はGC.Allocが発生しています。 + +それぞれの実装をUnityCsReferenceから引用しました。 +どちらもネイティブコードで実装された処理を呼び出していることが分かります。 + +UnityではスクリプトをC#で実装しますが、Unity自体はC++で実装されています。 +C#メモリ空間とC++メモリ空間は共有できないため、C++側からC#側に文字列情報を受け渡すためにメモリの確保が行われます。 +これは呼び出すたびに行われるので、複数回アクセスする場合はキャッシュしておきましょう。 + +Unityの仕組みとC#とC++間のメモリに関する詳細は@{basic|basic_unity_output_binary_runtime}を参照してください。 + +//listnum[get_tag][UnityCsReference GameObject.bindings.cs@{UnityCsReference_GameObject}から引用][csharp]{ +public extern string tag +{ + [FreeFunction("GameObjectBindings::GetTag", HasExplicitThis = true)] + get; + [FreeFunction("GameObjectBindings::SetTag", HasExplicitThis = true)] + set; +} +//} + +//listnum[get_name][UnityCsReference UnityEngineObject.bindings.cs@{UnityCsReference_UnityEngineObject}から引用][csharp]{ +public string name +{ + get { return GetName(this); } + set { SetName(this, value); } +} + +[FreeFunction("UnityEngineObjectBindings::GetName")] +extern static string GetName([NotNull("NullExceptionObject")] Object obj); +//} + +//footnote[UnityCsReference_GameObject][@{https://github.com/Unity-Technologies/UnityCsReference/blob/c84064be69f20dcf21ebe4a7bbc176d48e2f289c/Runtime/Export/Scripting/GameObject.bindings.cs}] +//footnote[UnityCsReference_UnityEngineObject][@{https://github.com/Unity-Technologies/UnityCsReference/blob/c84064be69f20dcf21ebe4a7bbc176d48e2f289c/Runtime/Export/Scripting/UnityEngineObject.bindings.cs}] + +=={practice_script_unity_component_cache} コンポーネントの取得 +同じ@{GameObject}にアタッチされている他のコンポーネントを取得する@{GetComponent()}も注意が必要な1つです。 + +前節の@{tag}プロパティや@{name}プロパティ同様にネイティブコードで実装された処理を呼び出していることもそうですが、 +指定した型のコンポーネントを「検索する」コストがかかることにも気をつけなければなりません。 + +下記サンプルコードでは毎フレーム@{Rigidbody}コンポーネントを検索するコストがかかることになります。 +頻繁にアクセスする場合は、あらかじめキャッシュしたものを使い回すようにしましょう。 + +//listnum[get_component][毎フレームGetComponent()するコード][csharp]{ +void Update() +{ + Rigidbody rb = GetComponent(); + rb.AddForce(Vector3.up * 10f); +} +//} + +=={practice_script_unity_transform} transformへのアクセス +@{Transform}コンポーネントは位置や回転、スケール(拡大・縮小)、親子関係の変更など頻繁にアクセスするコンポーネントです。 +下記サンプルコードのように複数の値を更新することも多いでしょう。 + +//listnum[sample_transform_NG][transformにアクセスする例][csharp]{ +void SetTransform(Vector3 position, Quaternion rotation, Vector3 scale) +{ + transform.position = position; + transform.rotation = rotation; + transform.localScale = scale; +} +//} + +@{transform}を取得するとUnity内部では@{GetTransform()}という処理が呼び出されます。 +前節の@{GetComponent()}に比べて最適化されていて高速です。 +しかしキャッシュした場合よりは遅いので、これも下記サンプルコードのようにキャッシュしてアクセスしましょう。 +位置と回転の2つは@{SetPositionAndRotation()}を使うことで関数呼び出し回数を減らすこともできます。 + +//listnum[sample_transform_OK][transformをキャッシュする例][csharp]{ +void SetTransform(Vector3 position, Quaternion rotation, Vector3 scale) +{ + var transformCache = transform; + transformCache.SetPositionAndRotation(position, rotation); + transformCache.localScale = scale; +} +//} + +=={practice_script_unity_destroy} 明示的な破棄が必要なクラス +UnityはC#で開発を行うため、GCによって参照されなくなったオブジェクトは解放されます。 +しかしUnityのいくつかのクラスは明示的に破棄する必要があります。 +代表的な例としては@{Texture2D}、 @{Sprite}、@{Material}、@{PlayableGraph}などです。 +@{new}や専用の@{Create}関数で生成した場合、必ず明示的に破棄を行いましょう。 + +//listnum[sample_create][生成と明示的な破棄][csharp]{ +void Start() +{ + _texture = new Texture2D(8, 8); + _sprite = Sprite.Create(_texture, new Rect(0, 0, 8, 8), Vector2.zero); + _material = new Material(shader); + _graph = PlayableGraph.Create(); +} + +void OnDestroy() +{ + Destroy(_texture); + Destroy(_sprite); + Destroy(_material); + + if (_graph.IsValid()) + { + _graph.Destroy(); + } +} +//} + +=={practice_script_unity_keyword_access} 文字列指定 +@{Animator}の再生するステートの指定、@{Material}の操作するプロパティの指定に文字列を使うのは避けましょう。 + +//embed[latex]{ +\clearpage +//} + +//listnum[sample_keyword_string][文字列指定の例][csharp]{ +_animator.Play("Wait"); +_material.SetFloat("_Prop", 100f); +//} + +これらの関数の内部では@{Animator.StringToHash()}や@{Shader.PropertyToID()}を実行して、文字列から一意な識別値に変換をしています。 +何回もアクセスする場合に都度変換が行われるのはムダなので、識別値をキャッシュしておいて使い回すようにしましょう。 +下記サンプルのようにキャッシュした識別値の一覧となるクラスを定義しておくと、取り回しがよいでしょう。 + +//listnum[sample_keyword_cache][識別値のキャッシュの例][csharp]{ +public static class ShaderProperty +{ + public static readonly int Color = Shader.PropertyToID("_Color"); + public static readonly int Alpha = Shader.PropertyToID("_Alpha"); + public static readonly int ZWrite = Shader.PropertyToID("_ZWrite"); +} +public static class AnimationState +{ + public static readonly int Idle = Animator.StringToHash("idle"); + public static readonly int Walk = Animator.StringToHash("walk"); + public static readonly int Run = Animator.StringToHash("run"); +} +//} + +=={practice_script_unity_json_utility} JsonUtilityの落とし穴 +UnityではJSONのシリアライズ/デシリアライズのために@{JsonUtility}というクラスが提供されています。 +公式ドキュメント@{Unity_JSONSerialization}にもC#標準のものよりも高速であることが記載されていて、パフォーマンスを意識した実装をするなら利用することも多いでしょう。 +//footnote[Unity_JSONSerialization][@{https://docs.unity3d.com/ja/current/Manual/JSONSerialization.html}] + +//quote{ +JsonUtilityは(機能は .NET JSON より少ないですが)、よく使用されている .NET JSON よりも著しく早いことが、ベンチマークテストで示されています。 +//} + +しかしパフォーマンスに関わることでひとつ気をつけるべきことがあります。 +それは「@{null}の扱い」です。 + +下記サンプルコードでシリアライズ処理とその結果を示しています。 +クラスAのメンバーb1を明示的に@{null}にしているにもかかわらず、クラスBおよびクラスCをデフォルトコンストラクターで生成した状態でシリアライズされているのが分かります。 +このようにシリアライズ対象となるフィールドに@{null}があった場合、JSON化の際にダミーオブジェクトが@{new}されるので、そのオーバーヘッドは考慮しておいたほうがよいでしょう。 + +//listnum[sample_json][シリアライズの挙動][csharp]{ +[Serializable] public class A { public B b1; } +[Serializable] public class B { public C c1; public C c2; } +[Serializable] public class C { public int n; } + +void Start() +{ + Debug.Log(JsonUtility.ToJson(new A() { b1 = null, })); + // {"b1":{"c1":{"n":0},"c2":{"n":0}}} +} +//} + +=={practice_script_unity_material_leak} RenderやMeshFilterの落とし穴 +@{Renderer.material}で取得したマテリアル、@{MeshFilter.mesh}で取得したメッシュは複製されたインスタンスなので +使い終わったら明示的な破棄が必要です。 +公式ドキュメント@{Unity_Renderer_material}@{Unity_MeshFilter_mesh}にもそれぞれ下記のように明記されています。 +//footnote[Unity_Renderer_material][@{https://docs.unity3d.com/ja/current/ScriptReference/Renderer-material.html}] +//footnote[Unity_MeshFilter_mesh][@{https://docs.unity3d.com/ja/current/ScriptReference/MeshFilter-mesh.html}] + +//quote{ +If the material is used by any other renderers, this will clone the shared material and start using it from now on. +//} + +//quote{ +It is your responsibility to destroy the automatically instantiated mesh when the game object is being destroyed. +//} + +取得したマテリアルやメッシュはメンバー変数に保持しておき、然るべきタイミングで破棄するようにしましょう。 + +//listnum[sample_destroy_copy_material][複製されたマテリアルの明示的な破棄][csharp]{ +void Start() +{ + _material = GetComponent().material; +} + +void OnDestroy() +{ + if (_material != null) { + Destroy(_material); + } +} +//} + + +=={practice_script_unity_log_delete} ログ出力コードの除去 +Unityでは@{Debug.Log()}、@{Debug.LogWarning()}、@{Debug.LogError()}といったログ出力用の関数が提供されています。 +便利な機能ではありますがいくつかの問題点もあります。 + + * ログ出力自体がそこそこ重たい処理 + * リリースビルドでも実行される + * 文字列の生成や連結によってGC.Allocが発生する + +UnityのLogging設定をオフにした場合、スタックトレースは停止しますが、ログは出力されます。 +@{UnityEngine.Debug.unityLogger.logEnabled}にUnityでは@{false}を設定すると、ログは出力されませんが、 +関数内部で分岐しているだけなので、関数の呼び出しコストや不要なはずの文字列の生成や連結は行われてしまいます。 +@{#if}ディレクティブを使うという手段もありますが、すべてのログ出力処理に手を入れるのは現実的ではありません。 + +//listnum[sample_directive][#ifディレクティブ][csharp]{ +#if UNITY_EDITOR + Debug.LogError($"Error {e}"); +#endif +//} + +このような場合に活用できるのが@{Conditional}属性です。 +@{Conditional}属性が付いた関数は、指定したシンボルが定義されていない場合、コンパイラによって呼び出し部分が除去されます。 +@{sample_conditional}のサンプルのように、自作のログ出力クラスを通してUnity側のログ機能を呼び出すのをルールとして、自作クラス側の各関数に@{Conditional}属性を付加することで、 +必要に応じて関数の呼び出しごと除去できるようにするとよいでしょう。 + +//embed[latex]{ +\clearpage +//} + +//listnum[sample_conditional][Conditional属性の例][csharp]{ +public static class Debug +{ + private const string MConditionalDefine = "DEBUG_LOG_ON"; + + [System.Diagnostics.Conditional(MConditionalDefine)] + public static void Log(object message) + => UnityEngine.Debug.Log(message); +} +//} + +注意点として、指定したシンボルが関数の呼び出し側から参照できる必要があるということです。 +@{#define}で定義されたシンボルのスコープは、記述したファイル内に限定されてしまいます。 +@{Conditional}属性が付いた関数を呼び出しているすべてのファイルにシンボルを定義していくのは現実的ではありません。 +UnityにはScripting Define Symbolsというプロジェクト全体に対してシンボルを定義する機能があるので活用しましょう。 +「Project Settings -> Player -> Other Settings」で設定ができます。 + +//image[practice_script_unity_define][Scripting Define Symbols] + +=={practice_script_unity_burst} Burstを用いたコードの高速化 + +Burst@{burst}はUnity公式が開発する、ハイパフォーマンスなC#スクリプティング行うためのコンパイラです。 +//footnote[burst][@{https://docs.unity3d.com/Packages/com.unity.burst@1.6/manual/docs/QuickStart.html}] + +BurstではC#のサブセット言語を用いてコードを記述します。 +BurstがC#コードをLLVMというコンパイラ基盤@{llvm}の中間構文であるIR(Intermediate Representation)に変換し、IRを最適化をした上で機械語に変換されます。 + +//footnote[llvm][@{https://llvm.org/}] + +このときにコードを可能な限りベクトル化し、SIMDという命令を積極的に使った処理に置き換えます。これによって、より高速なプログラムの出力が期待できます。 + +SIMDはSingle Instruction/Multiple Dataの略で、単一の命令を同時に複数のデータに適用するような命令を指します。 +つまりSIMD命令を積極的に利用することで、1命令でデータがまとめて処理されるため、通常の命令と比べて高速に動作します。 + +==={practice_script_csharp_optimize_code_with_burst} Burstを用いたコードの高速化 + +BurstではHigh Performance C#(HPC#)@{burst_hpc}と呼ばれるC#のサブセット言語を用いてコードを記述します。 +//footnote[burst_hpc][@{https://docs.unity3d.com/Packages/com.unity.burst@1.7/manual/docs/CSharpLanguageSupport_Types.html}] + +HPC#の特徴の1つとしてC#の参照型、つまりクラスや配列などが利用できません。そのため原則として構造体を用いてデータ構造を記述します。 + +配列のようなコレクションは代わりに@{NativeArray}などのNativeContainer@{burst_native_container}を利用します。HPC#の詳細については脚注記載のドキュメントを参考にしてください。 +//footnote[burst_native_container][@{https://docs.unity3d.com/Manual/JobSystemNativeContainer.html}] + +BurstはC# Job Systemと組み合わせて利用します。そのため自身の処理を@{IJob}を実装したジョブの@{Execute}メソッド内に記述します。 +定義したジョブに@{BurstCompile}属性を付与することで、そのジョブがBurstによって最適化されます。 + +@{burst_job}に、与えられた配列の各要素を二乗して@{Output}配列に格納する例を示します。 + +//listnum[burst_job][簡単な検証用のJob実装][csharp]{ +[BurstCompile] +private struct MyJob : IJob +{ + [ReadOnly] + public NativeArray Input; + + [WriteOnly] + public NativeArray Output; + + public void Execute() + { + for (int i = 0; i < Input.Length; i++) + { + Output[i] = Input[i] * Input[i]; + } + } +} +//} + +@{burst_job}の14行目の各要素はそれぞれ独立して計算でき(計算に順序依存がない)、かつ出力配列のメモリアライメントは連続しているためSIMD命令を用いてまとめて計算が可能です。 + +コードがどのようなアセンブリに変換されるかは、@{burst_inspector}のようにBurst Inspectorを用いて確認できます。 + +//image[burst_inspector][Burst Inspectorを用いることで、コードがどのようなアセンブリに変換されるか確認できる][scale=1.0] + +@{burst_job}の14行目の処理は、ARMV8A_AARCH64向けのアセンブリで@{burst_job_asm_simd}に変換されます。 + +//listnum[burst_job_asm_simd][@{burst_job}の14行目のARMV8A_AARCH64向けのアセンブリ]{ + fmul v0.4s, v0.4s, v0.4s + fmul v1.4s, v1.4s, v1.4s +//} + +アセンブリのオペランドに、@{.4s}というサフィックスがついていることから、SIMD命令が利用されていることが確認できます。 + +ピュアなC#により実装されたコードとBurstにより最適化されたコードのパフォーマンスを実機で比較します。 + +実機にはAndroid Pixel 4a、IL2CPPをスクリプトバックエンドとしてビルドを行い比較しています。また配列のサイズは2^20 = 1,048,576としています。 +計測は同じ処理を10回繰り返し、処理時間の平均をとりました。 + +@
{burst_comp}にパフォーマンス比較の計測結果を示します。 + +//tsize[|latex||l|r|] +//table[burst_comp][ピュアなC#実装とBurstによる最適化されたコードの処理時間の比較]{ +手法 処理時間(非表示) +------------------------------------------------------------- +ピュアなC#実装 5.73ms +Burstによる実装 0.98ms +//} + +ピュアなC#実装と比べて約5.8倍ほど高速化を確認できました。 \ No newline at end of file diff --git a/articles/text/tuning_practice_third_party.re b/articles/text/tuning_practice_third_party.re new file mode 100644 index 0000000..1c47bd3 --- /dev/null +++ b/articles/text/tuning_practice_third_party.re @@ -0,0 +1,202 @@ +={practice_third_party} Tuning Practice - Third Party + +この章では、Unityでゲームを開発する際によく使用されるサードパーティライブラリを導入する上で、パフォーマンスの観点から気をつけるべきことを紹介します。 + + +=={practice_third_party_dotween} DOTween +DOTween@{dotween}は、スクリプトで滑らかなアニメーションを実現できるライブラリです。 +たとえば、拡大して縮小するアニメーションは以下のコードのように簡単に記述できます。 + +//listnum[dotween_example][DOTween使用例][csharp]{ +public class Example : MonoBehaviour { + public void Play() { + DOTween.Sequence() + .Append(transform.DOScale(Vector3.one * 1.5f, 0.25f)) + .Append(transform.DOScale(Vector3.one, 0.125f)); + } +} +//} + +==={practice_third_party_dotween_set_auto_kill} SetAutoKill +@{DOTween.Sequence()}や@{transform.DOScale(...)}など、Tweenを生成する処理は基本的にメモリアロケーションを伴うため、 +頻繁に再生されるアニメーションはインスタンスの再利用を検討しましょう。 + +デフォルトではアニメーション完了時に自動的にTweenが破棄されてしまうので、@{SetAutoKill(false)}でこれを抑制します。 +最初の使用例は、次のコードに置き換えることができます。 + +//embed[latex]{ +\clearpage +//} + +//listnum[dotween_reuse_tween][Tweenインスタンスを再利用する][csharp]{ + private Tween _tween; + + private void Awake() { + _tween = DOTween.Sequence() + .Append(transform.DOScale(Vector3.one * 1.5f, 0.25f)) + .Append(transform.DOScale(Vector3.one, 0.125f)) + .SetAutoKill(false) + .Pause(); + } + + public void Play() { + _tween.Restart(); + } +//} + +@{SetAutoKill(false)}を呼び出したTweenは、明示的に破棄しなければリークしてしまうため注意が必要です。 +不要になったタイミングで@{Kill()}を呼び出すか、後述する@{SetLink}を使用するとよいでしょう。 + +//listnum[dotween_kill_tween][Tweenを明示的に破棄する][csharp]{ + private void OnDestroy() { + _tween.Kill(); + } +//} + +==={practice_third_party_dotween_set_link} SetLink +@{SetAutoKill(false)}を呼び出したTweenや、@{SetLoops(-1)}で無限に繰り返し再生されるようにしたTweenは自動的には破棄されなくなるため、そのライフタイムを自前で管理する必要があります。 +そのようなTweenには@{SetLink(gameObject)}で関連するGameObjectと紐付け、GameObjectがDestroyされると同時にTweenも破棄されるようにするとよいでしょう。 + +//listnum[dotween_restart_tween][TweenをGameObjectのライフタイムに紐付ける][csharp]{ + private void Awake() { + _tween = DOTween.Sequence() + .Append(transform.DOScale(Vector3.one * 1.5f, 0.25f)) + .Append(transform.DOScale(Vector3.one, 0.125f)) + .SetAutoKill(false) + .SetLink(gameObject) + .Pause(); + } +//} + +==={practice_third_party_dotween_inspector} DOTween Inspector +Unity Editorで再生中に、@{[DOTween]}という名前のGameObjectを選択することで、Inspector上からDOTweenの状態や設定を確認できます。 + +//image[dotween_inspector_game_object][[DOTween\] GameObject][scale=0.5] +//image[dotween_inspector][DOTween Inspector][scale=0.5] + +関連するGameObjectが破棄されているにもかかわらず動き続けているTweenがないか、 +また、Pause状態で破棄されずにリークしているTweenがないかなど調査するときに役立ちます。 + +//footnote[dotween][@{http://dotween.demigiant.com/index.php}] + + +=={practice_third_party_unirx} UniRx +UniRx@{unirx}はUnityに最適化されたReactive Extensionsを実装したライブラリです。 +豊富なオペレーター群やUnity向けのヘルパーにより、複雑な条件のイベントハンドリングを簡潔に記述できます。 + +==={practice_third_party_unirx_add_to} 購読の解除 +UniRxでは、ストリーム発行元の@{IObservable}に対して購読 (@{Subscribe}) することで、そのメッセージの通知を受け取ることができます。 + +この購読時に、通知を受け取るためのオブジェクトや、メッセージを処理するコールバックなどのインスタンスが生成されます。 +これらのインスタンスが@{Subscribe}した側の寿命を超えてメモリに残り続けるのを避けるため、 +通知を受け取る必要がなくなった場合は、基本的に@{Subscribe}した側の責任で購読を解除しましょう。 + +購読を解除する方法はいくつかありますが、パフォーマンスを考慮する場合@{Subscribe}の戻り値の@{IDisposable}を保持して明示的に@{Dispose}するのがよいでしょう。 + +//listnum[unirx_unsbscribe][][csharp]{ +public class Example : MonoBehaviour { + private IDisposable _disposable; + + private void Awake() { + _disposable = Observable.EveryUpdate() + .Subscribe(_ => { + // 毎フレーム実行する処理 + }); + } + + private void OnDestroy() { + _disposable.Dispose(); + } +} +//} + +またMonoBehaviourを継承したクラスであれば@{AddTo(this)}を呼ぶことで、自身がDestroyされるタイミングで自動的に解除することもできます。 +Destroyを監視するため内部的に@{AddComponent}が呼ばれるオーバーヘッドがありますが、記述が簡潔になるこちらを利用するのもよいでしょう。 + +//listnum[unirx_monobehaviour_add_to_this][][csharp]{ + private void Awake() { + Observable.EveryUpdate() + .Subscribe(_ => { + // 毎フレーム実行する処理 + }) + .AddTo(this); + } +//} + +//footnote[unirx][@{https://github.com/neuecc/UniRx}] + + +=={practice_third_party_unitask} UniTask +UniTaskはUnityでハイパフォーマンスな非同期処理を実現するための強力なライブラリで、 +値型ベースの@{UniTask}型によりゼロアロケーションで非同期処理を行えることが特徴です。 +またUnityのPlayerLoopに沿った実行タイミングの制御も可能なため、従来のコルーチンを完全に置き換えることができます。 + +==={practice_third_party_unitask_v2} UniTask v2 +UniTaskは2020年6月、メジャーバージョンアップになるUniTask v2がリリースされました。 +UniTask v2はasyncメソッド全体のゼロアロケーション化など大幅な性能改善と、 +非同期LINQ対応や外部アセットのawaitサポートなどの機能追加が行われています。@{unitask_v2} + +一方で、@{UniTask.Delay(...)}などFactoryで返すタスクが呼び出し時に起動されたり、 +通常の@{UniTask}インスタンスへの複数回awaitが禁止@{unitask_v2_multiple_await}されるなど、 +破壊的な変更も含まれるためUniTask v1からアップデートする際は注意が必要です。 +しかしながらアグレッシブな最適化によりさらにパフォーマンスが向上しているため、基本的にはUniTask v2を使用するとよいでしょう。 + +//footnote[unitask_v2][@{https://tech.cygames.co.jp/archives/3417/}] +//footnote[unitask_v2_multiple_await][@{UniTask.Preserve}を使用することで複数回awaitできるUniTaskに変換することが可能です。] + +==={practice_third_party_unitask_unitask_tracker} UniTask Tracker +@{UniTask Tracker}を使用することで待機中のUniTaskと、その生成時のスタックトレースを可視化できます。 +//image[unitask_tracker][UniTask Tracker][scale=0.75] + +たとえば、何かに衝突したら@{_hp}が1ずつ減っていくMonoBehaviourがあるとします。 + +//listnum[unitask_leak_task][][csharp]{ +public class Example : MonoBehaviour { + private int _hp = 10; + + public UniTask WaitForDeadAsync() { + return UniTask.WaitUntil(() => _hp <= 0); + } + + private void OnCollisionEnter(Collision collision) { + _hp -= 1; + } +} +//} + +このMonoBehaviourの@{_hp}が減り切る前にDestroyされた場合、 +それ以上@{_hp}が減ることはないため@{WaitForDeadAsync}の戻り値の@{UniTask}は完了する機会を失い、 +そのままずっと待機し続けてしまいます。 + +このように終了条件の設定ミスなどでリークしている@{UniTask}がないか、このツールを用いて確認するとよいでしょう。 + +====[column] タスクのリークを防ぐ + +例示したコードでタスクがリークするのは、終了条件を満たす前に自身がDestroyされるケースを考慮できていないのが原因でした。 + +これには、シンプルに自身がDestroyされていないかチェックする。 +または自身への@{this.GetCancellationTokenOnDestroy()}で得られる@{CancellationToken}を@{WaitForDeadAsync}へ渡し、 +Destroy時にタスクがキャンセルされるようにする、といった対応が考えられます。 + +//listnum[unitask_fix_leak_task][][csharp]{ + // 自身がDestroyされているかチェックするパターン + public UniTask WaitForDeadAsync() { + return UniTask.WaitUntil(() => this == null || _hp <= 0); + } + + // CancellationTokenを渡すパターン + public UniTask WaitForDeadAsync(CancellationToken token) { + return UniTask.WaitUntil( + () => _hp <= 0, + cancellationToken: token); + } +//} + +//listnum[unitask_fix_leak_task_caller][WaitForDeadAsync(CancellationToken) 呼び出し例][csharp]{ + Example example = ... + var token = example.GetCancellationTokenOnDestroy(); + await example.WaitForDeadAsync(token); +//} + +Destroy時に前者の@{UniTask}は何事もなく完了しますが、後者は@{OperationCanceledException}が投げられます。 +どちらの挙動が望ましいかは状況によって異なりますので、適切な実装を選択するとよいでしょう。 diff --git a/articles/text/tuning_practice_ui.re b/articles/text/tuning_practice_ui.re new file mode 100644 index 0000000..fba768a --- /dev/null +++ b/articles/text/tuning_practice_ui.re @@ -0,0 +1,197 @@ +={practice_ui} Tuning Practice - UI +Unity標準のUIシステムであるuGUIと、画面にテキストを描画する仕組みであるTextMeshProについて、 +チューニングプラクティスを紹介します。 + +=={practice_ui_rebuild} Canvasの分割 + +uGUIでは@{Canvas}内の要素に変化があったとき、 +@{Canvas}全体のUIのメッシュを再構築する処理(リビルド)が走ります。 +変化とは、アクティブ切り替えや移動やサイズの変更など、 +見た目が大きく変わるようなものから一見ではわからないような細かいものまで、あらゆる変更を指します。 +リビルドの処理のコストは高いため、実行される回数が多かったり +@{Canvas}内のUIの数が多かったりするとパフォーマンスに悪影響を及ぼします。 + +これに対して、ある程度のUIのまとまりごとに@{Canvas}を分割することで、 +リビルドのコストを抑えることができます。 +たとえば、アニメーションで動くUIと何も動かないUIがあったとき、 +それらを別の@{Canvas}の下に配置することで、 +アニメーションによるリビルドの対象となるものを最小限にできます。 + +ただし、@{Canvas}を分割すると描画のバッチが効かなくなるため、 +どのように分割すればよいかに関しては注意深く考える必要があります。 + +//info{ + +@{Canvas}の分割は、@{Canvas}の配下に@{Canvas}を入れ子で配置する場合でも有効です。 +子の@{Canvas}に含まれる要素が変化しても、子の@{Canvas}のリビルドが走るだけで親の@{Canvas}のリビルドは走りません。 +ただし詳しく確認したところ、@{SetActive}によって子の@{Canvas}内のUIをアクティブ状態に切り替えたときは事情が違うようです。 +このとき、親の@{Canvas}内にUIが大量に配置されている場合は高負荷になる現象があるようです。 +なぜそのような挙動になるのかの詳細は分かりませんが、入れ子の@{Canvas}内のUIのアクティブ状態を切り替えるときは注意が必要そうです。 + +//} + +=={practice_ui_unity_white} UnityWhite + +UIの開発をしていると、単純な長方形型のオブジェクトを表示したいということがよくあります。 +そこで注意するべきなのが、UnityWhiteの存在です。 +UnityWhiteは、@{Image}コンポーネントや@{RawImage}コンポーネントで +利用する画像を指定しなかったとき(@{none_image})に使われるUnity組み込みのテクスチャです。 +UnityWhiteが使われている様子はFrame Debuggerで確認できます(@{unity_white})。 +この仕組みを使うと白色の長方形を描画できるため、 +これに乗算する色を組み合わせることによって単純な長方形型の表示を実現できます。 + +//image[none_image][UnityWhiteの利用][scale=0.8] +//image[unity_white][UnityWhiteが使われている様子][scale=0.8] + +しかし、UnityWhiteはプロジェクトで用意したSpriteAtlasとは別のテクスチャであるため、 +描画のバッチが途切れてしまうという問題が起こります。 +これによって、ドローコールが増加し描画効率が悪化してしまいます。 + +そのため、SpriteAtlasに小さい(たとえば4 × 4ピクセルの)白色正方形の画像を追加し、 +そのSpriteを利用して単純な長方形を描画するようにするべきです。 +これによって、同じSpriteAtlasを使っていれば同一マテリアルになるため、バッチを効かせることができます。 + +=={practice_ui_layout_component} Layoutコンポーネント + +uGUIにはオブジェクトをきれいに整列させるための機能を持つLayoutコンポーネントが用意されています。 +たとえば縦方向に整列するなら@{VerticalLayoutGroup}、 +グリッド上に整列するなら@{GridLayoutGroup}が使われます(@{layout_group})。 + +//image[layout_group][左側が@{VerticalLayoutGroup}、右側が@{GridLayoutGroup}を使った例][scale=0.99] + +Layoutコンポーネントを利用すると、対象のオブジェクトを生成したときや、特定のプロパティを編集したときにLayoutのリビルドが発生します。 +Layoutのリビルドも、メッシュのリビルドと同様にコストの高い処理です。 + +Layoutのリビルドによるパフォーマンス低下を避けるには、 +Layoutコンポーネントを極力使わないというのが有効です。 + +たとえば、テキストの内容に応じて配置が変わるといった動的な配置が必要ないのであれば、 +Layoutコンポーネントを使う必要がありません。 +本当に動的な配置が必要な場合も、画面上で多く使用される場合などは、 +独自のスクリプトで制御したほうがよい場合もあります。 +また、親のサイズが変わっても親から見て特定の位置に配置したいという要件であれば、 +@{RectTransform}のアンカーを調整することで実現できます。 +プレハブを作成するときに配置に便利だからという理由でLayoutコンポーネントを使った場合は、 +必ず削除して保存するようにしましょう。 + +=={practice_ui_raycast_target} Raycast Target +@{Image}や@{RawImage}のベースクラスである@{Graphic}には、 +Raycast Targetというプロパティがあります(@{raycast_target})。 +このプロパティを有効にすると、その@{Graphic}がクリックやタッチの対象になります。 +画面をクリックしたりタッチしたりしたとき、このプロパティが有効なオブジェクトが +処理の対象となるため、できる限りこのプロパティを無効にすることでパフォーマンスを向上できます。 + +//image[raycast_target][Raycast Targetプロパティ][scale=0.8] + +このプロパティはデフォルトで有効ですが、 +実際のところ多くの@{Graphic}ではこのプロパティを有効にする必要がありません。 +一方、Unityではプリセット@{preset}と呼ばれる機能があり、デフォルトの値をプロジェクトで変更することが可能です。 +具体的には、@{Image}コンポーネントと@{RawImage}コンポーネントに対してそれぞれプリセットを作成し、 +それをProject Settingsのプリセットマネージャーからデフォルトのプリセットとして登録します。 +この機能を使ってRaycast Targetプロパティをデフォルトで無効にしてもよいかもしれません。 + +//footnote[preset][@{https://docs.unity3d.com/ja/current/Manual/Presets.html}] + +=={practice_ui_mask} マスク + +uGUIでマスクを表現するには、@{Mask}コンポーネントか@{RectMask2d}コンポーネントを利用します。 + +@{Mask}ではステンシルを利用してマスクを実現しているため、コンポーネントが増えるたびに描画コストが大きくなります。 +それに対して@{RectMask2d}はシェーダーのパラメーターでマスクを実現しているため描画コストの増加が抑えられています。 +ただし、@{Mask}は好きな形でくり抜ける一方、@{RectMask2d}は長方形でしかくり抜けないという制約があります。 + +利用できるなら@{RectMask2d}を選択するべきだというのが通説ですが、 +最近のUnityでは@{RectMask2d}の利用にも注意が必要です。 + +具体的には、@{RectMask2d}が有効のとき、そのマスク対象が増えるに連れ、 +それに比例して毎フレームカリングのCPU負荷が発生します。 +UIを何も動かさなくても毎フレーム負荷が発生するこの現象は、uGUIの内部実装のコメントを見る限りUnity 2019.3で入ったとあるissue@{mask_issue}の修正の +副作用によるもののようです。 +//footnote[mask_issue][@{https://issuetracker.unity3d.com/issues/rectmask2d-diffrently-masks-image-in-the-play-mode-when-animating-rect-transform-pivot-property}] + +そのため、@{RectMask2d}も極力使わないようにする、 +使ったとしても必要ない状態のときは@{enabled}を@{false}にする、 +マスク対象は必要最低限にするなどの対策を取ることが有効です。 + +=={practice_ui_text_mesh_pro} TextMeshPro +TextMeshProでテキストを設定する一般的な方法は@{text}プロパティにテキストを代入する方法ですが、 +それとは別に@{SetText}というメソッドを使う方法があります。 + +@{SetText}には多くのオーバーロードが存在しますが、 +たとえば文字列と@{float}型の値を引数に取るものがあります。 +このメソッドを @{setText} のように利用すると、第2引数の値を表示できます。 +ただし、@{label}は@{TMP_Text}(もしくはそれを継承した)型、 +@{number}は@{float}型の変数であるとします。 + +//listnum[setText][SetTextの利用例][csharp]{ +label.SetText("{0}", number); +//} + +この方法の利点は、文字列の生成コストを抑えられるという点です。 + +//listnum[noSetText][SetTextを使わない例][csharp]{ +label.text = number.ToString(); +//} + +@{noSetText} のように@{text}プロパティを使う方法では、 +@{float}型の@{ToString()}が実行されるのでこの処理が実行されるたびに文字列の生成コストが発生します。 +それに対して@{SetText}を使った方法は、文字列を極力生成しないような工夫が行われているため、 +とくに頻繁に表示するテキストが変わるような場合、パフォーマンス的に有利です。 + +またこのTextMeshProの機能は、ZString@{zstring}と組み合わせると非常に強力なものになります。 +ZStringは文字列生成におけるメモリアロケーションを削減できるライブラリです。 +ZStringは@{TMP_Text}型に対する多くの拡張メソッドを提供しており、 +それらのメソッドを使うことで文字列の生成コストを抑えつつ柔軟なテキスト表示を実現できます。 + +//footnote[zstring][@{https://github.com/Cysharp/ZString}] + +=={practice_ui_active} UIの表示切り替え + +uGUIのコンポーネントは、@{SetActive}によるオブジェクトのアクティブ切り替えのコストが大きいという特徴があります。 +これは、@{OnEnable}で各種リビルドのDirtyフラグを立てたり、マスクに関する初期化を行ったりしていることが原因です。 +そのため、UIの表示非表示の切り替えの方法として、@{SetActive}による方法以外の選択肢も検討することが重要です。 + +まず1つ目の方法は、@{Canvas}の@{enabled}を@{false}にするという方法です(@{canvas_disable})。 +これによって、@{Canvas}配下のオブジェクトがすべて描画されなくなります。 +そのためこの方法は、@{Canvas}配下のオブジェクトを丸ごと非表示にしたい場合のみにしか使えないという欠点があります。 + +//image[canvas_disable][@{Canvas}を無効にする][scale=0.8] + +もう1つの方法は、@{CanvasGroup}を使った方法です。 +@{CanvasGroup}には、その配下のオブジェクトの透明度を一括で調整できる機能があります。 +この機能を利用して、透明度を0にしてしまえば、 +その@{CanvasGroup}配下のオブジェクトをすべて非表示にできます(@{canvas_group})。 + +//image[canvas_group][@{CanvasGroup}の透明度を0にする][scale=0.8] + +これらの方法は@{SetActive}による負荷を避けることが期待できますが、 +@{GameObject}はアクティブ状態のままとなるため注意が必要な場合もあります。 +たとえば@{Update}メソッドが定義されている場合には、その処理は非表示の状態でも実行され続けるため、 +思わぬ負荷の向上に繋がってしまうかもしれないことに気をつけましょう。 + +参考までに、@{Image}コンポーネントを付けた1280個の@{GameObject}に対して、 +それぞれの手法で表示非表示の切り替えをしたときの処理時間を計測しました(@
{activation_time})。 +処理時間はUnityエディターで計測し、Deep Profileは用いていません。 +実際に切り替えを行ったまさにその処理の実行時間@{activation_profile}と、 +そのフレームでの@{UIEvents.WillRenderCanvases}の実行時間を足し合わせたものを +その手法の処理時間としています。 +@{UIEvents.WillRenderCanvases}の実行時間を足し合わせているのは、 +この中でUIのリビルドが実行されるためです。 + +//footnote[activation_profile][たとえば@{SetActive}なら、@{SetActive}メソッドを呼び出す部分を@{Profiler.BeginSample}と@{Profiler.EndSample}で囲って計測しています。] + +#@# SetActive 210.41+113.38=323.79 208.24+1.69=209.93 +#@# Canva 8.99+52.26=61.25 9.07+52.16=61.23 +#@# CanvasGroup 0.95+2.69=3.64 0.97+2.43=3.40 + +//tsize[|latex||l|r|r|] +//table[activation_time][表示切り替えの処理時間]{ +手法 処理時間(表示) 処理時間(非表示) +------------------------------------------------------------- +@{SetActive} 323.79ms 209.93ms +@{Canvas}の@{enabled} 61.25ms 61.23ms +@{CanvasGroup}の@{alpha} 3.64ms 3.40ms +//} + +@
{activation_time}の結果から、 +今回試したシチュエーションではCanvasGroupを使った手法が圧倒的に処理時間が短いことがわかりました。 diff --git a/articles/text/tuning_start.re b/articles/text/tuning_start.re new file mode 100644 index 0000000..fbd4267 --- /dev/null +++ b/articles/text/tuning_start.re @@ -0,0 +1,539 @@ +={start} パフォーマンスチューニングを始めよう +本章ではパフォーマンスチューニングを行うにあたり必要な事前準備や、取り組む際のフローに関して解説します。 + +まずはパフォーマンスチューニングを始める前に決めておくべきことや、考慮すべきことについて取り上げます。 +プロジェクトがまだ初期フェーズの場合はぜひ目を通してみてください。 +ある程度プロジェクトが進んでいたとしても、記載されている内容が考慮されているか改めてチェックするのもよいでしょう。 +次に性能低下が発生しているアプリケーションに対して、どのように取り組んでいくべきかを解説します。 +原因の切り分け方とその解決手法を学ぶことで、パフォーマンスチューニングの一連のフローを実践できるようになるでしょう。 + +=={start_ready} 事前準備 +パフォーマンスチューニングの前に達成したい指標を決めましょう。言葉にすると簡単ですが実は難易度が高い作業です。 +それは世の中にはさまざまなスペックの端末が溢れかえっており、低スペック端末を使っているユーザーを無視することはできないからです。 +そのような状況の中でゲーム仕様やターゲットユーザー層、海外展開の有無など、さまざまなことを考慮する必要があります。 +この作業はエンジニアだけでは完結しません。他職種の方と協議しながらクオリティラインを決める必要があり、技術検証も必要になるでしょう。 + +これらの指標は負荷を測るほどの機能実装やアセットが存在しない初期フェーズから決めることは難易度が高いです。 +そのためある程度プロジェクトが進んでから決めるのも1つの手です。 +ただしプロジェクトが@{量産フェーズに入る前までに必ず決める}ように心掛けておきましょう。 +一度量産を開始すると変更コストが莫大なものになるためです。 +本節で紹介する指標を決めるには時間がかかりますが、焦らずしっかり進めていきましょう。 + +====[column] 量産フェーズ後の仕様変更の恐ろしさ + +いま量産フェーズ後でありながら、低スペック端末で描画にボトルネックのあるプロジェクトがあるとします。 +メモリはすでに限界近い使用量なので、距離によって低負荷モデルに切り替える手法も使えません。 +そのためモデルの頂点数を削減することにしました。 + +まずはデータをリダクションするために発注し直します。新たに発注書が必要になるでしょう。 +次にディレクターが品質をチェックし直す必要があります。そして最後にデバッグする必要もあります。 +簡易的に書きましたが、実際にはより細かいオペレーションやスケジュール調整があるでしょう。 + +上記のような対応を必要とするアセットが、量産後ともなると数十〜数百個はあるでしょう。 +これは時間と労力が大変掛かるため、プロジェクトにとって致命傷となりかねません。 + +このような事態を防ぐために@{もっとも負荷の掛かるシーンを作成}し、指標を満たしているか@{事前に検証を行う}ことがとても大事なのです。 + +====[/column] + +==={start_ready_objective} 指標を決める +指標を決めると目指すべき目標が定まります。逆に指標がないといつまで経っても終わりません。 +@
{table_object_tuning}に決めるとよい指標を取り上げます。 +//table[table_object_tuning][指標]{ +項目 要素 +-------------------- +フレームレート 常時どのぐらいのフレームレートを目指すか +メモリ どの画面でメモリが最大になるか試算し、限界値を決める +遷移時間 遷移時間待ちはどれぐらいが適切か +熱 連続プレイX時間で、どのぐらいの熱さまで許容できるか +バッテリー 連続プレイX時間で、どれぐらいのバッテリー消費が許容できるか +//} + +@
{table_object_tuning}の中でも、とくに@{フレームレート}と@{メモリ}は重要な指標なので必ず決めましょう。 +この時点では低スペック端末のことは置いておきましょう。 +まずはボリュームゾーンにある端末に対して指標を決めることが大事です。 + +//info{ +ボリュームゾーンの定義はプロジェクト次第です。 +ベンチマークになる他タイトルや市場調査を行って決めるのもよいでしょう。 +もしくはモバイルデバイスのリプレイスが長期化した背景から、4年ぐらい前のミドルレンジをひとまず指標にしてもよいでしょう。 +根拠は少し曖昧でも目指すべき旗を立てましょう。そこから調整していけばよいのです。 +//} + +ここで実例を考えてみましょう。 +いま次のような目標を掲げているプロジェクトがあるとします。 + + * 競合アプリケーションのよくない点はすべて改善したい + * とくにインゲームは滑らかに動かしたい + * 上記以外は競合と同等ぐらいでよい + +この曖昧な目標をチームで言語化すると次のような指標が生まれました。 + + * フレームレート + ** インゲームは60フレーム、アウトゲームはバッテリー消費の観点から30フレームとする。 + * メモリ + ** 遷移時間の高速化のために、インゲーム中にアウトゲームの一部リソースも保持しておく設計とする。最大使用量を1GBとする。 + * 遷移時間 + ** インゲームやアウトゲームへの遷移時間は競合と同じレベルを目指す。時間にすると3秒以内。 + * 熱 + ** 競合と同じレベル。検証端末で連続1時間は熱くならない。(充電していない状態) + * バッテリー + ** 競合と同じレベル。検証端末で連続1時間でバッテリー消費は20%ほど。 + +このように目指すべき指標が決まれば基準となる端末で触ってみましょう。 +まったく目標に届かないという状態でなければ指標としてはよいでしょう。 + +====[column] ゲームジャンルによる最適化 + +今回は滑らかに動くことがテーマだったのでフレームレートを60フレームとしました。 +他にもリズムアクションゲームや、FPS(ファーストパーソンシューティング)のような判定がシビアなゲームも高フレームレートが望ましいでしょう。 +しかし高フレームレートにはデメリットもあります。それはフレームレートが高いほどバッテリーを消費することです。 +他にもメモリは使用量が多いほどサスペンド時にOSからキルされやすくなります。 +このようなメリット・デメリットを考慮しゲームジャンルごとに適切な目標を決めましょう。 + +====[/column] + +==={start_ready_memory} メモリの最大使用量を把握する +この節ではメモリの最大使用量について焦点をあてます。 +メモリの最大使用量を知るためには、まずはサポート対象となるデバイスがメモリをどれぐらい確保できるかを把握しましょう。 +基本的には動作を保証する端末のうち最低スペックのデバイスで検証するのがよいでしょう。 +ただしOSバージョンによってメモリ確保の仕組みが変更されている可能性があるため、出来ればメジャーバージョンが違う端末を複数用意するのがよいでしょう。 +また計測するツールによっても計測ロジックが違うため、使用するツールは必ず1つに限定しましょう。 + +参考までに筆者がiOSにて検証した内容を記載しておきます。 +検証プロジェクトではTexture2Dをランタイムで生成し、どれぐらいでクラッシュするかを計測しました。 +コードは次のとおりです。 +//listnum[texture_leak][検証コード][csharp]{ +private List _textureList = new List(); +... +public void CreateTexture(int size) { + Texture2D texture = new Texture2D(size, size, TextureFormat.RGBA32, false); + _textureList.Add(texture); +} +//} + +//embed[latex]{ +\clearpage +//} + +検証結果は@{memory_crash_ios}のようになりました。 +//image[memory_crash_ios][クラッシュのしきい値] + +検証環境はUnity 2019.4.3、Xcode 11.6を用いて、XcodeのDebug NavigatorのMemoryセクションの数値を参考にしています。 +この検証結果からiPhone6Sや7などの搭載メモリが2GBの端末では、メモリを1.3GB以内におさめておくのがよいと言えるでしょう。 +またiPhone6のような搭載メモリが1GBの端末をサポートする場合、メモリ使用量の制約がかなり厳しくなるのも見て取れます。 +他にも特徴的なのはiOS11の場合、メモリ管理の仕組みが違うためかメモリ使用量が突出していることです。 +検証する際にはこのようなOSによる差分がまれにあることを注意してください。 + +//info{ +@{memory_crash_ios}では検証環境が少し古いため、執筆時の最新環境を用いて一部計測し直しました。 +環境はUnity 2020.3.25、2021.2.0の2バージョンとXcode13.3.1を用いて、OSバージョンが14.6と15.4.1のiPhoneXRにビルドを行いました。 +結果として計測値に差異はとくにみられなかったので、まだ信頼性のあるデータかと思います。 +//} + +====[column] メモリ計測ツール + +計測するツールとして推奨したいのは、XcodeやAndroidStudioなどのネイティブ準拠のツールです。 +たとえばUnity Profilerの計測では、プラグインなどが独自で確保したネイティブメモリ領域は計測対象外になります。 +他にもIL2CPPビルドの場合、IL2CPPのメタデータ(100MBほど)も計測対象になっていません。 +一方でネイティブツールのXcodeの場合、アプリで確保されたメモリはすべて計測されます。 +そのためより正確に値が計測されるネイティブ準拠のツールを使うのがよいでしょう。 + +//image[xcode_debug_memory][Xcode Debug Navigator] + +====[/column] + +==={start_ready_low_spec} 動作保証端末を決める +パフォーマンスチューニングをどこまで行うかを決める指標として、最低限の動作を保証する端末を決めることも重要です。 +この動作保証端末の選定は経験がないと即座に決めることは難しいですが、勢いで決めずにまずは低スペック端末の候補を洗い出すことから始めましょう。 + +筆者がオススメする方法としては「SoCのスペック」を計測したデータを参考にする方法です。 +具体的にはベンチマーク計測アプリで計測されたデータをWeb上で探します。 +まずは基準とした端末のスペックを把握し、それよりいくらか計測値の低い端末を何パターンか選定しましょう。 + +端末が洗い出せれば、実際にアプリケーションをインストールして動作確認をします。 +動作が重くても落胆しないでください。これでやっと何を削ぎ落とすか議論できるスタートラインに立てました。 +次項では機能を削ぎ落とすにあたって考えておきたい大事な仕様について紹介します。 + +//info{ +ベンチマーク計測アプリはいくつかありますが、筆者はAntutuを基準としています。 +こちらは計測データのまとめサイトが存在していますし、 +有志の方も積極的に計測データを報告してくれているのが理由です。 +//} + +==={start_ready_quality_setting} 品質設定の仕様を決める +市場にさまざまなスペックの端末が溢れ返っているいま、1つの仕様で多くの端末をカバーするのは難しいでしょう。 +そのため近年ではゲーム内にいくつかの品質設定を設けることで、さまざまな端末に対して安定した動作を保証することが主流となっています。 + +たとえば次のような項目を品質設定の高、中、低で切り分けるとよいでしょう。 + + * 画面解像度 + * オブジェクトの表示数 + * 影の有無 + * ポストエフェクト機能 + * フレームレート + * CPU負荷の高いスクリプトのスキップ機能など + +ただし見た目のクオリティを下げる事になるので、ディレクターと相談しながら +どこまでのラインをプロジェクトとして許容できるかを一緒に探っていきましょう。 + +=={start_detect_degration} 未然に防ぐ +性能低下は不具合と同じく時間が経過することでさまざまな原因が絡み合い、調査の難易度が上がってしまいます。 +なるべく早期に気づけるような仕組みをアプリケーションに実装しておくのがよいでしょう。 +実装が簡単で効果の高い方法としては、画面上に現在のアプリケーションの状態を表示しておくことです。 +少なくとも次の要素は画面上に常に表示することを推奨します。 + + * 現在のフレームレート + * 現在のメモリ使用量 + +//image[performance_stats][パフォーマンスの可視化] + +フレームレートは体感で性能が低下していることがわかりますが、メモリはクラッシュすることでしか検知できません。 +@{performance_stats}のように画面に常に表示しておくだけでメモリリークが早期に発見できる確率が上がるでしょう。 + +この表示方法はさらに工夫することで効果が高くなります。 +たとえばフレームレートの達成したい目標が30フレームなら、25〜30フレームを緑色、20〜25フレームを黄色、それ以下を赤色にしてみましょう。 +そうすることで直感的にアプリケーションが基準を満たしているのか一目でわかるようになります。 + +=={start_tuning} パフォーマンスチューニングに取り組む +いくら未然に性能低下を防ぐ努力をしても、すべてを防ぐことは厳しいでしょう。 +これは仕方ありません。開発を進める上で性能低下は切っても切り離せないものです。パフォーマンスチューニングと向き合う時は必ず来るでしょう。 +以降ではどのようにしてパフォーマンスチューニングに取り組んでいくべきかを解説します。 + +==={start_tuning_attitude} 心構え +パフォーマンスチューニングを始める前にまずは大事な心構えを紹介します。 +たとえばフレームレートが低下しているアプリケーションがあるとしましょう。明らかにリッチなモデルが複数体表示されています。 +周りの人はこのモデルがきっと原因に違いないと言っています。 +果たして本当にそうでしょうか、その証拠はどこにあるのかしっかり見定める必要があるでしょう。 +パフォーマンスチューニングの心構えとして、必ず意識して欲しいことが2つあります。 + +1つ目は、@{計測し、原因を特定}することです。@{推測ではいけません}。 + +2つ目は、修正したら必ず@{結果を比較}しましょう。前後のプロファイルを比較するとよいでしょう。 +ポイントとしては修正箇所だけでなく、全体で性能低下が発生していないかを確認することです。 +パフォーマンスチューニングの恐ろしい所は、修正箇所は高速化したが他の箇所で負荷が上がり、全体でみると性能低下しているという事がまれにあります。 +これでは本末転倒です。 + +//image[tuning_basic_flow][パフォーマンスチューニングの心構え] +確実に原因を突き止め確実に速くなったことを確認する。それがパフォーマンスチューニングの大事な心構えなのです。 + +==={start_tuning_any_degration} 性能低下の種類 +性能低下といってもそれぞれが指し示すものは違うでしょう。 +本書では大別して以下の3つと定義します。(@{degration}) +//image[degration][性能低下の原因] + +まずクラッシュする場合は@{「メモリ超過」}か@{「プログラムの実行エラー」}の2種類に大別されるでしょう。 +後者についてはパフォーマンスチューニングの領域ではないため、具体的な内容は本書では取り扱いません。 + +次に画面の処理落ちやロードが長いのは@{「CPUやGPUの処理時間」}が原因の大半を占めるでしょう。 +以降では「メモリ」と「処理時間」に焦点を当てて性能低下を深掘ります。 + +=={start_memory} メモリ超過の原因切り分け +クラッシュの原因にはメモリ超過が考えられると取り上げました。 +ここからはメモリ超過の原因をさらに分解していきましょう。 + +==={start_memory_leak} メモリがリークしている +メモリ超過の原因の1つとしてメモリのリークが考えられます。 +これを確認するためにシーンの遷移に伴ってメモリ使用量が徐々に増えるかどうか確認しましょう。 +ここでいうシーンの遷移とはただの画面遷移ではなく、大きく画面が転換する事を指します。 +たとえばタイトル画面からアウトゲーム、アウトゲームからインゲームなどです。 +計測する際は次の手順で進めましょう。 + + 1. あるシーンでのメモリ使用量をメモする + 2. 違うシーンに遷移する + 3. 「1」〜「2」の流れを3〜5回ほど繰り返す + +計測した結果、メモリ使用量が純増していれば間違いなく何かがリークしています。 +これは目に見えない不具合と言ってもいいでしょう。まずはリークを無くしましょう。 + +また「2」の遷移を行う前に、いくつか画面遷移を挟んでおくのもよいでしょう。 +なぜなら特定の画面でロードしたリソースだけが、例外的にリークしていたという事もあり得るためです。 + +リークに確信が持てたらリークの原因を探りましょう。 +@{start_leak_survey}にて具体的な調査方法について解説します。 + +====[column]繰り返す理由 + +これは筆者の経験談ですが、リソース解放後(UnloadUnusedAssets後)に +タイミングの問題でいくつかリソースが解放されていないケースがありました。 +この解放されていないリソースは次のシーンに遷移すると解放されます。 +これに対して、遷移を繰り返した際にメモリ使用量が徐々に増加する場合は、いずれクラッシュを引き起こします。 +前者の問題と切り分けるために本書では、メモリ計測時に遷移を何度か繰り返す手法を推奨しています。 + +ちなみに前者のような問題があった場合、リソース解放時には何かしらのオブジェクトがまだ参照を握っており、その後に解放されているのでしょう。 +致命傷とはなりませんが、原因調査を行い解決しておくのが良いでしょう。 + +====[/column] + +==={start_memory_large_used} メモリ使用量が単純に多い +リークしていない状態でメモリ使用量が多い場合、削減できる箇所を探っていく必要があります。 +@{start_memory_reduction}にて、具体的な方法について解説します。 + +=={start_leak_survey} メモリリークを調査しよう +まずはメモリリークを再現させた上で、次に紹介するツールを用いて原因を探っていきましょう。 +ここでは簡単にツールの特徴を説明します。 +ツールの使い方の詳細は@{profile_tool}にて取り扱うので、参考にしながら調査してみてください。 + +===={start_leak_survey_unity_memory} Profiler (Memory) +Unityエディターにデフォルトで搭載されているプロファイラーツールです。 +そのため気軽に計測ができます。基本的には「Detailed」かつ「Gather object references」を設定した状態でメモリをスナップショットし、調査することになるでしょう。 +このツールでの計測データは、他のツールと違いスナップショットの比較はできません。 +使い方の詳細は@{profile_tool|memory_module_unity_prfoiler}を参照してください。 + +===={start_leak_survey_memory_profiler} Memory Profiler +こちらはPackage Managerからインストールする必要があります。 +ツリーマップではグラフィカルにメモリの内容が表示されます。 +Unity公式でサポートされており、現在も頻繁にアップデートされています。 +v0.5以降、参照関係を追跡する方法が大きく改善されているため最新版の利用を推奨します。 +使い方の詳細は@{profile_tool|tool_memory_profiler}を参照してください。 + +===={start_leak_survey_heap_explorer} Heap Explorer +こちらはPackage Managerからインストールする必要があります。 +個人が開発したツールですが、非常に使いやすく動作も軽量です。 +参照関係を追跡する際にリスト形式で見ることが可能で、v0.4以前のMemory Profilerの痒い所に手が届いたツールです。 +v0.5のMemory Profilerが利用できない際の代替ツールとして利用すると良いでしょう。 +使い方の詳細は@{profile_tool|tool_heap_explorer}を参照してください。 + +=={start_memory_reduction} メモリを削減しよう +メモリを削減するポイントは@{大きな箇所から削る}ことです。 +なぜなら1KBを1,000個削っても1MBの削減にしかなりません。しかし10MBのテクスチャを圧縮して2MBになると8MBも削減されます。 +費用対効果を考えて、まずは大きいものから削減していくことを意識しましょう。 + +本節ではメモリ削減に使用するツールはProfiler(Memory)として話を進めます。 +使用したことがない方は@{profile_tool|memory_module_unity_prfoiler}にて詳細を確認してください。 + +以降の節では、削減する際に見るべき項目を取り上げていきます。 + +==={start_memory_reduction_asset} Assets +Simple ViewでAssets関連が多い場合、不要なアセットやメモリリークの可能性が考えられます。 +ここでいうAssets関連とは@{profiler_memory_simple}の矩形で囲った部分です。 +//image[profiler_memory_simple][Assets関連の項目] +この場合、調査するべきことは次の3つです。 + +===={start_memory_reduction_asset_unused} 不要アセット調査 +不要なアセットとは、現在のシーンにまったく必要のないリソースを指します。 +たとえばタイトル画面でしか使用しないBGMがアウトゲーム内でもメモリに常駐しているなどです。 +まずは今のシーンに必要なものだけにしましょう。 + +===={start_memory_reduction_asset_duplicate} 重複アセット調査 +これはアセットバンドル対応を行なった際によく発生します。 +アセットバンドルの依存関係の切り分け方が良くないために、同じアセットが複数のアセットバンドルに含まれている状態です。 +しかし依存関係は切り分けすぎてもダウンロードファイル数の増加や、ファイル展開コストの増加に繋がります。 +この辺りは計測しながらバランス感覚を養う必要があるかもしれません。 +アセットバンドルに関する詳細は、@{basic|basic_unity_assetbundle}を参照してください。 + +===={start_memory_reduction_asset_regulation} レギュレーションをチェックする +それぞれの項目のレギュレーションが守られているかを見直しましょう。 +レギュレーションがない場合は、メモリの見積もりが適切にできていない可能性があるので確認しましょう。 + +たとえばテクスチャなら次のような内容をチェックをするとよいでしょう。 + + * サイズは適切か + * 圧縮設定は適切か + * MipMapの設定が適切か + * Read/Writeの設定が適切かなど + +それぞれのアセットごとに気をつけるべき点は@{tuning_practice_asset}を参照してください。 + +==={start_memory_reduction_gc} GC(Mono) +Simple ViewでGC(Mono)が多い場合は、一度に大きなGC.Allocが発生している可能性が高いです。 +もしくは毎フレームGC.Allocが発生してメモリが断片化しているかもしれません。 +これらが原因でマネージドヒープ領域が余計に拡張している可能性があります。 +この場合はGC.Allocを地道に削減していきましょう。 + +マネージドヒープに関しては@{basic|basic_memory}を参照してください。 +同様にGC.Allocに関する詳細は@{basic|basic_csharp_stack_heap}にて取り扱っています。 + +=====[column] バージョンによる表記違い + +2020.2以降では「GC」と表示されていますが、2020.1以下のバージョンまでは「Mono」と記載されています。 +どちらもマネージドヒープの占有量を指しています。 + +=====[/column] + +==={start_memory_reduction_other} Other +Detailed Viewで怪しい項目がないかを確認しましょう。 +たとえば@{Other}の項目などは一度開いて調査するとよいでしょう。 +//image[profiler_memory_other][Otherの項目] +筆者の経験ではSerializedFileやPersistentManager.Remapperが、かなり肥大化していた事例がありました。 +複数プロジェクトで数値比較ができる場合、一度比較してみるのも良いでしょう。 +それぞれの数値を見比べると異常値がわかるかもしれません。 +詳細は@{profile_tool|memory_module_detailed_view}を参照してください。 + +==={start_memory_reduction_plugin} プラグイン +ここまではUnityの計測ツールを使って原因の切り分けをしてきました。 +しかしUnityで計測できるのはUnityが管理しているメモリのみです。 +つまりプラグインで独自に確保しているメモリ量などは計測されません。 +ThirdParty製品で余分にメモリを確保していないかを調べましょう。 + +具体的にはネイティブ計測ツール(XcodeのInstruments)を利用します。 +詳細は@{profile_tool|tool_instruments}を参照してください。 + +==={start_memory_reduction_planning} 仕様を検討する +これは最後の手段です。 +ここまでに取り上げてきた内容で削れる部分がない場合、仕様を検討するしかありません。 +以下に例を挙げます。 + + * テクスチャの圧縮率を変更する + ** テクスチャの一部分だけ圧縮率を一段回上げる + * ロード・アンロードするタイミングを変更する + ** 常駐メモリに載っているオブジェクトを解放し都度ロードにする + * ロード仕様を変更する + ** インゲームでロードするキャラクターのバリエーションを1種類減らす + +どれも影響範囲は大きく、ゲームの面白さに根本的に関わる可能性もあります。 +そのため仕様の検討は最後の手段なのです。 +そうならないように早い段階でメモリの見積もり、計測はしっかり行いましょう。 + +=={start_process_reduction} 処理落ちの原因切り分け +ここからは処理時間を計測し最適化していく流れについて紹介します。 +画面の処理落ちは「瞬間的な処理落ち」か「定常的な処理落ち」かで対処法が変わります。 + +//info{ +瞬間的な処理落ちは針のように尖った処理負荷が計測されます。 +その見た目からスパイクとも言われています。 +//} + +//image[process_heavy][スパイクと定常処理負荷] + +@{process_heavy}では、定常負荷が急に増加しており、さらに定期的なスパイクも発生している計測データです。 +どちらの事象もパフォーマンスチューニングが必要でしょう。 +まずは比較的シンプルな瞬間的な負荷調査を解説します。 +その後、定常的な負荷調査について解説します。 + +=={start_spike} 瞬間的な負荷を調査する +スパイクの調査方法としてはProfiler(CPU)を用いて原因を調査します。 + +ツールの詳細な使い方は@{profile_tool|cpu_module_unity_profiler}を参照してください。 +まずは原因がGCによるものかどうかを切り分けましょう。 +原因の切り分け自体にはDeep Profileは必要ありませんが、解決するためには必要になるでしょう。 + +==={start_spike_gc} GCによるスパイク +GC(ガベージコレクション)が発生している場合、GC.Allocを削減する必要があります。 +どの処理がどれぐらいアロケートしているかはDeep Profileを利用するとよいでしょう。 +まず削減すべきところは費用対効果の高い箇所です。 +次にあげるような項目を中心に修正するとよいでしょう。 + + * 毎フレームアロケートしている箇所 + * 大量のアロケートが発生している箇所 + +アロケーションは少ないほど良いですが、必ずゼロにすべきということではありません。 +たとえば生成処理(Instantiate)などで発生するアロケートは防ぎようがないでしょう。 +このような場合は都度オブエジェクトを生成せずに、オブジェクトを使い回すプーリングといった手法が有効です。 +GCの詳細は@{basic|basic_csharp_gc}を参照してください。 + +==={start_spike_heavy_process} 重い処理によるスパイク +GCが原因でない場合は何かしらの重い処理が瞬間的に行われています。 +ここでもDeep Profileを利用して何の処理がどれぐらい重いのかを調査し、もっとも処理に時間がかかっている箇所を見直していきましょう。 + +よくある一時的な重い処理といえば次のようなものが考えられるでしょう。 + + * Instantiate処理 + * 大量のオブジェクト、あるいは階層が深いオブジェクトのアクティブ切り替え + * 画面のキャプチャ処理など + +このようにプロジェクトコードにかなり依存する部分なので、解決方法は一律にこうすればよいというものはありません。 +実際に計測して原因が判明したらプロジェクトメンバーに計測結果を共有し、どのように改善すべきか検討してみてください。 + +=={start_heavy_process} 定常的な負荷を調査する +定常的な処理負荷を改善する際は、1フレーム内での処理をいかに削減するかが重要になります。 +1フレーム内で行っている処理は大別するとCPU処理とGPU処理に分けることができます。 +まずはこの2つの処理のうち、どちらがボトルネックになっているのか、もしくは同じくらいの処理負荷なのかを切り分けるとよいでしょう。 + +//info{ +CPUにボトルネックがある状態をCPUバウンド、GPUにボトルネックがある状態をGPUバウンドと呼びます +//} + +切り分ける簡単な方法として、次に当てはまる内容があればGPUバウンドの可能性が高いでしょう。 + + * 画面の解像度を下げた際に処理負荷が劇的に改善する + * Profilerで計測した際に@{Gfx.WaitForPresent}が存在する + +逆にこれらがない場合はCPUバウンドの可能性があります。 +以降ではCPUバウンドとGPUバウンドの調査方法について説明します。 + +==={start_heavy_process_cpu} CPUバウンド +CPUバウンドは前節でも取り扱ったCPU (Profiler)を利用します。 +Deep Profileを利用して調査し、特定のアルゴリズムに大きな処理負荷がかかっていないかを確認します。 +大きな処理負荷がない場合は均等に重いということになるので地道に改善していきましょう。 +地道な改善を重ねても到底目標の削減値に及ばない場合は +@{start_ready_quality_setting}に立ち戻って再考し直すのもよいでしょう。 + +==={start_heavy_process_gpu} GPUバウンド +GPUバウンドの場合はFrame Debuggerを利用して調査するのがよいでしょう。 +使い方の詳細は@{profile_tool|tool_frame_debugger}を参照してください。 + +===={start_heavy_process_gpu_resolution} 解像度が適切か +GPUバウンドの中でも解像度はGPUの処理負荷に大きく影響します。 +そのため解像度を適切に設定できていない状態であれば、まずは最優先に適切な解像度にする必要があります。 + +まずは想定の品質設定で適切な解像度になっているかを確認しましょう。 +確認方法としてはFrame Debugger内で処理しているレンダーターゲットの解像度に注目するとよいでしょう。 +次にあげるようなことを意図的に実装していない場合、最適化に取り組みましょう。 + + * UI要素だけデバイスのフル解像度で描画されている + * ポストエフェクト用の一時テクスチャの解像度が高いなど + +===={start_heavy_process_gpu_unused} 不要なオブジェクトがあるか +Frame Debuggerで不要な描画がないかを確認しましょう。 +たとえば必要のないカメラがアクティブ状態になっており、裏で関係のない描画が行われているかもしれません。 +また他の遮蔽物によって直前の描画がムダになっているケースが多いのであれば、@{オクルージョンカリング}を検討するのもよいでしょう。 +オクルージョンカリングの詳細は@{tuning_practice_graphics|practice_graphics_occlusion_culling}を参照してください。 + +//info{ +オクルージョンカリングはデータを事前に準備する必要があり、そのデータをメモリに展開するためメモリ使用量が増えることにも注意しましょう。 +このようにパフォーマンスを向上させるために、メモリに事前に準備した情報を構築するのはよくある手法です。 +メモリとパフォーマンスは反比例することが多いので、何かを採用する際はメモリも意識すると良いでしょう。 +//} + +===={start_heavy_process_gpu_batching} バッチングが適切か +描画対象をまとめて描画する事をバッチングと言います。 +まとめて描画されることで描画効率が向上するためGPUバウンドに効果があります。 +たとえばStatic Batchingを利用すると複数の動かないオブジェクトのメッシュをまとめてくれます。 + +バッチングに関しては色々な方法があるため、代表的なものをいくつかあげておきます。 +気になったものがあれば@{tuning_practice_graphics|practice_graphics_draw_call}を参照してみください。 + + * Static Batching + * Dynamic Batching + * GPU Instancing + * SRP Batcherなど + +===={start_heavy_process_gpu_any} 個別に負荷をみる +まだ処理負荷が高い場合は個別にみていくしかありません。 +オブジェクトの頂点数が多すぎたり、Shaderの処理に原因があるかもしれません。 +これを切り分けるには個々のオブジェクトに対してアクティブを切り替え、処理負荷の変わり具合を見ていきます。 +具体的には背景を消してみてどうなるか、キャラクターを消してみてどうなるか、といったようにカテゴリごとに絞っていきます。 +処理負荷の高いカテゴリがわかれば、さらに次のような要素をみていくとよいでしょう。 + + * 描画するオブジェクトが多すぎないか + ** まとめて描画できないか検討する + * 1オブジェクトの頂点数が多すぎないか + ** リダクション、LODを考える + * シンプルなShaderに付け替えて処理負荷が改善するか + ** Shaderの処理を見直す + +===={start_heavy_process_gpu_others} それ以外 +それぞれのGPU処理が積み上がって重いと言えるでしょう。 +この場合は地道に1つ1つ改善していくしかありません。 + +またこちらもCPUバウンドと同様に、到底目標の削減値に及ばない場合 +@{start_ready_quality_setting}に立ち戻って再考するのもよいでしょう。 + +=={start_summary} まとめ +この章では「パフォーマンスチューニング前」と「パフォーマンスチューニング中」に気をつけて欲しい事を取り上げました。 + +パフォーマンスチューニング前に気をつけることは以下のとおりです。 + + * 「指標」「動作保証端末」「品質設定の仕様」を決めること + ** 量産前までに検証を行い指標を確定させること + * 性能低下に気づきやすい仕組みを作っておくこと + +パフォーマンスチューニング中に気をつけることは以下のとおりです。 + + * 性能低下の原因を切り分け、適切な対処を行うこと + * 「計測」、「改善」、「再度計測 (結果のチェック)」の一連の流れを必ず行うこと + +パフォーマンスチューニングはここまで解説してきたように、計測して原因を切り分けていくことが重要です。 +このドキュメントに記載されていない事例が発生しても、その基礎が守られていれば大きな問題になることはないでしょう。 +一度もパフォーマンスチューニングをしたことがない方は、ぜひこの章の内容を実践してもらえると幸いです。 diff --git a/articles/title-epub.tex b/articles/title-epub.tex new file mode 100644 index 0000000..b50b7f4 --- /dev/null +++ b/articles/title-epub.tex @@ -0,0 +1,4 @@ +% タイトル画像 +\begin{titlepage} + \includefullpagegraphics[width=\paperwidth,height=\paperheight,keepaspectratio]{/book/articles/images/cover/tuning_bible_cover_monochrome.png} +\end{titlepage}\clearpage diff --git a/articles/title.tex b/articles/title.tex new file mode 100644 index 0000000..f21db83 --- /dev/null +++ b/articles/title.tex @@ -0,0 +1,9 @@ +% 「はじめに」を左右見開きにするための遊び紙 +%\thispagestyle{empty}% +%\begin{center}% +%\end{center}\clearpage + +% タイトル画像 +\begin{titlepage} + \includefullpagegraphics[width=\paperwidth,height=\paperheight,keepaspectratio]{/book/articles/images/cover/tuning_bible_cover_monochrome.png} +\end{titlepage}\clearpage diff --git a/build-in-docker-epub.sh b/build-in-docker-epub.sh new file mode 100755 index 0000000..590dfe3 --- /dev/null +++ b/build-in-docker-epub.sh @@ -0,0 +1,7 @@ +#!/bin/bash +[ ! -z $REVIEW_CONFIG_FILE ] || REVIEW_CONFIG_FILE=config-epub.yml + +# コマンド手打ちで作業したい時は以下の通り /book に pwd がマウントされます +# docker run -i -t -v $(pwd):/book vvakame/review:5.1 /bin/bash + +docker run -t --rm -v $(pwd):/book -v $(pwd)/articles/fonts/bizud:/usr/share/fonts/truetype/bizud vvakame/review:5.1 /bin/bash -ci "cd /book && ./setup.sh && REVIEW_CONFIG_FILE=$REVIEW_CONFIG_FILE npm run pdf" \ No newline at end of file diff --git a/build-in-docker.sh b/build-in-docker.sh new file mode 100755 index 0000000..f8efb41 --- /dev/null +++ b/build-in-docker.sh @@ -0,0 +1,7 @@ +#!/bin/bash +[ ! -z $REVIEW_CONFIG_FILE ] || REVIEW_CONFIG_FILE=config.yml + +# コマンド手打ちで作業したい時は以下の通り /book に pwd がマウントされます +# docker run -i -t -v $(pwd):/book vvakame/review:5.1 /bin/bash + +docker run -t --rm -v $(pwd):/book -v $(pwd)/articles/fonts/bizud:/usr/share/fonts/truetype/bizud vvakame/review:5.1 /bin/bash -ci "cd /book && ./setup.sh && REVIEW_CONFIG_FILE=$REVIEW_CONFIG_FILE npm run pdf" \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..046642d --- /dev/null +++ b/package-lock.json @@ -0,0 +1,2357 @@ +{ + "name": "unity-performance-tuning-bible", + "version": "1.0.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "dev": true + }, + "ansi-regex": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.0.0.tgz", + "integrity": "sha512-iB5Dda8t/UqpPI/IjsejXu5jOGDrzn41wJyljwPH65VCIbk6+1BzFIMJGFwTNrYXT1CrD+B4l19U7awiQ8rk7w==", + "dev": true + }, + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "requires": { + "sprintf-js": "~1.0.2" + }, + "dependencies": { + "sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", + "dev": true + } + } + }, + "arr-diff": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", + "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=", + "dev": true + }, + "arr-flatten": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", + "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==", + "dev": true + }, + "arr-union": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", + "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=", + "dev": true + }, + "array-differ": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/array-differ/-/array-differ-1.0.0.tgz", + "integrity": "sha1-7/UuN1gknTO+QCuLuOVkuytdQDE=", + "dev": true + }, + "array-each": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/array-each/-/array-each-1.0.1.tgz", + "integrity": "sha1-p5SvDAWrF1KEbudTofIRoFugxE8=", + "dev": true + }, + "array-find-index": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-find-index/-/array-find-index-1.0.2.tgz", + "integrity": "sha1-3wEKoSh+Fku9pvlyOwqWoexBh6E=", + "dev": true + }, + "array-slice": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/array-slice/-/array-slice-1.1.0.tgz", + "integrity": "sha512-B1qMD3RBP7O8o0H2KbrXDyB0IccejMF15+87Lvlor12ONPRHP6gTjXMNkt/d3ZuOGbAe66hFmaCfECI24Ufp6w==", + "dev": true + }, + "array-union": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz", + "integrity": "sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk=", + "dev": true, + "requires": { + "array-uniq": "^1.0.1" + } + }, + "array-uniq": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz", + "integrity": "sha1-r2rId6Jcx/dOBYiUdThY39sk/bY=", + "dev": true + }, + "array-unique": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", + "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", + "dev": true + }, + "arrify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", + "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=", + "dev": true + }, + "assign-symbols": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", + "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=", + "dev": true + }, + "async": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", + "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=", + "dev": true + }, + "atob": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", + "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==", + "dev": true + }, + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "dev": true + }, + "base": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz", + "integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==", + "dev": true, + "requires": { + "cache-base": "^1.0.1", + "class-utils": "^0.3.5", + "component-emitter": "^1.2.1", + "define-property": "^1.0.0", + "isobject": "^3.0.1", + "mixin-deep": "^1.2.0", + "pascalcase": "^0.1.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "dev": true, + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + } + } + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "braces": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "dev": true, + "requires": { + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "cache-base": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", + "integrity": "sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==", + "dev": true, + "requires": { + "collection-visit": "^1.0.0", + "component-emitter": "^1.2.1", + "get-value": "^2.0.6", + "has-value": "^1.0.0", + "isobject": "^3.0.1", + "set-value": "^2.0.0", + "to-object-path": "^0.3.0", + "union-value": "^1.0.0", + "unset-value": "^1.0.0" + } + }, + "camelcase": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-2.1.1.tgz", + "integrity": "sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8=", + "dev": true + }, + "camelcase-keys": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-2.1.0.tgz", + "integrity": "sha1-MIvur/3ygRkFHvodkyITyRuPkuc=", + "dev": true, + "requires": { + "camelcase": "^2.0.0", + "map-obj": "^1.0.0" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "class-utils": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", + "integrity": "sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==", + "dev": true, + "requires": { + "arr-union": "^3.1.0", + "define-property": "^0.2.5", + "isobject": "^3.0.0", + "static-extend": "^0.1.1" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + } + } + }, + "coffeescript": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/coffeescript/-/coffeescript-1.10.0.tgz", + "integrity": "sha1-56qDAZF+9iGzXYo580jc3R234z4=", + "dev": true + }, + "collection-visit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", + "integrity": "sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA=", + "dev": true, + "requires": { + "map-visit": "^1.0.0", + "object-visit": "^1.0.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true + }, + "colors": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.1.2.tgz", + "integrity": "sha1-FopHAXVran9RoSzgyXv6KMCE7WM=", + "dev": true + }, + "component-emitter": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz", + "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=", + "dev": true + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, + "copy-descriptor": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", + "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=", + "dev": true + }, + "currently-unhandled": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/currently-unhandled/-/currently-unhandled-0.4.1.tgz", + "integrity": "sha1-mI3zP+qxke95mmE2nddsF635V+o=", + "dev": true, + "requires": { + "array-find-index": "^1.0.1" + } + }, + "dateformat": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-1.0.12.tgz", + "integrity": "sha1-nxJLZ1lMk3/3BpMuSmQsyo27/uk=", + "dev": true, + "requires": { + "get-stdin": "^4.0.1", + "meow": "^3.3.0" + } + }, + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", + "dev": true + }, + "decode-uri-component": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", + "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=", + "dev": true + }, + "define-property": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", + "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", + "dev": true, + "requires": { + "is-descriptor": "^1.0.2", + "isobject": "^3.0.1" + }, + "dependencies": { + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + } + } + }, + "detect-file": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/detect-file/-/detect-file-1.0.0.tgz", + "integrity": "sha1-8NZtA2cqglyxtzvbP+YjEMjlUrc=", + "dev": true + }, + "error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "requires": { + "is-arrayish": "^0.2.1" + } + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true + }, + "esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true + }, + "eventemitter2": { + "version": "0.4.14", + "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-0.4.14.tgz", + "integrity": "sha1-j2G3XN4BKy6esoTUVFWDtWQ7Yas=", + "dev": true + }, + "exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha1-BjJjj42HfMghB9MKD/8aF8uhzQw=", + "dev": true + }, + "expand-brackets": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", + "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", + "dev": true, + "requires": { + "debug": "^2.3.3", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "posix-character-classes": "^0.1.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "expand-tilde": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/expand-tilde/-/expand-tilde-2.0.2.tgz", + "integrity": "sha1-l+gBqgUt8CRU3kawK/YhZCzchQI=", + "dev": true, + "requires": { + "homedir-polyfill": "^1.0.1" + } + }, + "extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "dev": true + }, + "extend-shallow": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", + "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", + "dev": true, + "requires": { + "assign-symbols": "^1.0.0", + "is-extendable": "^1.0.1" + }, + "dependencies": { + "is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "dev": true, + "requires": { + "is-plain-object": "^2.0.4" + } + } + } + }, + "extglob": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", + "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", + "dev": true, + "requires": { + "array-unique": "^0.3.2", + "define-property": "^1.0.0", + "expand-brackets": "^2.1.4", + "extend-shallow": "^2.0.1", + "fragment-cache": "^0.2.1", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "dev": true, + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + } + } + }, + "fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", + "dev": true, + "requires": { + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "find-up": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz", + "integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=", + "dev": true, + "requires": { + "path-exists": "^2.0.0", + "pinkie-promise": "^2.0.0" + } + }, + "findup-sync": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-0.3.0.tgz", + "integrity": "sha1-N5MKpdgWt3fANEXhlmzGeQpMCxY=", + "dev": true, + "requires": { + "glob": "~5.0.0" + }, + "dependencies": { + "glob": { + "version": "5.0.15", + "resolved": "https://registry.npmjs.org/glob/-/glob-5.0.15.tgz", + "integrity": "sha1-G8k2ueAvSmA/zCIuz3Yz0wuLk7E=", + "dev": true, + "requires": { + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "2 || 3", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + } + } + }, + "fined": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/fined/-/fined-1.1.1.tgz", + "integrity": "sha512-jQp949ZmEbiYHk3gkbdtpJ0G1+kgtLQBNdP5edFP7Fh+WAYceLQz6yO1SBj72Xkg8GVyTB3bBzAYrHJVh5Xd5g==", + "dev": true, + "requires": { + "expand-tilde": "^2.0.2", + "is-plain-object": "^2.0.3", + "object.defaults": "^1.1.0", + "object.pick": "^1.2.0", + "parse-filepath": "^1.0.1" + } + }, + "flagged-respawn": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/flagged-respawn/-/flagged-respawn-1.0.1.tgz", + "integrity": "sha512-lNaHNVymajmk0OJMBn8fVUAU1BtDeKIqKoVhk4xAALB57aALg6b4W0MfJ/cUE0g9YBXy5XhSlPIpYIJ7HaY/3Q==", + "dev": true + }, + "for-in": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", + "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=", + "dev": true + }, + "for-own": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/for-own/-/for-own-1.0.0.tgz", + "integrity": "sha1-xjMy9BXO3EsE2/5wz4NklMU8tEs=", + "dev": true, + "requires": { + "for-in": "^1.0.1" + } + }, + "fragment-cache": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", + "integrity": "sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk=", + "dev": true, + "requires": { + "map-cache": "^0.2.2" + } + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true + }, + "get-stdin": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-4.0.1.tgz", + "integrity": "sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4=", + "dev": true + }, + "get-value": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", + "integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=", + "dev": true + }, + "getobject": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/getobject/-/getobject-0.1.0.tgz", + "integrity": "sha1-BHpEl4n6Fg0Bj1SG7ZEyC27HiFw=", + "dev": true + }, + "glob": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.0.6.tgz", + "integrity": "sha1-IRuvr0nlJbjNkyYNFKsTYVKz9Xo=", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.2", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "global-modules": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-1.0.0.tgz", + "integrity": "sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg==", + "dev": true, + "requires": { + "global-prefix": "^1.0.1", + "is-windows": "^1.0.1", + "resolve-dir": "^1.0.0" + } + }, + "global-prefix": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-1.0.2.tgz", + "integrity": "sha1-2/dDxsFJklk8ZVVoy2btMsASLr4=", + "dev": true, + "requires": { + "expand-tilde": "^2.0.2", + "homedir-polyfill": "^1.0.1", + "ini": "^1.3.4", + "is-windows": "^1.0.1", + "which": "^1.2.14" + } + }, + "graceful-fs": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz", + "integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==", + "dev": true + }, + "grunt": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/grunt/-/grunt-1.1.0.tgz", + "integrity": "sha512-+NGod0grmviZ7Nzdi9am7vuRS/h76PcWDsV635mEXF0PEQMUV6Kb+OjTdsVxbi0PZmfQOjCMKb3w8CVZcqsn1g==", + "dev": true, + "requires": { + "coffeescript": "~1.10.0", + "dateformat": "~1.0.12", + "eventemitter2": "~0.4.13", + "exit": "~0.1.1", + "findup-sync": "~0.3.0", + "glob": "~7.0.0", + "grunt-cli": "~1.2.0", + "grunt-known-options": "~1.1.0", + "grunt-legacy-log": "~2.0.0", + "grunt-legacy-util": "~1.1.1", + "iconv-lite": "~0.4.13", + "js-yaml": "~3.13.1", + "minimatch": "~3.0.2", + "mkdirp": "~1.0.3", + "nopt": "~3.0.6", + "path-is-absolute": "~1.0.0", + "rimraf": "~2.6.2" + }, + "dependencies": { + "grunt-cli": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/grunt-cli/-/grunt-cli-1.2.0.tgz", + "integrity": "sha1-VisRnrsGndtGSs4oRVAb6Xs1tqg=", + "dev": true, + "requires": { + "findup-sync": "~0.3.0", + "grunt-known-options": "~1.1.0", + "nopt": "~3.0.6", + "resolve": "~1.1.0" + } + }, + "resolve": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.1.7.tgz", + "integrity": "sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs=", + "dev": true + } + } + }, + "grunt-cli": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/grunt-cli/-/grunt-cli-1.3.2.tgz", + "integrity": "sha512-8OHDiZZkcptxVXtMfDxJvmN7MVJNE8L/yIcPb4HB7TlyFD1kDvjHrb62uhySsU14wJx9ORMnTuhRMQ40lH/orQ==", + "dev": true, + "requires": { + "grunt-known-options": "~1.1.0", + "interpret": "~1.1.0", + "liftoff": "~2.5.0", + "nopt": "~4.0.1", + "v8flags": "~3.1.1" + }, + "dependencies": { + "nopt": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.1.tgz", + "integrity": "sha1-0NRoWv1UFRk8jHUFYC0NF81kR00=", + "dev": true, + "requires": { + "abbrev": "1", + "osenv": "^0.1.4" + } + } + } + }, + "grunt-contrib-clean": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/grunt-contrib-clean/-/grunt-contrib-clean-2.0.0.tgz", + "integrity": "sha512-g5ZD3ORk6gMa5ugZosLDQl3dZO7cI3R14U75hTM+dVLVxdMNJCPVmwf9OUt4v4eWgpKKWWoVK9DZc1amJp4nQw==", + "dev": true, + "requires": { + "async": "^2.6.1", + "rimraf": "^2.6.2" + }, + "dependencies": { + "async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.2.tgz", + "integrity": "sha512-H1qVYh1MYhEEFLsP97cVKqCGo7KfCyTt6uEWqsTBr9SO84oK9Uwbyd/yCW+6rKJLHksBNUVWZDAjfS+Ccx0Bbg==", + "dev": true, + "requires": { + "lodash": "^4.17.11" + } + } + } + }, + "grunt-known-options": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/grunt-known-options/-/grunt-known-options-1.1.1.tgz", + "integrity": "sha512-cHwsLqoighpu7TuYj5RonnEuxGVFnztcUqTqp5rXFGYL4OuPFofwC4Ycg7n9fYwvK6F5WbYgeVOwph9Crs2fsQ==", + "dev": true + }, + "grunt-legacy-log": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/grunt-legacy-log/-/grunt-legacy-log-2.0.0.tgz", + "integrity": "sha512-1m3+5QvDYfR1ltr8hjiaiNjddxGdQWcH0rw1iKKiQnF0+xtgTazirSTGu68RchPyh1OBng1bBUjLmX8q9NpoCw==", + "dev": true, + "requires": { + "colors": "~1.1.2", + "grunt-legacy-log-utils": "~2.0.0", + "hooker": "~0.2.3", + "lodash": "~4.17.5" + } + }, + "grunt-legacy-log-utils": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/grunt-legacy-log-utils/-/grunt-legacy-log-utils-2.0.1.tgz", + "integrity": "sha512-o7uHyO/J+i2tXG8r2bZNlVk20vlIFJ9IEYyHMCQGfWYru8Jv3wTqKZzvV30YW9rWEjq0eP3cflQ1qWojIe9VFA==", + "dev": true, + "requires": { + "chalk": "~2.4.1", + "lodash": "~4.17.10" + } + }, + "grunt-legacy-util": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/grunt-legacy-util/-/grunt-legacy-util-1.1.1.tgz", + "integrity": "sha512-9zyA29w/fBe6BIfjGENndwoe1Uy31BIXxTH3s8mga0Z5Bz2Sp4UCjkeyv2tI449ymkx3x26B+46FV4fXEddl5A==", + "dev": true, + "requires": { + "async": "~1.5.2", + "exit": "~0.1.1", + "getobject": "~0.1.0", + "hooker": "~0.2.3", + "lodash": "~4.17.10", + "underscore.string": "~3.3.4", + "which": "~1.3.0" + } + }, + "grunt-shell": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/grunt-shell/-/grunt-shell-3.0.1.tgz", + "integrity": "sha512-C8eR4frw/NmIFIwSvzSLS4wOQBUzC+z6QhrKPzwt/tlaIqlzH35i/O2MggVOBj2Sh1tbaAqpASWxGiGsi4JMIQ==", + "dev": true, + "requires": { + "chalk": "^2.4.1", + "npm-run-path": "^2.0.0", + "strip-ansi": "^5.0.0" + } + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "has-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", + "integrity": "sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc=", + "dev": true, + "requires": { + "get-value": "^2.0.6", + "has-values": "^1.0.0", + "isobject": "^3.0.0" + } + }, + "has-values": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz", + "integrity": "sha1-lbC2P+whRmGab+V/51Yo1aOe/k8=", + "dev": true, + "requires": { + "is-number": "^3.0.0", + "kind-of": "^4.0.0" + }, + "dependencies": { + "kind-of": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", + "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "homedir-polyfill": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.1.tgz", + "integrity": "sha1-TCu8inWJmP7r9e1oWA921GdotLw=", + "dev": true, + "requires": { + "parse-passwd": "^1.0.0" + } + }, + "hooker": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/hooker/-/hooker-0.2.3.tgz", + "integrity": "sha1-uDT3I8xKJCqmWWNFnfbZhMXT2Vk=", + "dev": true + }, + "hosted-git-info": { + "version": "2.8.8", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.8.tgz", + "integrity": "sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg==", + "dev": true + }, + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "indent-string": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-2.1.0.tgz", + "integrity": "sha1-ji1INIdCEhtKghi3oTfppSBJ3IA=", + "dev": true, + "requires": { + "repeating": "^2.0.0" + } + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "dev": true + }, + "ini": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.7.tgz", + "integrity": "sha512-iKpRpXP+CrP2jyrxvg1kMUpXDyRUFDWurxbnVT1vQPx+Wz9uCYsMIqYuSBLV+PAaZG/d7kRLKRFc9oDMsH+mFQ==", + "dev": true + }, + "interpret": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.1.0.tgz", + "integrity": "sha1-ftGxQQxqDg94z5XTuEQMY/eLhhQ=", + "dev": true + }, + "is-absolute": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-absolute/-/is-absolute-1.0.0.tgz", + "integrity": "sha512-dOWoqflvcydARa360Gvv18DZ/gRuHKi2NU/wU5X1ZFzdYfH29nkiNZsF3mp4OJ3H4yo9Mx8A/uAGNzpzPN3yBA==", + "dev": true, + "requires": { + "is-relative": "^1.0.0", + "is-windows": "^1.0.1" + } + }, + "is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", + "dev": true + }, + "is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", + "dev": true + }, + "is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^0.1.6", + "is-data-descriptor": "^0.1.4", + "kind-of": "^5.0.0" + }, + "dependencies": { + "kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "dev": true + } + } + }, + "is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=", + "dev": true + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", + "dev": true + }, + "is-finite": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-finite/-/is-finite-1.1.0.tgz", + "integrity": "sha512-cdyMtqX/BOqqNBBiKlIVkytNHm49MtMlYyn1zxzvJKWmFMlGzm+ry5BBfYyeY9YmNKbRSo/o7OX9w9ale0wg3w==", + "dev": true + }, + "is-glob": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", + "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", + "dev": true, + "requires": { + "is-extglob": "^2.1.0" + } + }, + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "requires": { + "isobject": "^3.0.1" + } + }, + "is-relative": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-relative/-/is-relative-1.0.0.tgz", + "integrity": "sha512-Kw/ReK0iqwKeu0MITLFuj0jbPAmEiOsIwyIXvvbfa6QfmN9pkD1M+8pdk7Rl/dTKbH34/XBFMbgD4iMJhLQbGA==", + "dev": true, + "requires": { + "is-unc-path": "^1.0.0" + } + }, + "is-unc-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-unc-path/-/is-unc-path-1.0.0.tgz", + "integrity": "sha512-mrGpVd0fs7WWLfVsStvgF6iEJnbjDFZh9/emhRDcGWTduTfNHd9CHeUwH3gYIjdbwo4On6hunkztwOaAw0yllQ==", + "dev": true, + "requires": { + "unc-path-regex": "^0.1.2" + } + }, + "is-utf8": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz", + "integrity": "sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI=", + "dev": true + }, + "is-windows": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", + "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", + "dev": true + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", + "dev": true + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + }, + "js-yaml": { + "version": "3.13.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", + "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", + "dev": true, + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + } + }, + "kind-of": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", + "dev": true + }, + "liftoff": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/liftoff/-/liftoff-2.5.0.tgz", + "integrity": "sha1-IAkpG7Mc6oYbvxCnwVooyvdcMew=", + "dev": true, + "requires": { + "extend": "^3.0.0", + "findup-sync": "^2.0.0", + "fined": "^1.0.1", + "flagged-respawn": "^1.0.0", + "is-plain-object": "^2.0.4", + "object.map": "^1.0.0", + "rechoir": "^0.6.2", + "resolve": "^1.1.7" + }, + "dependencies": { + "findup-sync": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-2.0.0.tgz", + "integrity": "sha1-kyaxSIwi0aYIhlCoaQGy2akKLLw=", + "dev": true, + "requires": { + "detect-file": "^1.0.0", + "is-glob": "^3.1.0", + "micromatch": "^3.0.4", + "resolve-dir": "^1.0.1" + } + } + } + }, + "load-grunt-tasks": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/load-grunt-tasks/-/load-grunt-tasks-4.0.0.tgz", + "integrity": "sha512-w5JYPHpZgMxu9XFR9N9MEzyX8E0mLhQkwQ1qVP4mb3gmuomw8Ww8J49NHMbXqyQliq2LUCqdU7/wW96IVuPCKw==", + "dev": true, + "requires": { + "arrify": "^1.0.0", + "multimatch": "^2.0.0", + "pkg-up": "^2.0.0", + "resolve-pkg": "^1.0.0" + } + }, + "load-json-file": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", + "integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "parse-json": "^2.2.0", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0", + "strip-bom": "^2.0.0" + } + }, + "locate-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", + "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", + "dev": true, + "requires": { + "p-locate": "^2.0.0", + "path-exists": "^3.0.0" + }, + "dependencies": { + "path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", + "dev": true + } + } + }, + "lodash": { + "version": "4.17.19", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.19.tgz", + "integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ==", + "dev": true + }, + "loud-rejection": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/loud-rejection/-/loud-rejection-1.6.0.tgz", + "integrity": "sha1-W0b4AUft7leIcPCG0Eghz5mOVR8=", + "dev": true, + "requires": { + "currently-unhandled": "^0.4.1", + "signal-exit": "^3.0.0" + } + }, + "make-iterator": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/make-iterator/-/make-iterator-1.0.1.tgz", + "integrity": "sha512-pxiuXh0iVEq7VM7KMIhs5gxsfxCux2URptUQaXo4iZZJxBAzTPOLE2BumO5dbfVYq/hBJFBR/a1mFDmOx5AGmw==", + "dev": true, + "requires": { + "kind-of": "^6.0.2" + } + }, + "map-cache": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", + "integrity": "sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=", + "dev": true + }, + "map-obj": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", + "integrity": "sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0=", + "dev": true + }, + "map-visit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz", + "integrity": "sha1-7Nyo8TFE5mDxtb1B8S80edmN+48=", + "dev": true, + "requires": { + "object-visit": "^1.0.0" + } + }, + "meow": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/meow/-/meow-3.7.0.tgz", + "integrity": "sha1-cstmi0JSKCkKu/qFaJJYcwioAfs=", + "dev": true, + "requires": { + "camelcase-keys": "^2.0.0", + "decamelize": "^1.1.2", + "loud-rejection": "^1.0.0", + "map-obj": "^1.0.1", + "minimist": "^1.1.3", + "normalize-package-data": "^2.3.4", + "object-assign": "^4.0.1", + "read-pkg-up": "^1.0.1", + "redent": "^1.0.0", + "trim-newlines": "^1.0.0" + } + }, + "micromatch": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", + "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "dev": true, + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "braces": "^2.3.1", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "extglob": "^2.0.4", + "fragment-cache": "^0.2.1", + "kind-of": "^6.0.2", + "nanomatch": "^1.2.9", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.2" + } + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", + "dev": true + }, + "mixin-deep": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz", + "integrity": "sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==", + "dev": true, + "requires": { + "for-in": "^1.0.2", + "is-extendable": "^1.0.1" + }, + "dependencies": { + "is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "dev": true, + "requires": { + "is-plain-object": "^2.0.4" + } + } + } + }, + "mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + }, + "multimatch": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/multimatch/-/multimatch-2.1.0.tgz", + "integrity": "sha1-nHkGoi+0wCkZ4vX3UWG0zb1LKis=", + "dev": true, + "requires": { + "array-differ": "^1.0.0", + "array-union": "^1.0.1", + "arrify": "^1.0.0", + "minimatch": "^3.0.0" + } + }, + "nanomatch": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz", + "integrity": "sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==", + "dev": true, + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "fragment-cache": "^0.2.1", + "is-windows": "^1.0.2", + "kind-of": "^6.0.2", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + } + }, + "nopt": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz", + "integrity": "sha1-xkZdvwirzU2zWTF/eaxopkayj/k=", + "dev": true, + "requires": { + "abbrev": "1" + } + }, + "normalize-package-data": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "dev": true, + "requires": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + } + }, + "npm-run-path": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", + "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", + "dev": true, + "requires": { + "path-key": "^2.0.0" + } + }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", + "dev": true + }, + "object-copy": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", + "integrity": "sha1-fn2Fi3gb18mRpBupde04EnVOmYw=", + "dev": true, + "requires": { + "copy-descriptor": "^0.1.0", + "define-property": "^0.2.5", + "kind-of": "^3.0.3" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "object-visit": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz", + "integrity": "sha1-95xEk68MU3e1n+OdOV5BBC3QRbs=", + "dev": true, + "requires": { + "isobject": "^3.0.0" + } + }, + "object.defaults": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/object.defaults/-/object.defaults-1.1.0.tgz", + "integrity": "sha1-On+GgzS0B96gbaFtiNXNKeQ1/s8=", + "dev": true, + "requires": { + "array-each": "^1.0.1", + "array-slice": "^1.0.0", + "for-own": "^1.0.0", + "isobject": "^3.0.0" + } + }, + "object.map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/object.map/-/object.map-1.0.1.tgz", + "integrity": "sha1-z4Plncj8wK1fQlDh94s7gb2AHTc=", + "dev": true, + "requires": { + "for-own": "^1.0.0", + "make-iterator": "^1.0.0" + } + }, + "object.pick": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", + "integrity": "sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c=", + "dev": true, + "requires": { + "isobject": "^3.0.1" + } + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, + "requires": { + "wrappy": "1" + } + }, + "os-homedir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", + "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", + "dev": true + }, + "os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", + "dev": true + }, + "osenv": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz", + "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", + "dev": true, + "requires": { + "os-homedir": "^1.0.0", + "os-tmpdir": "^1.0.0" + } + }, + "p-limit": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", + "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", + "dev": true, + "requires": { + "p-try": "^1.0.0" + } + }, + "p-locate": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", + "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", + "dev": true, + "requires": { + "p-limit": "^1.1.0" + } + }, + "p-try": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", + "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=", + "dev": true + }, + "parse-filepath": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/parse-filepath/-/parse-filepath-1.0.2.tgz", + "integrity": "sha1-pjISf1Oq89FYdvWHLz/6x2PWyJE=", + "dev": true, + "requires": { + "is-absolute": "^1.0.0", + "map-cache": "^0.2.0", + "path-root": "^0.1.1" + } + }, + "parse-json": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", + "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=", + "dev": true, + "requires": { + "error-ex": "^1.2.0" + } + }, + "parse-passwd": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/parse-passwd/-/parse-passwd-1.0.0.tgz", + "integrity": "sha1-bVuTSkVpk7I9N/QKOC1vFmao5cY=", + "dev": true + }, + "pascalcase": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", + "integrity": "sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=", + "dev": true + }, + "path-exists": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz", + "integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=", + "dev": true, + "requires": { + "pinkie-promise": "^2.0.0" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true + }, + "path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", + "dev": true + }, + "path-parse": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", + "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==", + "dev": true + }, + "path-root": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/path-root/-/path-root-0.1.1.tgz", + "integrity": "sha1-mkpoFMrBwM1zNgqV8yCDyOpHRbc=", + "dev": true, + "requires": { + "path-root-regex": "^0.1.0" + } + }, + "path-root-regex": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/path-root-regex/-/path-root-regex-0.1.2.tgz", + "integrity": "sha1-v8zcjfWxLcUsi0PsONGNcsBLqW0=", + "dev": true + }, + "path-type": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz", + "integrity": "sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0" + } + }, + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", + "dev": true + }, + "pinkie": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", + "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=", + "dev": true + }, + "pinkie-promise": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", + "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", + "dev": true, + "requires": { + "pinkie": "^2.0.0" + } + }, + "pkg-up": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pkg-up/-/pkg-up-2.0.0.tgz", + "integrity": "sha1-yBmscoBZpGHKscOImivjxJoATX8=", + "dev": true, + "requires": { + "find-up": "^2.1.0" + }, + "dependencies": { + "find-up": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", + "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", + "dev": true, + "requires": { + "locate-path": "^2.0.0" + } + } + } + }, + "posix-character-classes": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", + "integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=", + "dev": true + }, + "read-pkg": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", + "integrity": "sha1-9f+qXs0pyzHAR0vKfXVra7KePyg=", + "dev": true, + "requires": { + "load-json-file": "^1.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^1.0.0" + } + }, + "read-pkg-up": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz", + "integrity": "sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI=", + "dev": true, + "requires": { + "find-up": "^1.0.0", + "read-pkg": "^1.0.0" + } + }, + "rechoir": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz", + "integrity": "sha1-hSBLVNuoLVdC4oyWdW70OvUOM4Q=", + "dev": true, + "requires": { + "resolve": "^1.1.6" + } + }, + "redent": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-1.0.0.tgz", + "integrity": "sha1-z5Fqsf1fHxbfsggi3W7H9zDCr94=", + "dev": true, + "requires": { + "indent-string": "^2.1.0", + "strip-indent": "^1.0.1" + } + }, + "regex-not": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz", + "integrity": "sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==", + "dev": true, + "requires": { + "extend-shallow": "^3.0.2", + "safe-regex": "^1.1.0" + } + }, + "repeat-element": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.3.tgz", + "integrity": "sha512-ahGq0ZnV5m5XtZLMb+vP76kcAM5nkLqk0lpqAuojSKGgQtn4eRi4ZZGm2olo2zKFH+sMsWaqOCW1dqAnOru72g==", + "dev": true + }, + "repeat-string": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", + "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=", + "dev": true + }, + "repeating": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/repeating/-/repeating-2.0.1.tgz", + "integrity": "sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo=", + "dev": true, + "requires": { + "is-finite": "^1.0.0" + } + }, + "resolve": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.10.0.tgz", + "integrity": "sha512-3sUr9aq5OfSg2S9pNtPA9hL1FVEAjvfOC4leW0SNf/mpnaakz2a9femSd6LqAww2RaFctwyf1lCqnTHuF1rxDg==", + "dev": true, + "requires": { + "path-parse": "^1.0.6" + } + }, + "resolve-dir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/resolve-dir/-/resolve-dir-1.0.1.tgz", + "integrity": "sha1-eaQGRMNivoLybv/nOcm7U4IEb0M=", + "dev": true, + "requires": { + "expand-tilde": "^2.0.0", + "global-modules": "^1.0.0" + } + }, + "resolve-from": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-2.0.0.tgz", + "integrity": "sha1-lICrIOlP+h2egKgEx+oUdhGWa1c=", + "dev": true + }, + "resolve-pkg": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg/-/resolve-pkg-1.0.0.tgz", + "integrity": "sha1-4ZoV54rKLhJEYdySsuOUPvk0lNk=", + "dev": true, + "requires": { + "resolve-from": "^2.0.0" + } + }, + "resolve-url": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", + "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=", + "dev": true + }, + "ret": { + "version": "0.1.15", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", + "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==", + "dev": true + }, + "rimraf": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", + "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", + "dev": true, + "requires": { + "glob": "^7.1.3" + }, + "dependencies": { + "glob": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz", + "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + } + } + }, + "safe-regex": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", + "integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=", + "dev": true, + "requires": { + "ret": "~0.1.10" + } + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true + }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + }, + "set-value": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz", + "integrity": "sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==", + "dev": true, + "requires": { + "extend-shallow": "^2.0.1", + "is-extendable": "^0.1.1", + "is-plain-object": "^2.0.3", + "split-string": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "signal-exit": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz", + "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==", + "dev": true + }, + "snapdragon": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", + "integrity": "sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==", + "dev": true, + "requires": { + "base": "^0.11.1", + "debug": "^2.2.0", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "map-cache": "^0.2.2", + "source-map": "^0.5.6", + "source-map-resolve": "^0.5.0", + "use": "^3.1.0" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "snapdragon-node": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz", + "integrity": "sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==", + "dev": true, + "requires": { + "define-property": "^1.0.0", + "isobject": "^3.0.0", + "snapdragon-util": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "dev": true, + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + } + } + }, + "snapdragon-util": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz", + "integrity": "sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==", + "dev": true, + "requires": { + "kind-of": "^3.2.0" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true + }, + "source-map-resolve": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.2.tgz", + "integrity": "sha512-MjqsvNwyz1s0k81Goz/9vRBe9SZdB09Bdw+/zYyO+3CuPk6fouTaxscHkgtE8jKvf01kVfl8riHzERQ/kefaSA==", + "dev": true, + "requires": { + "atob": "^2.1.1", + "decode-uri-component": "^0.2.0", + "resolve-url": "^0.2.1", + "source-map-url": "^0.4.0", + "urix": "^0.1.0" + } + }, + "source-map-url": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.0.tgz", + "integrity": "sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=", + "dev": true + }, + "spdx-correct": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.0.tgz", + "integrity": "sha512-lr2EZCctC2BNR7j7WzJ2FpDznxky1sjfxvvYEyzxNyb6lZXHODmEoJeFu4JupYlkfha1KZpJyoqiJ7pgA1qq8Q==", + "dev": true, + "requires": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-exceptions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz", + "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==", + "dev": true + }, + "spdx-expression-parse": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz", + "integrity": "sha512-Yg6D3XpRD4kkOmTpdgbUiEJFKghJH03fiC1OPll5h/0sO6neh2jqRDVHOQ4o/LMea0tgCkbMgea5ip/e+MkWyg==", + "dev": true, + "requires": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-license-ids": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.5.tgz", + "integrity": "sha512-J+FWzZoynJEXGphVIS+XEh3kFSjZX/1i9gFBaWQcB+/tmpe2qUsSBABpcxqxnAxFdiUFEgAX1bjYGQvIZmoz9Q==", + "dev": true + }, + "split-string": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", + "integrity": "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==", + "dev": true, + "requires": { + "extend-shallow": "^3.0.0" + } + }, + "sprintf-js": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.2.tgz", + "integrity": "sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug==", + "dev": true + }, + "static-extend": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", + "integrity": "sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY=", + "dev": true, + "requires": { + "define-property": "^0.2.5", + "object-copy": "^0.1.0" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + } + } + }, + "strip-ansi": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.0.0.tgz", + "integrity": "sha512-Uu7gQyZI7J7gn5qLn1Np3G9vcYGTVqB+lFTytnDJv83dd8T22aGH451P3jueT2/QemInJDfxHB5Tde5OzgG1Ow==", + "dev": true, + "requires": { + "ansi-regex": "^4.0.0" + } + }, + "strip-bom": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", + "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=", + "dev": true, + "requires": { + "is-utf8": "^0.2.0" + } + }, + "strip-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-1.0.1.tgz", + "integrity": "sha1-DHlipq3vp7vUrDZkYKY4VSrhoKI=", + "dev": true, + "requires": { + "get-stdin": "^4.0.1" + } + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + }, + "to-object-path": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", + "integrity": "sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "to-regex": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz", + "integrity": "sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==", + "dev": true, + "requires": { + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "regex-not": "^1.0.2", + "safe-regex": "^1.1.0" + } + }, + "to-regex-range": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", + "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", + "dev": true, + "requires": { + "is-number": "^3.0.0", + "repeat-string": "^1.6.1" + } + }, + "trim-newlines": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-1.0.0.tgz", + "integrity": "sha1-WIeWa7WCpFA6QetST301ARgVphM=", + "dev": true + }, + "unc-path-regex": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/unc-path-regex/-/unc-path-regex-0.1.2.tgz", + "integrity": "sha1-5z3T17DXxe2G+6xrCufYxqadUPo=", + "dev": true + }, + "underscore.string": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-3.3.5.tgz", + "integrity": "sha512-g+dpmgn+XBneLmXXo+sGlW5xQEt4ErkS3mgeN2GFbremYeMBSJKr9Wf2KJplQVaiPY/f7FN6atosWYNm9ovrYg==", + "dev": true, + "requires": { + "sprintf-js": "^1.0.3", + "util-deprecate": "^1.0.2" + } + }, + "union-value": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz", + "integrity": "sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==", + "dev": true, + "requires": { + "arr-union": "^3.1.0", + "get-value": "^2.0.6", + "is-extendable": "^0.1.1", + "set-value": "^2.0.1" + } + }, + "unset-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz", + "integrity": "sha1-g3aHP30jNRef+x5vw6jtDfyKtVk=", + "dev": true, + "requires": { + "has-value": "^0.3.1", + "isobject": "^3.0.0" + }, + "dependencies": { + "has-value": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz", + "integrity": "sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8=", + "dev": true, + "requires": { + "get-value": "^2.0.3", + "has-values": "^0.1.4", + "isobject": "^2.0.0" + }, + "dependencies": { + "isobject": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", + "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", + "dev": true, + "requires": { + "isarray": "1.0.0" + } + } + } + }, + "has-values": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz", + "integrity": "sha1-bWHeldkd/Km5oCCJrThL/49it3E=", + "dev": true + } + } + }, + "urix": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", + "integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=", + "dev": true + }, + "use": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", + "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==", + "dev": true + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", + "dev": true + }, + "v8flags": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/v8flags/-/v8flags-3.1.2.tgz", + "integrity": "sha512-MtivA7GF24yMPte9Rp/BWGCYQNaUj86zeYxV/x2RRJMKagImbbv3u8iJC57lNhWLPcGLJmHcHmFWkNsplbbLWw==", + "dev": true, + "requires": { + "homedir-polyfill": "^1.0.1" + } + }, + "validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dev": true, + "requires": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..74173e7 --- /dev/null +++ b/package.json @@ -0,0 +1,42 @@ +{ + "name": "unity-performance-tuning-bible", + "version": "1.0.0", + "private": true, + "description": "", + "main": "Gruntfile.js", + "repository": { + "type": "git", + "url": "git+https://github.com/CyberAgentGameEntertainment/UnityPerformanceTuningBible.git" + }, + "author": "", + "bugs": { + "url": "https://github.com/CyberAgentGameEntertainment/UnityPerformanceTuningBible/issues" + }, + "homepage": "https://github.com/CyberAgentGameEntertainment/UnityPerformanceTuningBible/blob/main/README.md", + "engines": { + "node": ">=6.0.0" + }, + "scripts": { + "global-bundler": "gem install bundler", + "global": "npm run global-bundler", + "postinstall": "bundle install", + "pdf": "grunt pdf", + "md": "grunt markdown", + "html": "grunt html", + "text": "grunt text", + "epub": "grunt epub", + "web": "grunt web", + "idgxml": "grunt idgxmlmaker", + "vivliostyle": "grunt vivliostyle", + "test": "npm run html" + }, + "dependencies": {}, + "devDependencies": { + "grunt": "^1.1.0", + "grunt-cli": "^1.2.0", + "grunt-contrib-clean": "^2.0.0", + "grunt-shell": "^3.0.1", + "js-yaml": "^3.13.1", + "load-grunt-tasks": "^4.0.0" + } +} diff --git a/prh-rules/README.ja.md b/prh-rules/README.ja.md new file mode 100644 index 0000000..28bfad4 --- /dev/null +++ b/prh-rules/README.ja.md @@ -0,0 +1,21 @@ +# A collection of prh rules [![CircleCI](https://circleci.com/gh/prh/rules.svg?style=svg)](https://circleci.com/gh/prh/rules) + +## フォルダ構成 + +* languages + * 各自然言語固有のルール +* terms + * 技術用語のルール + * 商標やツールの正式名称などのルール + * 各種ツール固有のルール +* media + * 各媒体固有のルール + +## ルール + +* yaml内でimportを使ってはならない + * rootディレクトリのprh.ymlとmediumの中は除く +* 原則として、media内のルールを他のファイルから参照しない(組み合わせて運用する前提のものではないため) +* mediaには原則として、各団体・出版社別ルールを置く + * 作品別ルールは作品のリポジトリにて個別に管理するのを推奨する +* 団体内部で複数のファイルを置きたい場合、 media/techbooster/ のように団体名のディレクトリを切ってその中で自由にすること diff --git a/prh-rules/README.md b/prh-rules/README.md new file mode 100644 index 0000000..5cdd724 --- /dev/null +++ b/prh-rules/README.md @@ -0,0 +1,3 @@ +# A collection of prh rules [![CircleCI](https://circleci.com/gh/prh/rules.svg?style=svg)](https://circleci.com/gh/prh/rules) + +[in japanese](https://github.com/prh/rules/blob/master/README.ja.md) diff --git a/prh-rules/languages/ja/typo.yml b/prh-rules/languages/ja/typo.yml new file mode 100644 index 0000000..a2ab296 --- /dev/null +++ b/prh-rules/languages/ja/typo.yml @@ -0,0 +1,11 @@ +# Rules for Japanese typo +meta: + reviewer: + - vvakame + - mhidaka + rules: https://github.com/prh/rules + +version: 1 +rules: + - expected: なるほど + pattern: なほるど diff --git a/prh-rules/media/WEB+DB_PRESS.yml b/prh-rules/media/WEB+DB_PRESS.yml new file mode 100644 index 0000000..2d86173 --- /dev/null +++ b/prh-rules/media/WEB+DB_PRESS.yml @@ -0,0 +1,3021 @@ +# Rules for WEB+DB PRESS +meta: + reviewer: + - vvakame + - mhidaka + related: http://gihyo.jp/magazine/wdpress + rules: https://github.com/prh/rules + +version: 1 + +# 非公開リポジトリだけど入手元はこのあたりから。公開許可はもらってある。現時点最新版ぽい。 +# https://github.com/vvakame/webdb-frontend2016/pull/59 +# > ただし、カタカナ語やアルファベットなどについてはそのまま使えなさそうだったため外してあります。 +# > また、うまくprhのルールとできなかったものも外しています。 +# とのこと。 +# 本家webdbrules.rel から @murashitas さんセンスで微妙そうなのが抜いてあるようだ。 + +# 他のファイルとpattern expectedの順序が逆なのに注意 +# 株式会社 のあたりから正常に戻ってるので更に注意 + +rules: + - pattern: /つく([らりるれろっ])/ + expected: 作$1 + - pattern: + - 結びつ + - むすびつ + expected: 結び付 + - pattern: + - 受けつ + - うけつ + expected: 受け付 + - pattern: + - 受けと + - うけと + expected: 受け取 + - pattern: 日付け + expected: 日付 + - pattern: つきあ + expected: 付き合 + - pattern: + - つけ加 + - つけくわ + expected: 付け加 + - pattern: つけた + expected: 付け足 + - pattern: 関連づけ + expected: 関連付け + - pattern: 受付 + expected: 受け付け + - pattern: 位置づけ + expected: 位置付け + - pattern: 意味づけ + expected: 意味付け + - pattern: 気をつ + expected: 気を付 + - pattern: 気づ + expected: 気付 + - pattern: 近づ + expected: 近付 + - pattern: + - 検討がつ + - 検討がつ + expected: 見当が付 + - pattern: 思いつく + expected: 思い付く + - pattern: + - 1つ1つ + - 一つひとつ + - ひとつひとつ + expected: 一つ一つ + - pattern: + - 一人ひとり + - ひとりひとり + - 1人1人 + expected: 一人一人 + - pattern: + - 一つめ + - ひとつめ + - 一つ目 + - ひとつ目 + expected: 1つ目 + - pattern: + - 二つめ + - ふたつめ + - 二つ目 + - ふたつ目 + expected: 2つ目 + - pattern: + - 三つめ + - みっつめ + - 三つ目 + - みっつ目 + expected: 3つ目 + - pattern: + - 四つめ + - よっつめ + - 四つ目 + - よっつ目 + expected: 4つ目 + - pattern: + - 五つめ + - いつつめ + - 五つ目 + - いつつ目 + expected: 5つ目 + - pattern: + - 六つめ + - むっつめ + - 六つ目 + - むっつ目 + expected: 6つ目 + - pattern: + - 七つめ + - ななつめ + - 七つ目 + - ななつ目 + expected: 7つ目 + - pattern: + - 八つめ + - やっつめ + - 八つ目 + - やっつ目 + expected: 8つ目 + - pattern: + - 九つめ + - ここのつめ + - 九つ目 + - ここのつ目 + expected: 9つ目 + - pattern: + - 一番目 + - 一番め + expected: 1番目 + - pattern: + - 二番目 + - 二番め + expected: 2番目 + - pattern: + - 三番目 + - 三番め + expected: 3番目 + - pattern: + - 四番目 + - 四番め + expected: 4番目 + - pattern: + - 五番目 + - 五番め + expected: 5番目 + - pattern: + - 六番目 + - 六番め + expected: 6番目 + - pattern: + - 七番目 + - 七番め + expected: 7番目 + - pattern: + - 八番目 + - 八番め + expected: 8番目 + - pattern: + - 九番目 + - 九番め + expected: 9番目 + - pattern: + - 十番目 + - 十番め + expected: 10番目 + - pattern: + - ふたつ + - 二つ + expected: 2つ + - pattern: + - みっつ + - 三つ + expected: 3つ + - pattern: + - よっつ + - 四つ + expected: 4つ + - pattern: + - いつつ + - 五つ + expected: 5つ + - pattern: + - むっつ + - 六つ + expected: 6つ + - pattern: + - ななつ + - 七つ + expected: 7つ + - pattern: + - やっつ + - 八つ + expected: 8つ + - pattern: + - ここのつ + - 九つ + expected: 9つ + - pattern: /([\d]+)つめ + expected: $1つ目 + - pattern: /([\d]+)番め/ + expected: $1番目 + - pattern: /いちばん([^め目])|1番([^め目])/ + expected: 一番$1 + - pattern: + - ただ1つ + - 唯1つ + - 唯一つ + expected: ただ一つ + - pattern: もう1度 + expected: もう一度 + - pattern: もう1つ + expected: もう一つ + - pattern: 1つは + expected: 一つは + - pattern: 一目了然 + expected: 一目瞭然 + - pattern: いちがいに + expected: 一概に + - pattern: 一種類 + expected: 1種類 + - pattern: 1種 + expected: 一種 + - pattern: いっしょ + expected: 一緒 + - pattern: + - ひととおり + - ひと通り + - 一とおり + expected: 一通り + - pattern: 1度 + expected: 一度 + - pattern: 1部 + expected: 一部 + - pattern: 1文 + expected: 一文 + - pattern: 1例 + expected: 一例 + - pattern: 2重 + expected: 二重 + - pattern: 3重 + expected: 三重 + - pattern: /([何数\\d一二三四五六七八九十0-9])箇所/ + expected: $1ヵ所 + - pattern: /([何数\\d一二三四五六七八九十0-9])個所/ + expected: $1ヵ所 + - pattern: ヶ + expected: ヵ + - pattern: + - 箇月 + - か月 + - 個月 + - ケ月 + - カ月 + expected: ヵ月 + - pattern: + - 箇国 + - か国 + - 個国 + - ケ国 + - カ国 + expected: ヵ国 + - pattern: + - か所 + - ケ所 + - カ所 + expected: ヵ所 + - pattern: 個所 + expected: 箇所 + - pattern: + - いちから + - 1から + - 一から + - イチから + - 0から + expected: ゼロから + - pattern: 曖昧 + expected: あいまい + - pattern: 敢えて + expected: あえて + - pattern: /煽([らりるれろっ])/ + expected: あお$1 + - pattern: 飽くまで + expected: あくまで + - pattern: 憧れ + expected: あこがれ + - pattern: 辺り + expected: あたり + - pattern: 後々 + expected: あとあと + - pattern: + - 貴方 + - 貴女 + expected: あなた + - pattern: 予め + expected: あらかじめ + - pattern: 改めて + expected: あらためて + - pattern: + - 有り難 + - 有難 + expected: ありがた + - pattern: 有りま + expected: ありま + - pattern: 或いは + expected: あるいは + - pattern: + - 有る + - 在る + expected: ある + - pattern: /一体([^に化感])/ + expected: いったい$1 + - pattern: 一向に + expected: いっこうに + - pattern: 一旦 + expected: いったん + - pattern: 如何 + expected: いかが + - pattern: 如何に + expected: いかに + - pattern: + - ゆきま + - 行きま + expected: いきま + - pattern: 幾つ + expected: いくつ + - pattern: 幾ら + expected: いくら + - pattern: 幾分 + expected: いくぶん + - pattern: 些か + expected: いささか + - pattern: + - 何れ + - いづれ + expected: いずれ + - pattern: /頂([いかきくけこ])|戴([いかきくけこ])/ + expected: いただ$1 + - pattern: 至って + expected: いたって + - pattern: 一々 + expected: いちいち + - pattern: /何時([^間])/ + expected: いつ$1 + - pattern: 一杯 + expected: いっぱい + - pattern: /厭([わいうえおっ])/ + expected: いと$1 + - pattern: おります + expected: います + - pattern: 今や + expected: いまや + - pattern: 色々 + expected: いろいろ + - pattern: 色んな + expected: いろんな + - pattern: 所謂 + expected: いわゆる + - pattern: ゆう + expected: いう + - pattern: /云([いうわっ])/ + expected: い$1 + - pattern: /要([らりるれろっ])/ + expected: い$1 + - pattern: /居([るた])/ + expected: い$1 + - pattern: /穿([たちつてとっ])/ + expected: うが$1 + - pattern: + - 憂っとおし + - 憂っとうし + - うっとおし + - 鬱陶し + expected: うっとうし + - pattern: + - うなづ + - 頷 + expected: うなず + - pattern: /上手([いく])|美味([いく])|ウマ([いく])/ + expected: うま$1 + - pattern: 嬉し + expected: うれし + - pattern: 美味し + expected: おいし + - pattern: 於いて + expected: おいて + - pattern: 大掛かり + expected: おおがかり + - pattern: 概ね + expected: おおむね + - pattern: + - お陰 + - 御陰 + expected: おかげ + - pattern: + - 押し並べて + - 押しなべて + expected: おしなべて + - pattern: 自ずと + expected: おのずと + - pattern: 恐らく + expected: おそらく + - pattern: お疲れ様 + expected: お疲れさま + - pattern: 面白 + expected: おもしろ + - pattern: 却って + expected: かえって + - pattern: 片仮名 + expected: カタカナ + - pattern: 且つ + expected: かつ + - pattern: /叶([わいうえおっ])/ + expected: かな$1 + - pattern: 仮名 + expected: かな + - pattern: /構([いうわっ])/ + expected: かま$1 + - pattern: /痒([いかくさみ])/ + expected: かゆ$1 + - pattern: + - 綺麗 + - 奇麗 + expected: きれい + - pattern: /括([らりるれろっ])/ + expected: くく$1 + - pattern: 下さい + expected: ください + - pattern: /([^解])決して|([^解])けして/ + expected: $1けっして + - pattern: /拘([らりるれろっ])/ + expected: こだわ$1 + - pattern: 諺 + expected: ことわざ + - pattern: 零れ + expected: こぼれ + - pattern: この章 + expected: 本章 + - pattern: /この様([なに])/ + expected: このよう$1 + - pattern: 年頃 + expected: 年ごろ + - pattern: 歳頃 + expected: 歳ごろ + - pattern: 日頃 + expected: 日ごろ + - pattern: 頃 + expected: ころ + - pattern: /遡([らりるれろっ])/ + expected: さかのぼ$1 + - pattern: さしづめ + expected: さしずめ + - pattern: 流石 + expected: さすが + - pattern: 早速 + expected: さっそく + - pattern: /捌([いかきくけこ])/ + expected: さば$1 + - pattern: 様々 + expected: さまざま + - pattern: /晒([さしすせそ])/ + expected: さら$1 + - pattern: 仕掛け + expected: しかけ + - pattern: 仕方 + expected: しかた + - pattern: /([^正率垂安素愚])直に/ + expected: $1じかに + - pattern: /仕組([^み])/ + expected: しくみ$1 + - pattern: 仕組み + expected: しくみ + - pattern: しし + expected: し + - pattern: しだい + expected: 次第 + - pattern: + - 暫く + - 暫らく + expected: しばらく + - pattern: いたします + expected: します + - pattern: 所詮 + expected: しょせん + - pattern: + - しようがない + - 仕様がない + expected: しょうがない + - pattern: 知れない + expected: しれない + - pattern: 知れません + expected: しれません + - pattern: 随分 + expected: ずいぶん + - pattern: 直ぐ + expected: すぐ + - pattern: づくめ + expected: ずくめ + - pattern: /凄([いかくけ])/ + expected: すご$1 + - pattern: づつ + expected: ずつ + - pattern: 既に + expected: すでに + - pattern: + - すなはち + - 即ち + expected: すなわち + - pattern: + - 素晴らし + - 素晴し + expected: すばらし + - pattern: 素早 + expected: すばや + - pattern: 須らく + expected: すべからく + - pattern: 折角 + expected: せっかく + - pattern: 是非 + expected: ぜひ + - pattern: そうゆう + expected: そういう + - pattern: 其々 + expected: それぞれ + - pattern: 其れ + expected: それ + - pattern: /揃([わいうえおっ])/ + expected: そろ$1 + - pattern: 大体 + expected: だいたい + - pattern: 大抵 + expected: たいてい + - pattern: 大変 + expected: たいへん + - pattern: 沢山 + expected: たくさん + - pattern: + - 高々 + - 高高 + - 高だか + expected: たかだか + - pattern: /叩([かきくけこ])/ + expected: たた$1 + - pattern: 但し + expected: ただし + - pattern: 直ち + expected: ただち + - pattern: 著者|私 + expected: 筆者 + - pattern: /辿([らりるれろっ])/ + expected: たど$1 + - pattern: 例えば + expected: たとえば + - pattern: + - 譬え + - 喩え + - 例え + expected: たとえ + - pattern: たのし + expected: 楽し + - pattern: 度々 + expected: たびたび + - pattern: /溜ま([らりるれろっ])/ + expected: たま$1 + - pattern: 因みに + expected: ちなみに + - pattern: 丁度 + expected: ちょうど + - pattern: 遂に + expected: ついに + - pattern: /掴([まみむめもん])/ + expected: つか$1 + - pattern: /繋([らりるれろっ])/ + expected: つなが$1 + - pattern: /繋([いがぎぐげご])/ + expected: つな$1 + - pattern: つねに + expected: 常に + - pattern: /つまづ([いかきくけこ])|躓([いかきくけこ])/ + expected: つまず$1 + - pattern: /辛([いかくけ])/ + expected: つら$1 + - pattern: /ずら([いかくけ])/ + expected: づら$1 + - pattern: んで([たなるろよ]) + expected: んでい$1 + - pattern: /てな(?!く)/ + expected: ていな + specs: + - from: わかってない + to: わかっていない + - from: 消えてなくなる + to: 消えてなくなる + - pattern: /([^捨当立])てる/ + expected: $1ている + - pattern: /出来([かがのをは過す損ずなまるたれてそ])/ + expected: でき$1 + - pattern: + - 出来上 + - 出来あ + - でき上 + expected: できあ + - pattern: + - することが可能です + - することが可能になります + expected: できます + - pattern: + - することができ + - することが可能で + expected: でき + - pattern: るようになります + expected: ます + - pattern: て欲し + expected: てほし + - pattern: で欲し + expected: でほし + - pattern: とうり + expected: とおり + - pattern: ときより + expected: ときおり + - pattern: 時々 + expected: ときどき + - pattern: とくに + expected: 特に + - pattern: + - 何処 + - 何所 + expected: どこ + - pattern: 途端 + expected: とたん + - pattern: /(ピン)?留([まめ])/ + regexpMustEmpty: $1 + expected: とど$2 + specs: + - from: ピン留め + to: ピン留め + - from: 踏み留まる + to: 踏みとどまる + - pattern: /とは言え([^なまる])/ + expected: とはいえ$1 + - pattern: 共に + expected: ともに + - pattern: /捉え([ずなにぬまよらたてるろっ方中])/ + expected: とらえ$1 + - pattern: 囚わ + expected: とらわ + - pattern: + - 取りあえず + - 取り合えず + - 取り敢えず + expected: とりあえず + - pattern: /([^有])無([かくいけし])/ + expected: $1な$1 + - pattern: /尚([^早徳])/ + expected: なお$1 + - pattern: + - 名残 + - 名残り + expected: なごり + - pattern: 何故 + expected: なぜ + - pattern: + - 何某か + - 某か + expected: なにがしか + - pattern: /成り?済ま?([さしすせそ])|成り?すま([さしすせそ])/ + expected: なりすま$1 + - pattern: なので、 + expected: ですので、 + - pattern: 何となく + expected: なんとなく + - pattern: /に当た([っらりるれろ])/ + expected: にあた$1 + - pattern: /難([いく])/ + expected: にく$1 + - pattern: /の様([なにだで])/ + expected: のよう$1 + - pattern: /育([まみむめもん])/ + expected: はぐく$1 + - pattern: /([^手])筈/ + expected: $1はず + - pattern: 甚だ + expected: はなはだ + - pattern: /孕([まみむめもんっ])/ + expected: はら$1 + - pattern: + - 遥か + - 遙か + expected: はるか + - pattern: 贔屓 + expected: ひいき + - pattern: しいては + expected: ひいては + - pattern: + - 独り善がり + - 独りよがり + expected: ひとりよがり + - pattern: 一塊 + expected: ひとかたまり + - pattern: + - 雛型 + - 雛形 + - ひな形 + - ひながた + expected: ひな型 + - pattern: /閃([かきくけこい])/ + expected: ひらめ$1 + - pattern: ふだん + expected: 普段 + - pattern: 相応し + expected: ふさわし + - pattern: + - 付箋 + - 付せん + expected: ふせん + - pattern: 殆ど + expected: ほとんど + - pattern: 正に + expected: まさに + - pattern: + - 益々 + - 増々 + expected: ますます + - pattern: 先ず + expected: まず + - pattern: /瞬([いかきくけこ])/ + expected: またた$1 + - pattern: 又は + expected: または + - pattern: + - 真っ新 + - 真っさら + expected: まっさら + - pattern: 迄 + expected: まで + - pattern: 纏ま + expected: まとま + - pattern: 纏め + expected: まとめ + - pattern: 稀 + expected: まれ + - pattern: /剥([いかきくけこ])/ + expected: む$1 + - pattern: 寧ろ + expected: むしろ + - pattern: 滅多 + expected: めった + - pattern: + - めんどくさ + - めんどうくさ + - 面倒臭 + - めんど臭 + - めんどう臭 + expected: 面倒くさ + - pattern: めんどう + expected: 面倒 + - pattern: 目論見 + expected: もくろみ + - pattern: 勿論 + expected: もちろん + - pattern: 勿体な + expected: もったいな + - pattern: もっとも + expected: 最も + - pattern: 尤も + expected: もっとも + - pattern: /貰([わいうえおっ])/ + expected: もら$1 + - pattern: 易し + expected: やさし + - pattern: 易々 + expected: やすやす + - pattern: 厄介 + expected: やっかい + - pattern: やっぱり + expected: やはり + - pattern: 闇雲 + expected: やみくも + - pattern: 止め + expected: やめ + - pattern: やり取り + expected: やりとり + - pattern: 他所 + expected: よそ + - pattern: /因([らりるれろっ])|依([らりるれろっ])/ + expected: よ$1 + - pattern: 僅か + expected: わずか + - pattern: いい + expected: よい + - pattern: 心地よ + expected: 心地良 + - pattern: /判([らりるれろっ])/ + expected: わか$1 + - pattern: /解([らりるれろっ])/ + expected: わか$1 + - pattern: /の分([^散割類解野離析岐])/ + expected: のぶん$1 + - pattern: + - 気をつか + - 気を使 + expected: 気を遣 + - pattern: 小さ目 + expected: 小さめ + - pattern: 大き目 + expected: 大きめ + - pattern: 少な目 + expected: 少なめ + - pattern: /多目([^的])/ + expected: 多め$1 + - pattern: 低目 + expected: 低め + - pattern: 高目 + expected: 高め + - pattern: /([のる])かわり/ + expected: $1代わり + - pattern: /代([らりるれろっ])/ + expected: 代わ$1 + - pattern: 全て + expected: すべて + - pattern: 全く + expected: まったく + - pattern: /([にも])関わらず|([にも])関らず|([にも])拘わらず|([にも])拘らず/ + expected: $1かかわらず + - pattern: /埋めこ([まむめもん])|うめこ([まむめもん])/ + expected: 埋め込$1 + - pattern: /置き変([わえ])|置き代([わえ])|置き替([わえ])|置きか([わえ])/ + expected: 置き換$1 + - pattern: /書きこ([まみむめもん])|かきこ([まみむめもん])/ + expected: 書き込$1 + - pattern: /書き替([えわ])|書きか([えわ])|かきか([えわ])|書換([えわ])|書き変([えわ])/ + expected: 書き換$1 + - pattern: /読みこ([まみむめもん])|よみこ([まみむめもん])|読込([まみむめもん])/ + expected: 読み込$1 + - pattern: /読みか([えわ])|よみか([えわ])|読替([えわ])|読換([えわ])|読み換([えわ])/ + expected: 読み替$1 + - pattern: /きりか([えわ])|切り換([えわ])|切換([えわ])|切替([えわ])/ + expected: 切り替$1 + - pattern: /くみこ([まむめもん])|組込([まむめもん])|組みこ([まむめもん])/ + expected: 組み込$1 + - pattern: /くみこみ([^ま])|組み込み([^ま])|組みこみ([^ま])/ + expected: 組込み$1 + - pattern: /組合([わいうえおっ])|くみあ([わいうえおっ])/ + expected: 組み合$1 + - pattern: /組合([さしすせそっ])|くみあわ([さしすせそっ])/ + expected: 組み合わ$1 + - pattern: /くみか([わいうえおっ])|組替([わいうえおっ])/ + expected: 組み替$1 + - pattern: /ことな([りるれっ])/ + expected: 異な$1 + - pattern: /立ち合([わいうえおっ])|立ち遭([わいうえおっ])|立ち逢([わいうえおっ])/ + expected: 立ち会$1 + - pattern: /取りく([まみむめもん])|とりく([まみむめもん])|とり組([まみむめもん])/ + expected: 取り組$1 + - pattern: /取り変([わえ])|取り代([わえ])|取り換([わえ])|取りか([わえ])/ + expected: 取り替$1 + - pattern: /話し会([わいうえおっ])|話し遭([わいうえおっ])|話し逢([わいうえおっ])|話しあ([わいうえおっ])/ + expected: 話し合$1 + - pattern: /ひきつ([がぎぐげご])|引きつ([がぎぐげご])/ + expected: 引き継$1 + - pattern: /引き替([えわ])|引きか([えわ])|ひきか([えわ])|引換([えわ])|引き変([えわ])/ + expected: 引き換$1 + - pattern: /目が会([わいうえおっ])|目が遭([わいうえおっ])|目が逢([わいうえおっ])|目があ([わいうえおっ])/ + expected: 目が合$1 + - pattern: + - よびだ + - 呼びだ + - 呼出 + expected: 呼び出 + - pattern: 作りだ + expected: 作り出 + - pattern: 読みだ + expected: 読み出 + - pattern: 貼りだ + expected: 貼り出 + - pattern: 取りだ + expected: 取り出 + - pattern: + - とりあげ + - 取りあげ + expected: 取り上げ + - pattern: /とりこ([まみむめもん])|取りこ([まみむめもん])|とり込([まみむめもん])/ + expected: 取り込$1 + - pattern: 作りあ + expected: 作り上 + - pattern: + - からみあ + - からみ合 + - 絡みあ + expected: 絡み合 + - pattern: からっぽ + expected: 空っぽ + - pattern: 売上げ + expected: 売り上げ + - pattern: + - 巡り会 + - 巡り遭 + - 巡り逢 + expected: 巡り合 + - pattern: 割込み + expected: 割り込み + - pattern: 割当 + expected: 割り当て + - pattern: 割切り + expected: 割り切り + - pattern: + - しめくく + - 締め括 + expected: 締めくく + - pattern: 使いなれ + expected: 使い慣れ + - pattern: + - 使いわけ + - つかいわけ + expected: 使い分け + - pattern: 落としこ + expected: 落とし込 + - pattern: 切りわけ + expected: 切り分け + - pattern: + - 間に会 + - 間に遭 + - 間に逢 + expected: 間に合 + - pattern: + - 災難に合 + - 災難に会 + - 災難に逢 + expected: 災難に遭 + - pattern: + - 事故に合 + - 事故に会 + - 事故に逢 + expected: 事故に遭 + - pattern: + - 気が会 + - 気が遭 + - 気が逢 + expected: 気が合$1 + - pattern: + - 落ち会 + - 落ち遭 + - 落ち逢 + expected: 落ち合 + - pattern: /ふるま([わいうえおっ])|振舞([わいうえおっ])|振るま([わいうえおっ])/ + expected: 振る舞$1 + - pattern: /みはから([いうえっ])/ + expected: 見計ら$1 + - pattern: みえ + expected: 見え + - pattern: みあた + expected: 見当た + - pattern: + - いいわけ + - 言訳 + - 言いわけ + expected: 言い訳 + - pattern: いえない + expected: 言えない + - pattern: いえなく + expected: 言えなく + - pattern: いえる + expected: 言える + - pattern: いわれる + expected: 言われる + - pattern: + - 未だ + - 今だ + expected: いまだ + - pattern: + - 今ひとつ + - 今一つ + - いま一つ + expected: いまひとつ + - pattern: + - 今更 + - 今さら + expected: いまさら + - pattern: 今頃 + expected: 今ごろ + - pattern: /([^な])いままで/ + expected: $1今まで + - pattern: 今時 + expected: 今どき + - pattern: 今更 + expected: 今さら + - pattern: 尚更 + expected: なおさら + - pattern: 更なる + expected: さらなる + - pattern: /([^変])更に/ + expected: $1さらに + - pattern: やむをえ|止むを得 + expected: やむを得 + - pattern: すこし + expected: 少し + - pattern: /すくな(?!り|る)/ + expected: 少な + specs: + - from: すくなく + to: 少なく + - from: わかりやすくなり + to: わかりやすくなり + - pattern: 少い + expected: 少ない + - pattern: おこし + expected: 起こし + - pattern: おこす + expected: 起こす + - pattern: 何げな + expected: 何気な + - pattern: なにか + expected: 何か + - pattern: なにしろ + expected: 何しろ + - pattern: + - なにとそ + - なにとぞ + - 何卒 + expected: 何とぞ + - pattern: なんど + expected: 何度 + - pattern: なんら + expected: 何ら + - pattern: なんの + expected: 何の + - pattern: 生れ + expected: 生まれ + - pattern: それ故 + expected: それゆえ + - pattern: /([^事])故に/ + expected: $1ゆえに + - pattern: 程々 + expected: ほどほど + - pattern: これ程 + expected: これほど + - pattern: それ程 + expected: それほど + - pattern: あれ程 + expected: あれほど + - pattern: どれ程 + expected: どれほど + - pattern: なる程 + expected: なるほど + - pattern: さきほど|先程 + expected: 先ほど + - pattern: 本紙 + expected: 本誌 + - pattern: /基([いかきくけこ])|基ず([いかきくけこ])|もとず([いかきくけこ])|もとづ([いかきくけこ])/ + expected: 基づ$1 + - pattern: + - 元々 + - 本々 + expected: もともと + - pattern: /基([^数準盤板底礎本])/ + expected: もと$1 + - pattern: /([^追])及び/ + expected: $1および + - pattern: した上 + expected: したうえ + - pattern: その上 + expected: そのうえ + - pattern: 始めとする + expected: はじめとする + - pattern: 始めとした + expected: はじめとした + - pattern: はじめて + expected: 初めて + - pattern: はじめた + expected: 始めた + - pattern: はじま + expected: 始ま + - pattern: はじめ + expected: 始め + - pattern: かたまり + expected: 塊 + - pattern: やりかた + expected: やり方 + - pattern: ありかた + expected: あり方 + - pattern: かたち + expected: 形 + - pattern: かた + expected: 方 + - pattern: 済まな + expected: すまな + - pattern: 済みません + expected: すみません + - pattern: あたりまえ + expected: 当たり前 + - pattern: /汚な([かくいけ])/ + expected: 汚$1 + - pattern: /果([さしすせそ])/ + expected: 果た$1 + - pattern: /([^排人])他([^ァ-ヶ社者人方])/ + expected: $1ほか$2 + - pattern: 後ほど + expected: のちほど + - pattern: うしろ + expected: 後ろ + - pattern: 同士 + expected: どうし + - pattern: + - 頭が堅 + - 頭が硬 + expected: 頭が固 + - pattern: あつめ + expected: 集め + - pattern: /あては([まめ])/ + expected: 当ては$1 + - pattern: 意志決定 + expected: 意思決定 + - pattern: 威大 + expected: 偉大 + - pattern: いっそう + expected: 一層 + - pattern: おおまか + expected: 大まか + - pattern: あふれ + expected: 溢れ + - pattern: 窺え + expected: うかがえ + - pattern: + - 窺い知 + - 伺い知 + expected: うかがい知 + - pattern: /失な([わいうえおっ])/ + expected: 失$1 + - pattern: /おお([かいくけこ])/ + expected: 多$1 + - pattern: そって + expected: 沿って + - pattern: /押え([なたまれる])/ + expected: 押さえ$1 + - pattern: 押し進め + expected: 推し進め + - pattern: /折返([さしすせそ])/ + expected: 折り返$1 + - pattern: /加([らりるれろっ])/ + expected: 加わ$1 + - pattern: 静的型付け言語 + expected: 静的型付き言語 + - pattern: 動的型付け言語 + expected: 動的型付き言語 + - pattern: 変わり映え + expected: 代わり映え + - pattern: 狗肉の策 + expected: 苦肉の策 + - pattern: /こわれ([たちつてとらりるれろっ])/ + expected: 壊れ$1 + - pattern: 混み入っ + expected: 込み入っ + - pattern: /怪([いまみむめも])/ + expected: 怪し$1 + - pattern: おのおの + expected: 各々 + - pattern: 最新の注意 + expected: 細心の注意 + - pattern: 列志向 + expected: 列指向 + - pattern: 行志向 + expected: 行指向 + - pattern: オブジェクト志向 + expected: オブジェクト指向 + - pattern: ドキュメント志向 + expected: ドキュメント指向 + - pattern: アスペクト志向 + expected: アスペクト指向 + - pattern: 粗結合 + expected: 疎結合 + - pattern: 確かに + expected: たしかに + - pattern: /たしか([^に])/ + expected: 確か + - pattern: /確め([たなまれる])/ + expected: 確かめ$1 + - pattern: /かっこ([^良悪いイ])|カッコ([^良悪いイ])/ + expected: 括弧$1 + - pattern: かんたん + expected: 簡単 + - pattern: 業社 + expected: 業者 + - pattern: /陥([たなま])/ + expected: 陥れ$1 + - pattern: 危い + expected: 危ない + - pattern: /喜し([かくいけ])/ + expected: 喜ばし$1 + - pattern: /起([さしすせそらりるれろっ])/ + expected: 起こ$1 + - pattern: /輝し([かくいけ])/ + expected: 輝かし$1 + - pattern: /恐し([かくいけ])/ + expected: 恐ろし$1 + - pattern: 近頃 + expected: 近ごろ + - pattern: /隣合([わいうえおっ])/ + expected: 隣り合$1 + - pattern: + - 決っ + - きまっ + expected: 決まっ + - pattern: + - 現われ + - あらわれ + expected: 現れ + - pattern: /かぎ([らりるれろっ])/ + expected: 限$1 + - pattern: /語([わいうえお])/ + expected: 語ら$1 + - pattern: /交([らりるれろっ])/ + expected: 交わ$1 + - pattern: /行な([わいうえおっ])|おこな([わいうえおっ])/ + expected: 行$1 + - pattern: あわて + expected: 慌 + - pattern: あわ + expected: 合わ + - pattern: /混([らりるれろっ])/ + expected: 混ざ$1 + - pattern: おも([いうわ]) + expected: 思$1 + - pattern: 差違 + expected: 差異 + - pattern: + - プレフィックス + - プレフィクス + - プリフィックス + expected: 接頭辞 + - pattern: + - サフィックス + - サフィックス + - サフィクス + expected: 接尾辞 + - pattern: 事前準備 + expected: 下準備 + - pattern: 前準備 + expected: 下準備 + - pattern: 誌幅 + expected: 紙幅 + - pattern: 紙面 + expected: 誌面 + - pattern: /賜わ([らりるれろっ])/ + expected: 賜$1 + - pattern: しだいに + expected: 次第に + - pattern: /すて([たちつてとらりるれろっ])/ + expected: 捨て$1 + - pattern: 成功を納め + expected: 成功を収め + - pattern: /おわ([らりるれろっ][^か])/ + expected: 終わ$1 + - pattern: /終([らりるれろっ])/ + expected: 終わ$1 + - pattern: /集([らりるれろっ])/ + expected: 集ま$1 + - pattern: 充分 + expected: 十分 + - pattern: したがう + expected: 従う + - pattern: とりま + expected: 取り巻 + - pattern: とうてい + expected: 到底 + - pattern: /柔か([だない])/ + expected: 柔らか$1 + - pattern: /重([ずじ])/ + expected: 重ん$1 + - pattern: /畳込([まみむめも])/ + expected: 畳み込$1 + - pattern: 定形的 + expected: 定型的 + - pattern: + - 出合 + - 出遭 + - 出逢 + expected: 出会 + - pattern: + - できごと + - でき事 + - 出来ごと + expected: 出来事 + - pattern: 適正 + expected: 適性 + - pattern: /助([たなまれる])/ + expected: 助け$1 + - pattern: + - 問合せ + - 問い合せ + - 問合わせ + - 問いあわせ + - といあわせ + expected: 問い合わせ + - pattern: /承わ([らりるれろっ])/ + expected: 承$1 + - pattern: ご紹介 + expected: 紹介 + - pattern: 晴やか + expected: 晴れやか + - pattern: /清か([だな])/ + expected: 清らか$1 + - pattern: おいたち + expected: 生い立ち + - pattern: /積([らりるれろっ])/ + expected: 積も$1 + - pattern: ご説明 + expected: 説明 + - pattern: 憎しい + expected: 憎らしい + - pattern: /つづ([^つ])/ + expected: 続$1 + - pattern: + - 取って変 + - 取って換 + - 取って替 + expected: 取って代 + - pattern: + - 存知 + - ぞんじ + expected: 存じ + - pattern: だれ + expected: 誰 + - pattern: /断わ([らりるれろっ])/ + expected: 断$1 + - pattern: 内臓 + expected: 内蔵 + - pattern: /なか([かでに])/ + expected: 中$1 + - pattern: /なら([ばびぶべぼん])/ + expected: 並$1 + - pattern: /著わ([さしすせそ])/ + expected: 著$1 + - pattern: 著い + expected: 著しい + - pattern: + - 手に追えな + - てに追えな + - てに負えな + - てにおえな + - 手におえな + expected: 手に負えな + - pattern: /定([らりるれろっ])/ + expected: 定ま$1 + - pattern: /当([らりるれろっ])/ + expected: 当た$1 + - pattern: + - 踏え + - ふまえ + expected: 踏まえ + - pattern: /おなじ([^み])/ + expected: 同じ$1 + - pattern: なじみ + expected: 馴染み + - pattern: + - 引きづ + - ひきづ + expected: 引きず + - pattern: むずかしい + expected: 難しい + - pattern: /まか([さしすせそ])/ + expected: 任$1 + - pattern: /([^っ])伴な|([^っ])ともな/ + expected: $1伴 + - pattern: /悲([まみむめも])/ + expected: 悲し$1 + - pattern: + - 較べ + - くらべ + expected: 比べ + - pattern: + - かならず + - 必らず + expected: 必ず + - pattern: /表わ([さしすせそ])|あらわ([さしすせそ])/ + expected: 表$1 + - pattern: /浮([ばびぶべぼん])/ + expected: 浮か$1 + - pattern: /わかれ([^ば])/ + expected: 分かれ$1 + - pattern: /わけ([らるれろてよ、])/ + expected: 分け$1 + - pattern: /聞え([なたまれる])/ + expected: 聞こえ$1 + - pattern: /変([らりるれろっ])/ + expected: 変わ$1 + - pattern: /捕え([なたまれる])/ + expected: 捕らえ$1 + - pattern: /暮([さしすせそ])/ + expected: 暮ら$1 + - pattern: /([^鮮])明か/ + expected: $1明らか + - pattern: もどって + expected: 戻って + - pattern: + - 返り値 + - 返値 + expected: 戻り値 + - pattern: /勇し([かくい])/ + expected: 勇まし$1 + - pattern: /揺([がぎぐげご])/ + expected: 揺ら$1 + - pattern: /頼し([かくい])/ + expected: 頼もし$1 + - pattern: /冷([さしすせそ])/ + expected: 冷や$1 + - pattern: /連([らりるれろっ])|つらな([らりるれろっ])/ + expected: 連な$1 + - pattern: はさまざま + expected: はさまざま + - pattern: /はさ([まみむめもん])/ + expected: 挟$1 + - pattern: おそれ + expected: 恐れ + - pattern: /([^っ])さきに/ + expected: $1先に + - pattern: 自分自信 + expected: 自分自身 + - pattern: /満([さしすせそっ])|みた([さしすせそっ])/ + expected: 満た$1 + - pattern: /([をがに])生か/ + expected: $1活か + - pattern: /いとな([まみむめも])/ + expected: 営$1 + - pattern: /かかげ([たてらる])/ + expected: 掲げ$1 + - pattern: /がた([ちつっ])/ + expected: が経$1 + - pattern: /うめ([たつて])/ + expected: 埋め$1 + - pattern: + - おすすめ + - お薦め + - お奨め + - オススメ + expected: お勧め + - pattern: + - てがけ + - 手掛け + expected: 手がけ + - pattern: みずから + expected: 自ら + - pattern: /いた([るれろ])/ + expected: 至$1 + - pattern: そなえ + expected: 備え + - pattern: /まね([いかきくけこ])/ + expected: 招$1 + - pattern: 子供 + expected: 子ども + - pattern: 塔載 + expected: 搭載 + - pattern: 一番最初 + expected: 一番初め + - pattern: 貼付け + expected: 貼り付け + - pattern: 必用 + expected: 必要 + - pattern: /あたえ([たつてらりるれろっ])/ + expected: 与え$1 + - pattern: /すす([まみむめもん])/ + expected: 進$1 + - pattern: 焦点をあて + expected: 焦点を当て + - pattern: /おも([いうえおわっ])/ + expected: 思$1 + - pattern: /おも([なにとだで])/ + expected: 主$1 + - pattern: 高値の花 + expected: 高嶺の花 + - pattern: たいする + expected: 対する + - pattern: たいし + expected: 対し + - pattern: + - たずさわ + - たづさわ + expected: 携わ + - pattern: 訪ず + expected: 訪 + - pattern: 短か + expected: 短 + - pattern: わたし + expected: 私 + - pattern: /くわし([いく])/ + expected: 詳し$1 + - pattern: 陽の目 + expected: 日の目 + - pattern: 保障 + expected: 保証 + - pattern: 移譲 + expected: 委譲 + - pattern: 稼動 + expected: 稼働 + - pattern: 芋蔓 + expected: 芋づる + - pattern: あいだ + expected: 間 + - pattern: /しめ([さしすせそ])/ + expected: 示$1 + - pattern: /のべ([^たてなるまよ、])/ + expected: 延べ$1 + - pattern: /あつま([らりるれろ])/ + expected: 集ま$1 + - pattern: /あきらめ([たてなるまよ、])/ + expected: 諦め$1 + - pattern: 成立ち + expected: 成り立ち + - pattern: + - たどりつ + - 辿り着 + - 辿りつ + expected: たどり着 + - pattern: 安定板 + expected: 安定版 + - pattern: /かえ([さしすせそっ])/ + expected: 返$1 + - pattern: /さが([さしすせそっ])/ + expected: 探$1 + - pattern: 見様見真似 + expected: 見よう見まね + - pattern: 買物 + expected: 買い物 + - pattern: /ほどこ([さしすせそ])/ + expected: 施$1 + - pattern: /使いき([らりるれろっ])/ + expected: 使い切$1 + - pattern: /逃が([さしすせそれ])/ + expected: 逃$1 + - pattern: /濡れ手[でに][泡粟]/ + expected: 濡れ手で粟 + - pattern: /もど([^ち])/ + expected: 戻$1 + - pattern: 携帯キャリア + expected: 携帯電話キャリア + - pattern: + - 代金引き換え + - 代引 + - 代引き + expected: 代金引換 + - pattern: 割引 + expected: 割り引き + - pattern: + - ひとめ + - ひと目 + expected: 一目 + - pattern: 復号化 + expected: 復号 + - pattern: 手間取 + expected: 手間ど + - pattern: 中味 + expected: 中身 + - pattern: /則([らりるれろっ])/ + expected: のっと$1 + - pattern: 洗錬 + expected: 洗練 + - pattern: 基板 + expected: 基盤 + - pattern: ふれ + expected: 触れ + - pattern: /あお([がぎぐげごっ])/ + expected: 仰$1 + - pattern: + - 独り事 + - ひとり事 + - ひとりごと + expected: 独り言 + - pattern: 本校 + expected: 本稿 + - pattern: /つぎ([にの])/ + expected: 次$1 + - pattern: 見きれ + expected: 見切れ + - pattern: /見做([さしすせそ])/ + expected: みな$1 + - pattern: 無駄使い + expected: 無駄遣い + - pattern: 肩をなでおろ + expected: 胸をなでおろ + - pattern: /陥い([らりるれろっ])|おちい([らりるれろっ])/ + expected: 陥$1 + - pattern: /かさ([なね])/ + expected: 重$1 + - pattern: 情報原 + expected: 情報源 + - pattern: 過度期 + expected: 過渡期 + - pattern: 流れを組 + expected: 流れをく + - pattern: + - 無駄使い + - ムダ遣い + - ムダ使い + - 無駄づかい + - 無駄ずかいい + - ムダづかい + - ムダずかい + expected: 無駄遣い + - pattern: 発火 + expected: 発行 + - pattern: + - 至難のわざ + - 至難の技 + expected: 至難の業 + - pattern: /こわ([さしすせそれ])/ + expected: 壊$1 + - pattern: /かか([え])/ + expected: 抱$1 + - pattern: まわり + expected: 周り + - pattern: こころ + expected: 心 + - pattern: じゃま + expected: 邪魔 + - pattern: ひんぱん + expected: 頻繁 + - expected: (株) + pattern: /株式会社/ + - expected: (有) + pattern: /有限会社/ + - expected: (社) + pattern: /社団法人/ + - expected: (合) + pattern: /合名会社/ + - expected: (特) + pattern: /特殊法人/ + - expected: (財) + pattern: /財団法人/ + - expected: (学) + pattern: /学校法人/ + - expected: (監) + pattern: /監査法人/ + - expected: (資) + pattern: /合資会社/ + - expected: (協) + pattern: /協力会社/ + - expected: (同) + pattern: /合同会社/ + - expected: (独) + pattern: /独立行政法人/ + - expected: (医) + pattern: /医療法人/ + - expected: (宗) + pattern: /宗教法人/ + - expected: オーバー$1 + pattern: /オーバ([^ー])/ + specs: + - from: オーバ気味 + to: オーバー気味 + - expected: スーパー$1 + pattern: /スーパ([^ー])/ + - expected: ハイパー$1 + pattern: /ハイパ([^ーフ])/ + specs: + - from: ハイパバイザ + to: ハイパーバイザ + - from: ハイパフォーマンス + to: ハイパフォーマンス + - expected: Cookie + pattern: クッキー + - expected: Cookie + - expected: WebSocket + pattern: /\bWeb Socket\b/ + - expected: $1Web$2 + pattern: /(?:([^/])ウェブ)|(?:ウェブ([^/\+]))/ + - expected: $1Web$2 + pattern: /(?:([^/])\bWEB)|(?:WEB\b([^/\+]))/ + - expected: $1Web$2 + pattern: /(?:([^/])ウェッブ)|(?:ウェッブ([^/\+]))/ + - expected: O/Rマッ + pattern: /ORマッ|O-Rマッ/ + - expected: O/Rマッパ + pattern: /O\/Rマッパー|\bORM\b/ + - expected: アイデア + pattern: /アィディア|アイディア|アィディア|アィデア/ + - expected: アクワイアラ + pattern: /アクワイアラー/ + - expected: アスタリスク + pattern: /アステリスク/ + - expected: アーキテクチャ + pattern: /アーキテクチャー|アーキティクチャ/ + - expected: アクティビティ + pattern: /\bActivity\b|アクティビティー/ + - expected: アダプタ + pattern: /アダプター/ + - expected: アドバイス + pattern: /アドヴァイス/ + - expected: アノテーション + pattern: /アノーテーション/ + - expected: アプレット + pattern: /\bApplet\b/ + - expected: アプリケーション + pattern: /アプリ(?!ケ)/ + specs: + - from: アプリケーション + to: アプリケーション + - from: アプリ + to: アプリケーション + - expected: アンダースコア + pattern: /アンダーバー/ + - expected: イシュア + pattern: /イシュアー/ + - expected: イディオム + pattern: /イデオム/ + - expected: インジケータ + pattern: /インジケーター|インジゲーター|インジゲータ/ + - expected: インスタンス + pattern: /インスンタンス/ + - expected: インストーラ + pattern: /インストーラー/ + - expected: インスパイア + pattern: /インスパイヤ/ + - expected: インスペクタ + pattern: /インスペクター/ + - expected: インタフェース + pattern: /インタフェイス|インターフェイス|インターフェース|インターフェィス/ + - expected: インタプリタ + pattern: /インタープリタ|インタプリター|インタープリター/ + - expected: インデックス + pattern: /インデクス/ + - expected: インテント + pattern: /\bIntent\b/ + - expected: ウィジェット + pattern: /\bWidget\b|\bwidget\b/ + - expected: ウィルス + pattern: /ウイルス/ + - expected: ウィンドウ + pattern: /ウインドウ/ + - expected: ウェア + pattern: ウエア + spec: + - from: ハードウエア + to: ハードウェア + - expected: エディタ + pattern: /エディター/ + - expected: エミッタ + pattern: /エミッター/ + - expected: エンコーダ + pattern: /エンコーダー/ + - expected: デコーダ + pattern: /デコーダー/ + - expected: エミュレータ + pattern: /エミュレーター/ + - expected: エンティティ + pattern: + - Entity + - entity + - エンティティー + - expected: エントリ + pattern: /エントリー/ + - expected: オブザーバ + pattern: /オブザーバー|オブサーバー|オブサーバ/ + - expected: オプション + pattern: /オブション/ + - expected: オン/オフ + pattern: /ON\/OFF|ON/OFF|オン・オフ|オン\/オフ/ + - expected: カウンタ + pattern: /カウンター/ + - expected: カバレッジ + pattern: /カバリッジ|カバレージ/ + - expected: ガベージ + pattern: /ガベージ・|ガーベジ|ガーベジ・|ガーベージ|ガーベージ・|ガーベッジ|ガーベッジ・|ガベッジ/ + - expected: カテゴリ + pattern: /カテゴリー/ + - expected: カラムナ + pattern: /カラムナー/ + - expected: カンマ + pattern: /コンマ/ + - expected: キャラクタ + pattern: /キャラクター/ + - expected: キャッシュ + pattern: + - /cache(?![^a-zA-Z\-])/ + - /Cache(?![^a-zA-Z\-])/ + - expected: クエリ文字列 + pattern: + - QueryString + - Query String + - クエリストリング + - クエリーストリング + - expected: クエリ + pattern: /クエリー/ + - expected: クオート + pattern: /クォート|クオーテーション|クォーテーション/ + - expected: クオリティ + pattern: /クオリティー|クォリティ|クォリティー/ + - expected: クライアント/サーバ + pattern: + - クライアント/サーバー + - クライアント/サーバ + - クライアント・サーバー + - クライアント・サーバ + - クライアントサーバー + - クライアントサーバ + specs: + - from: クライアント/サーバー + to: クライアント/サーバ + - expected: クラスタ + pattern: /クラスター/ + - expected: グラウンド + pattern: /グランド/ + - expected: グリッド + pattern: /\bGrid\b/ + - expected: クロージャ + pattern: /クロージャー/ + - expected: クローラ + pattern: /クローラー/ + - expected: ゲッタ + pattern: /ゲッター/ + - expected: コピー&ペースト + pattern: + - コピー&ペースト + - コピペ + - コピーアンドペースト + - コピーペースト + - expected: コミュニ + pattern: /コミニュ/ + - expected: アンコメント + pattern: /コメントイン/ + - expected: コンストラクタ + pattern: /コンストラクター/ + - expected: コンテキスト + pattern: /コンテクスト/ + - expected: コンテントプロバイダ + pattern: /\bContent provider\b/ + - expected: コンテナ + pattern: /コンテナー/ + - expected: コンピュータ + pattern: /コンピューター/ + - expected: コンポーネント + pattern: /コンポネント/ + - expected: コントローラ + pattern: /コントローラー/ + - expected: サーバ + pattern: /サーバー/ + - expected: サーブレット + pattern: + - Servlet + - SERVLET + - expected: サーブレット/JSP + pattern: + - サーブレット/JSP + - サーブレット&JSP + - サーブレット&JSP + - expected: サンフランシスコ + pattern: /\bSan Francisco\b/ + - expected: シェア + pattern: /シェア率/ + - expected: ジェスチャ + pattern: /ジェスチャー/ + - expected: ジェネレータ + pattern: /ジェネレーター/ + - expected: ジェネレーティブ + pattern: /ジェネレイティブ/ + - expected: ジョブズ + pattern: /ジョブス/ + - expected: ジオタグ + pattern: /\bGeotag\b|\bgeotag\b/ + - expected: シグネチャ + pattern: /シグネチャー/ + - expected: シミュレー + pattern: /シュミレー/ + - expected: シンクロナイザ + pattern: /シンクロナイザー/ + - expected: スカラ + pattern: /スカラー/ + - expected: スクラム + pattern: /\bScrum\b/ + - expected: スタンドアローン + pattern: /スタンドアロン/ + - expected: ストアド + pattern: /ストアード/ + - expected: ストレージ + pattern: /ストレッジ|ストレジ/ + - expected: スマートフォン + pattern: /スマフォ|スマホ/ + - expected: スムーズ + pattern: /スムース/ + - expected: セキュリティ + pattern: /セキュリティー/ + - expected: セッション + pattern: /\bsession\b|\bSession\b/ + - expected: セッタ + pattern: /セッター/ + - expected: セレクタ + pattern: /セレクター/ + - expected: センサ + pattern: /センサー/ + - expected: ソフトウェア + pattern: /(日経)?ソフトウエア/ + regexpMustEmpty: $1 + specs: + - from: 広義のソフトウエア + to: 広義のソフトウェア + - from: 日経ソフトウエア + to: 日経ソフトウエア + - expected: ソフトバンク + pattern: /\bSoftBank\b/ + - expected: ダイアグラム + pattern: /ダイヤグラム/ + - expected: タイムスタンプ + pattern: + - timestamp + - Timestamp + - expected: ツイート + pattern: + - tweet + - Tweet + - ツィート + - expected: ツリーオブジェクト + pattern: /ツリー・オブジェクト/ + - expected: ツリーエントリ + pattern: /ツリー・エントリ/ + - expected: テーブル + pattern: + - table + - Table + - expected: データサービス + pattern: /\bData Services\b/ + - expected: データ + pattern: /データー/ + - expected: データセンター$1 + pattern: /データセンタ([^ー])/ + - expected: データ同期 + pattern: /\bData Sync\b/ + - expected: チェイン + pattern: /チェーン/ + - expected: ディザスタリカバリ + pattern: /ディザスタ・リカバリ/ + - expected: ディスク + pattern: /\bDisk\b/ + - expected: ディスパッチャ + pattern: /ディスパッチャー/ + - expected: ディスプレイ + pattern: /ディスプレー/ + - expected: ディレクトリ + pattern: /ディレクトリー/ + - expected: テクノロジ + pattern: /テクノロジー/ + - expected: デジタルカメラ + pattern: /デジカメ/ + - expected: デバッグ + pattern: /デバック/ + - expected: デバッガ + pattern: /デバッガー/ + - expected: デフォルト + pattern: /既定|ディフォルト|デフォールト/ + - expected: デプロイ + pattern: /デプロイメント/ + - expected: デリバリ + pattern: /デリバリー/ + - expected: ドキュメント + pattern: /ドキュメンテーション/ + - expected: ドコモ + pattern: /\bdocomo\b|\bDocomo\b|\bDoCoMo\b/ + - expected: ドライバ + pattern: /ドライバー/ + - expected: ドラッグ&ドロップ + pattern: /ドラッグ・アンド・ドロップ|ドラッグアンドドロップ|ドラッグ&ドロップ|ドラッグ & ドロップ/ + - expected: トリガ + pattern: /トリガー/ + - expected: ニューヨーク + pattern: /\bNew York\b/ + - expected: バイナリ + pattern: /バイナリー/ + - expected: ハイパーリンク + pattern: /ハイパー・リンク/ + - expected: ハイパーバイザ + pattern: /ハイパーバイザー/ + - expected: パーサ + pattern: /パーサー|パーザー|パーザ/ + - expected: パーマリンク + pattern: /\bPermalink\b|\bpermalink\b|\bpermanent link\b|パーマネントリンク/ + - expected: バッファ + pattern: /バッファー/ + - expected: パス + pattern: /\bpath\b|\bPath\b/ + - expected: パターン + pattern: /パタン/ + - expected: ハッシュ + pattern: /\bhash\b|\bHash\b/ + - expected: バラ + pattern: /薔薇/ + - expected: バラエティ + pattern: /バラエティー/ + - expected: パラメータ + pattern: /パラメタ|パラメーター/ + - expected: バランサ + pattern: /バランサー/ + - expected: ハンドラ + pattern: /ハンドラー/ + - expected: ビューア + pattern: /ビューアー|ビューワー|ビューワ/ + - expected: ビルダ + pattern: /ビルダー/ + - expected: ビルトイン + pattern: /ビルドイン/ + - expected: ヒット率 + pattern: /hit率/ + - expected: ファイラ + pattern: /ファイラー/ + - expected: ファクトリ + pattern: /ファクトリー/ + - expected: フィーチャーフォン + pattern: /フィーチャフォン/ + - expected: ブラウザ + pattern: + - /Webブラウザー?/ + - /WEBブラウザー?/ + - ブラウザ + - expected: プライマリ + pattern: /プライマリー/ + - expected: プラットフォーム + pattern: /プラットホーム/ + - expected: ブレーク + pattern: /ブレイク/ + - expected: プレーン + pattern: プレイン(?!グ) + specs: + - from: プレイング + to: プレイング + - expected: ブローカ$1 + pattern: /ブローカー([^ー])/ + - expected: プロパティ + pattern: /プロパティー/ + - expected: ヘビー + pattern: /ヘヴィ/ + - expected: ファイアウォール + pattern: /ファイアーウォール|ファイヤーウォール|ファイヤウォール|ファイヤーウオール/ + - expected: ファイバチャネル + pattern: /ファイバチャンネル|ファイバチャネル|ファイバーチャンネル/ + - expected: フィルタ + pattern: /フィルター/ + - expected: フィクスチャ + pattern: /フィクスチャー/ + - expected: フェイルオーバー + pattern: /フェイルオーバ|フェールオーバー|フェールオーバ/ + - expected: フェーズ + pattern: /フェイズ/ + - expected: フッタ + pattern: /フッター|フッダ/ + - expected: プロキシ + pattern: /プロクシ|プロクシー|プロキシー|\bProxy\b|\bproxy\b/ + - expected: ブログ + pattern: /(?![^/])(?:blog|Blog)(?![^/])/ + specs: + - from: http://blog.example.com/ + to: http://blog.example.com/ + - from: http://example.com/foo-blog/ + to: http://example.com/foo-blog/ + - expected: プロシージャ + pattern: /プロシージャー/ + - expected: ブロードキャストレシーバ + pattern: /\bBroadcast receiver\b/ + - expected: プロバイダ + pattern: /プロバイダー/ + - expected: ペアプログラミング$1 + pattern: /ペアプロ([^グ])/ + - expected: ベンダー + pattern: /ベンダ([^ー])/ + - expected: ヘッダ + pattern: /ヘッダー|ヘッタ|ヘッター/ + - expected: ベクタ + pattern: /ベクター/ + - expected: ページャ + pattern: /ページャー/ + - expected: ポインタ + pattern: /ポインター/ + - expected: ポッドキャスト + pattern: /\bPodCast\b|\bpodcast\b|\bPodcast\b/ + - expected: ポリモフィズム + pattern: /ポリモルフィズム|ポリモーフィズム|ポルモルフィズム/ + - expected: $1マイクロ秒 + pattern: /([0-9])μs/ + - expected: $1ミリ秒 + pattern: /([0-9])\bms\b/ + - expected: マイナビ + pattern: /毎日コミュニケーションズ/ + - expected: マトリックス + pattern: /マトリクス/ + - expected: マッパ + pattern: /マッピングツール|マッパー/ + - expected: マネジメント + pattern: /マネージメント/ + - expected: メーカー + pattern: /メーカ/ + - expected: メーリングリスト + pattern: /\bML\b(?!系)/ + specs: + - from: SML# + to: SML# + - from: ML系言語 + to: ML系言語 + - from: MLなど + to: メーリングリストなど + - expected: メタファ + pattern: /メタファー/ + - expected: メモリ + pattern: /メモリー/ + - expected: メンテナンス + pattern: /メインテナンス/ + - expected: メンテナンス$1 + pattern: /メンテ([^ナ])/ + - expected: モジュール + pattern: /\bmodule\b|\bModule\b/ + - expected: モータ$1 + pattern: /モーター([^ー])/ + - expected: レジューム + pattern: /リジューム/ + - expected: レスポンシブWebデザイン + pattern: /\bResponsive Web Design\b/ + - expected: ユーティリティ + pattern: /ユーティリティー/ + - expected: ユニットテスト + pattern: + - Unitテスト + # 単体テストはケースバイケース + - 単体テスト + - expected: ライブラリ + pattern: /ライブラリー/ + - expected: ラスタ + pattern: /ラスター/ + - expected: ラッパ + pattern: /ラッパー/ + - expected: リガチャ + pattern: /リガチャー/ + - expected: リグレッション + pattern: + - デグレード + # デグレは英語圏では使わない by Jenkins川口さん + - デグレ + - expected: リスナ + pattern: /リスナー/ + - expected: リバースプロキシ + pattern: /\bReverse Proxy\b/ + - expected: リファラ + pattern: /リファラー/ + - expected: リポジトリ + pattern: /リポジトリー|レポジトリ|レポジトリー/ + - expected: ルータ + pattern: /ルーター/ + - expected: レイヤ + pattern: /(プ)?レイヤー/ + regexpMustEmpty: $1 + specs: + - from: レイヤー + to: レイヤ + - from: プレイヤー + to: プレイヤー + - expected: レジスタ + pattern: /レジスター/ + - expected: レジストリ + pattern: /レジストリー/ + - expected: レイテンシ + pattern: /レイテンシー/ + - expected: レコメンド + pattern: /リコメンド/ + - expected: ロータ$1 + pattern: /ローター([^ー])/ + - expected: ローダ + pattern: /ローダー/ + - expected: ロングテール + pattern: /ロングテイル/ + - expected: ワーカ + pattern: /ワーカー|\bworker\b/ + - expected: ワンタイムURL + pattern: /\bOnetime URL\b/ + - expected: クアッドコアCPU + pattern: /\bQuad Core CPU\b/ + - expected: クアッドコア + pattern: /クァッドコア|\bQuad Core\b|クァッドCore|Quadコア/ + - expected: デュアルコアCPU + pattern: /\bDual Core CPU\b/ + - expected: デュアルコア + pattern: /\bDual Core\b|デュアルCore|Dualコア/ + - expected: マスタ/スレーブ + pattern: /マスタ・スレーブ|マスタスレーブ/ + - expected: バックアップ$1 + pattern: /\bBackup\b([^.])/ + - expected: スレーブ$1 + pattern: /スレイブ([^.])|\bSlave\b([^.])/ + - expected: 記述子 + pattern: /ディスクリプタ/ + - expected: 属性 + pattern: /アトリビュート/ + - expected: 要素 + pattern: /エレメント/ + - expected: アニメーター + pattern: /アニメータ/ + - expected: キャラクタ$1 + pattern: /キャラクター([^ー])/ + - expected: コミッター$1 + pattern: /コミッタ([^ー])/ + - expected: ユーザー$1 + pattern: /ユーザ([^ー])/ + - expected: ユーザビリティ + pattern: /ユーザービリティ/ + - expected: ディレクター + pattern: /ディレクタ(?!ー)/ + specs: + - from: ディレクター + to: ディレクター + - expected: デザイナー$1 + pattern: /デザイナ([^ー])/ + - expected: デベロッパー$1 + pattern: /デベロッパ([^ー])|ディベロッパー([^ー])|ディベロッパ([^ー])|ディヴェロッパ([^ー])/ + - expected: ファシリテーター$1 + pattern: /ファシリテータ([^ー])/ + - expected: プレイヤー + pattern: /プレーヤ/ + - expected: プレーヤ + pattern: /プレーヤー/ + - expected: プログラマー$1 + pattern: /プログラマ([^ー])/ + - expected: プログラマブル + pattern: /プログラマーブル/ + - expected: プロデューサー$1 + pattern: /プロデューサ([^ー])/ + - expected: プランナー$1 + pattern: /プランナ([^ー])/ + - expected: $1マスタ$2 + pattern: /(?:([^ム])マスター)|(?:マスター([^.]))/ + - expected: $1マスタ$2 + pattern: /(?:([^ム])\bMaster)|(?:Master\b([^.]))/ + - expected: $1マスタ$2 + pattern: /(?:([^ム])\bmaster)|(?:master\b([^.]))/ + - expected: マネージャー$1 + pattern: /マネージャ([^ー])/ + - expected: メンテナー$1 + pattern: /メンテナ([^ー])/ + - expected: メンバー + # 人間の意味では「メンバー」、変数などは「メンバ」 + pattern: /メンバ(?!ー)/ + - expected: リーダー + # 人間の意味では「リーダー」、readerの意味では「リーダ」 + pattern: /リーダ(?!ー)/ + - expected: レビュアー$1 + pattern: /レビュア([^ー])|レビュワー([^ー])|レビュワ([^ー])|レビューアー([^ー])|レビューア([^ー])|レビューワー([^ー])|レビューワ([^ー])/ + - expected: Action Cable + pattern: /\bActionCable\b/ + - expected: Action Controller + pattern: /\bActionController\b/ + - expected: Action Mailer + pattern: /\bActionMailer\b/ + - expected: Action Pack + pattern: /\bActionPack\b/ + - expected: ActionScript + pattern: /\bAction Script\b|アクションスクリプト/ + - expected: ActionScript $1 + pattern: /ActionScript([0-9])/ + - expected: ActionScript Virtual Machine + pattern: /\bActionScriptVirtualMachine\b|\bActionScript VirtualMachine\b/ + - expected: Action View + pattern: /\bActionView\b/ + - expected: Active Job + pattern: /\bActiveJob\b/ + - expected: Active Model + pattern: /\bActiveModel\b/ + - expected: Active Record + pattern: /\bActiveRecord\b/ + - expected: Active Resource + pattern: /\bActiveResource\b/ + - expected: Active Support + pattern: /\bActiveSupport\b/ + - expected: ApplicationRecord Application Record + pattern: /,,RE,/ + - expected: ApplicationController + pattern: /\bApplication Controller\b/ + - expected: ApplicationRecord + pattern: /\bApplication Record\b/ + - expected: ApplicationJob + pattern: /\bApplication Job\b/ + - expected: Adobe AIR + pattern: /\bAdobe AIR\b/i + - expected: Akamai + pattern: /\bAkamai\b/i + - expected: Alias + pattern: /\bAilias\b/ + - expected: Amazon Web Services$1 + pattern: /\bAmazon Web Service\b([^s])/ + - expected: American Express(Amex) + pattern: /アメリカン・エキスプレス|アメックス|アメリカンエクスプレス/ + - expected: Android + pattern: /アンドロイド|\bandroid\b/ + - expected: Ant + pattern: /\bAnt\b/i + - expected: Apache + pattern: /\bapache\b|\bApatch\b|\bapatch\b/ + - expected: Apache $1$2 + pattern: /Apache([0-9])|Apatch([0-9])/ + - expected: ApiGen + pattern: /\bApiGen\b/i + - expected: APNs + pattern: /\bAPNs\b/i + - expected: App Store + pattern: /\bAppStore\b/ + - expected: Apple Watch + pattern: /\bApple Watch\b/i + - expected: Apple + pattern: /アップル/ + - expected: Apple + pattern: /\bApple\b/i + - expected: APT + pattern: /\bAPT\b/i + - expected: ASP.NET + pattern: /ASP \.NET/ + - expected: Backbone.js + pattern: /\bBackbone.js\b/i + - expected: Babel + pattern: /\bBabel\b/i + - expected: bash + pattern: /\bBash\b/ + - expected: Bean + pattern: /ビーン/ + - expected: Bigtable + pattern: /\bBigTable\b|\bBig Table\b|\bBig table\b/ + - expected: Bitbucket + pattern: /\bBitbucket\b/i + - expected: Bitcoin + pattern: /\bBitcoin\b/i + - expected: BlackBerry + pattern: /\bBlackBerry\b/i + - expected: Blu-ray + pattern: /\bBlu-ray\b/i + - expected: Bonnie++ + pattern: /\bBonnie\+\+\b/i + - expected: $1bot + pattern: /([^ロ])ボット/ + - expected: bstract + pattern: /\bbsctract\b|\bbscract\b/ + - expected: Bundler + pattern: /\bBundler\b/i + - expected: CakePHP + pattern: /\bCakePHP\b/i + - expected: CakePHP $1 + pattern: /CakePHP([0-9])/ + - expected: Capistrano + pattern: /\bCapistorano\b|\bcapistrano\b/ + - expected: Capybara + pattern: /\bCapybara\b/i + - expected: CarrierWave + pattern: /\bCarrierWave\b/i + - expected: CentOS + pattern: /\bCent OS\b/ + - expected: Chef + pattern: /\bChef\b/i + - expected: Chrome Web Store + pattern: /Chromeウェブストア|Chrome Webストア/ + - expected: Chromium + pattern: /\bChronium\b/ + - expected: CloudFlare + pattern: /\bCloudFlare\b/i + - expected: Cloudinary + pattern: /\bCloudinary\b/i + - expected: Cygwin + pattern: /\bCygwin\b/i + - expected: Cobbler + pattern: /\bCobbler\b/i + - expected: CocoaPods + pattern: /\bCocoaPods\b/i + - expected: Coca-Cola + pattern: /\bCoca Cola\b/ + - expected: Composer + pattern: /\bComposer\b/i + - expected: Coveralls + pattern: /\bCoveralls\b/i + - expected: CSSスプライト + pattern: /CSS-Sprite|\bCSS Sprite\b/ + - expected: DBFlute + pattern: /\bDBFlute\b/i + - expected: Debian GNU/Linux + pattern: /\bDebian\b|Debian\/GNU Linux/ + - expected: $1DeNA$2 + pattern: /(?:([^/.])ディー・エヌ・エー)|(?:ディー・エヌ・エー([^/.]))/ + - expected: $1DeNA$2 + pattern: /(?:([^/.])ディーエヌエー)|(?:ディーエヌエー([^/.]))/ + - expected: $1DeNA$2 + pattern: /(?:([^/.])\bDENA)|(?:DENA\b([^/.]))/ + - expected: Diners Club + pattern: /ダイナースクラブ|ダイナース/ + - expected: Docker Hub + pattern: /\bDockerHub\b/ + - expected: Dreamweaver + pattern: /\bDreamWeaver\b/ + - expected: easy_install + pattern: /\beasy_install\b/i + - expected: Eclipse + pattern: /\bEclipse\b/i + - expected: ECMAScript + pattern: /\bECMA Script\b/ + - expected: EJB-JARファイル + pattern: /\bEJB-JARファイル\b/i + - expected: Elisp + pattern: /\bElisp\b/i + - expected: Lisp + pattern: /\bLisp\b/i + - expected: Elixir + pattern: /\bElixr\b|\belixr\b/ + - expected: Emacs + pattern: /\bEmacs\b/i + - expected: Emacs $1 + pattern: /Emacs([0-9])/ + - expected: Emacs Lisp + pattern: /\bEmacs Lisp\b/i + - expected: ERB + pattern: /\bERB\b/i + - expected: Ethernet + pattern: /イーサネット/ + - expected: EventMachine + pattern: /\bEventMachine\b/i + - expected: Excel + pattern: /エクセル/ + - expected: Express + pattern: /\bexpress\b/ + - expected: Fabric + pattern: /\bFablic\b/ + - expected: $1Facebook$2 + pattern: /(?:([^/.])\bfacebook)|(?:facebook\b([^/.]))/ + - expected: Firebug + pattern: /\bFirebug\b/i + - expected: Firefox + pattern: /\bFireFox\b|\bFire Fox\b|ファイアーフォックス|ファイヤーフォックス/ + - expected: Fitbit + pattern: /\bFitbit\b/i + - expected: Flash + pattern: /フラッシュ/ + - expected: Flash Lite + pattern: /\bFlashLite\b/ + - expected: Flash Player + pattern: /Flashプレイヤー|Flash プレイヤー|Flashプレーヤ|Flash プレーヤ|Flashプレーヤー|Flash プレーヤー|\bFlashPlayer\b/ + - expected: Flashプラットフォーム + pattern: /\bFlash Platform\b/ + - expected: Flex Builder + pattern: /\bFlexBuilder\b/ + - expected: Fluentd + pattern: /\bFluentd\b/i + - expected: FORTRAN + pattern: /\bFORTRAN\b/i + - expected: Gears + pattern: /\bGoogleGears\b|\bGoogle Gears\b/ + - expected: Gemnasium + pattern: /\bGemnasium\b/i + - expected: GHCi + pattern: /\bGHCi\b/i + - expected: GitHub Pages + pattern: /\bGitHub Pages\b/i + - expected: GitHub Enterprise + pattern: /GH:E|\bGHE\b|GitHub:E/ + - expected: $1GitHub$2 + pattern: /(?:([^/.])\bgithub)|(?:github\b([^/.]))/ + - expected: $1GitHub$2 + pattern: /(?:([^/.])\bGithub)|(?:Github\b([^/.]))/ + - expected: Git + pattern: /\bGit\b/i + - expected: Gmail + pattern: /\bGmail\b/i + - expected: Google Gadget + pattern: /Googleガジェット/ + - expected: Google Maps + pattern: /\bGoogle Map\b|\bGoogleMaps\b|\bGoogleMap\b|Googleマップ/ + - expected: Gradle Wrapper + pattern: /\bGradleWrapper\b/ + - expected: Greasemonkey + pattern: /\bGreaseMonkey\b|\bGrease monkey\b|\bGrease Monkey\b/ + - expected: Grunt + pattern: /\bGrunt\b/i + - expected: gulp + pattern: /\bgulp\b/i + - expected: gzip + pattern: /\bgzip\b/i + - expected: hdparm + pattern: /\bhdparm\b/i + - expected: HDD + pattern: /ハードディスク/ + - expected: Heartbeat + pattern: /\bHeartbeat\b/i + - expected: Heroku + pattern: /\bHeroku\b/i + - expected: Homebrew + pattern: /\bHomebrew\b/i + - expected: HTML5 + pattern: /HTML 5/ + - expected: HTML $1 + pattern: /HTML([0-4])/ + - expected: HTTP/$1 + pattern: /HTTP([0-9])/ + - expected: HTTP/$1 + pattern: /HTTP ([0-9])/ + - expected: Hubot + pattern: /\bHubot\b/i + - expected: I/O + pattern: /\bIO\b/ + - expected: ImageMagick + pattern: /\bImageMagick\b/i + - expected: ImageProcessing + pattern: /\bImageProcessing\b/i + - expected: Internet Explorer $1$2 + pattern: /Internet Explorer([0-9])|IE([0-9])/ + - expected: Internet Explorer + pattern: /\bIE\b/ + - expected: inode + pattern: /iノード/ + - expected: iOS SDK + pattern: /\biOS SDK\b/i + - expected: iOS $1 + pattern: /iOS([0-9])/ + - expected: Iperf + pattern: /\bIperf\b/i + - expected: iPhone + pattern: /アイフォン|\biphone\b|\bIPHONE\b/ + - expected: iPhone 5s + pattern: /\biPhone 5s\b/i + - expected: iPhone 4S + pattern: /\biPhone 4S\b/i + - expected: IRKit + pattern: /\bIRKit\b/i + - expected: ISO + pattern: /ISO-/ + - expected: ISO $1 + pattern: /ISO([0-9])/ + - expected: iTunes Card + pattern: /\biTunes Card\b/i + - expected: iPad + pattern: /\biPad\b/i + - expected: iPhone + pattern: /\biPhone\b/i + - expected: Jade + pattern: /\bJade\b/i + - expected: JARファイル + pattern: /\bJARファイル\b/i + - expected: Java $1 + pattern: /Java([0-9])/ + - expected: Java 3D + pattern: /Java3D/ + - expected: Java EE + pattern: /\bJavaEE\b/ + - expected: Java EE $1 + pattern: /JavaEE([0-9])/ + - expected: Java EE $1 + pattern: /Java EE([0-9])/ + - expected: Java SE + pattern: /\bJavaSE\b/ + - expected: Java SE $1 + pattern: /JavaSE([0-9])/ + - expected: Java SE $1 + pattern: /Java SE([0-9])/ + - expected: JavaBeans + pattern: /\bJavaBean\b|\bJava Bean\b|\bJava Beans\b/ + - expected: Javadoc + pattern: /\bJavadoc\b/i + - expected: JavaScript + pattern: /\bJava Script\b|\bJavascript\b|\bjavascript\b/ + - expected: JavaScript $1 + pattern: /JavaScript([0-9])/ + - expected: JavaServer Faces + pattern: /\bJava Server Faces\b/ + - expected: JavaServer Pages + pattern: /\bJava Server Pages\b/ + - expected: Jenkins + pattern: /\bJenkins\b/i + - expected: JDBC-Redis + pattern: /\bJDBC-Redis\b/i + - expected: JDK $1 + pattern: /JDK([0-9])/ + - expected: Jedis + pattern: /\bJedis\b/i + - expected: JPEG + pattern: /\bJPEG\b/i + - expected: JSF $1 + pattern: /JSF([0-9])/ + - expected: JSP $1 + pattern: /JSP([0-9])/ + - expected: JVM + pattern: /\bJava VM\b|\bJavaVM\b/ + - expected: Keepalived + pattern: /\bKeepalived\b/i + - expected: key-value + pattern: /Key-Value|Key\/Value|キーバリュー|キー・バリュー|キー/バリュー/ + - expected: Kickstarter + pattern: /\bKickstarter\b/i + - expected: Kickstart + pattern: /\bKickstart\b/i + - expected: KitchenSink + pattern: /\bKitchenSink\b/i + - expected: knockout.js + pattern: /\bknockout.js\b/i + - expected: Kotlin $1 + pattern: /Kotlin([0-9])/ + - expected: Kotlin + pattern: /\bKoltin\b/ + - expected: Kyoto Cabinet + pattern: /\bKyotoCabinet\b/ + - expected: Kyoto Tycoon + pattern: /\bKyotoTycoon\b|Tokyo *Tycoon/ + - expected: LESS + pattern: /\bLESS\b/i + - expected: LINE + pattern: /\bLINE\b/i + - expected: LinkedIn + pattern: /\bLinked In\b/ + - expected: LL + pattern: /LL言語/ + - expected: macOS + pattern: /\bMacOS\b|\bMac OS\b|\bmac OS\b|\bOS X\b|\bOSX\b|\bMac OS X\b|\bMacOSX\b/ + - expected: OSS + pattern: /オープンソースソフトウェア/ + - expected: MacBook + pattern: /\bMac Book\b/ + - expected: MacPorts + pattern: /\bMacPorts\b/i + - expected: Maglica + pattern: /\bMaglica\b/i + - expected: Markdown + pattern: /\bMarkdown\b/i + - expected: MasterCard + pattern: /マスターカード/ + - expected: Maven + pattern: /\bMaven\b/i + - expected: MeCab + pattern: /\bMeCab\b/i + - expected: Mechanize + pattern: /\bMechanize\b/i + - expected: memcached + pattern: /\bmemcached\b/i + - expected: $1Microsoft$2 + pattern: /(?:([^/.])マイクロソフト)|(?:マイクロソフト([^/.]))/ + - expected: $1Microsoft$2 + pattern: /(?:([^/.])\bmicrosoft)|(?:microsoft\b([^/.]))/ + - expected: Migemo + pattern: /\bMigemo\b/i + - expected: MiniMagick + pattern: /\bMiniMagick\b/i + - expected: mixi + pattern: /\bmixi\b/i + - expected: MongoDB + pattern: /\bMongo DB\b/ + - expected: msysGit + pattern: /\bmsysGit\b/i + - expected: Munin + pattern: /\bMunin\b/i + - expected: MySQL + pattern: /\bMySQL\b/i + - expected: Nagios $1 + pattern: /Nagios([0-9])/ + - expected: Nagios + pattern: /\bNagios\b/i + - expected: Nokia + pattern: /\bNokia\b/i + - expected: nginx + pattern: /\bnginx\b/i + - expected: NGINX Plus + pattern: /\bNGINX Plus\b/i + - expected: Node.js + pattern: /\bNode.js\b/i + - expected: NVMe + pattern: /\bNVMe\b/i + - expected: OAuth + pattern: /\bOAuth\b/i + - expected: OmniAuth + pattern: /\bOmniAuth\b/i + - expected: OpenGL + pattern: /\bOpen GL\b/ + - expected: Opscode + pattern: /\bOpscode\b/i + - expected: OS + pattern: /オペレーティングシステム/ + - expected: Packagist + pattern: /\bPackagist\b/i + - expected: Paperclip + pattern: /\bPaperclip\b/i + - expected: PayPal + pattern: /\bPayPal\b/i + - expected: parallel + pattern: /\bpararllel\b/ + - expected: PC + pattern: /パソコン/ + - expected: PECL + pattern: /\bPECL\b/i + - expected: Pentium 4 + pattern: /\bPentium IV\b/ + - expected: Pentium II + pattern: /Pentium 2|Pentium2/ + - expected: Pentium III + pattern: /Pentium 3|Pentium3/ + - expected: Perl $1 + pattern: /Perl([0-9])/ + - expected: Perl + pattern: /\bPerl\b/i + - expected: Phan + pattern: /\bPhan\b/i + - expected: PhantomJS + pattern: /\bPhantomJS\b/i + - expected: Photoshop + pattern: /\bPhotoShop\b|\bphotoshop\b|フォトショップ/ + - expected: PHP $1 + pattern: /PHP([0-9])/ + - expected: PHPBrew + pattern: /\bPHPBrew\b/i + - expected: PHPDoc + pattern: /\bPHPDoc\b/i + - expected: PhpMetrics + pattern: /\bPhpMetrics\b/i + - expected: PhpStorm + pattern: /\bPhpStorm\b/i + - expected: PHPUnit + pattern: /\bPHPUnit\b/i + - expected: ping + pattern: /\bping\b/i + - expected: pip + pattern: /\bpip\b/i + - expected: pixiv + pattern: /\bpixiv\b/i + - expected: Playground + pattern: /\bPlaygroud\b/ + - expected: Polyfill + pattern: /ポリフィル/ + - expected: POPFile + pattern: /\bPOPFile\b/i + - expected: PostScript + pattern: /\bPostScript\b/i + - expected: PostgreSQL $1 + pattern: /PostgreSQL([0-9])/ + - expected: PowerPoint + pattern: /\bPower Point\b/ + - expected: Pull Request + pattern: /プルリクエスト|\bpull request\b|\bPull request\b/ + - expected: Python + pattern: /\bPython\b/i + - expected: Qiita:Team + pattern: "/Qiita Team|Qiita: Team/" + specs: + - from: "Qiita Team" + to: "Qiita:Team" + - from: "Qiita: Team" + to: "Qiita:Team" + - expected: Rack + pattern: /\bRack\b/i + - expected: RADIUS + pattern: /\bRADIUS\b/i + - expected: Rails $1 + pattern: /Rails([0-9])/ + - expected: Rake + pattern: /\bRake\b/i + - expected: Raspberry Pi + pattern: /\bRasbpberry Pi\b|\bRaspiberry Pi\b|\bRaspberryPi\b/ + - expected: Redmine + pattern: /\bRedmine\b/i + - expected: Red Hat + pattern: /\bRedHat\b|\bRedhat\b|\bredhat\b|\bRedHad\b|\bRedhad\b|\bredhad\b|レッドハット/ + - expected: Red Hat Enterprise Linux + pattern: /\bRHEL\b/ + - expected: Red Hat Linux + pattern: /\bRedHatLinux\b|\bRedHat Linux\b/ + - expected: Red Hat Linux $1 + pattern: /Red Hat Linux([0-9])|RedHatLinux([0-9])|RedHat Linux([0-9])/ + - expected: Redis + pattern: /\bRedis\b/i + - expected: Redshift + pattern: /\bRedshift\b/i + - expected: Refile + pattern: /\bRefile\b/i + - expected: RELAX NG + pattern: /\bRELAX NG\b/i + - expected: RFC $1 + pattern: /RFC([0-9])/ + - expected: Roda + pattern: /\bRoda\b/i + - expected: RPCサービス + pattern: /\bRPC Services\b/ + - expected: RRDtool + pattern: /\bRRDtool\b/i + - expected: RSpec + pattern: /\bRSpec\b/i + - expected: Ruby + pattern: /\bRuby\b/i + - expected: Ruby $1 + pattern: /Ruby([0-9])/ + - expected: Ruby on Rails + pattern: /\bRuby On Rails\b|\bRoR\b/ + - expected: RubyGems + pattern: /\bRubyGems\b/i + - expected: RubyGems + pattern: /\bRuby Gems\b/ + - expected: SAML $1 + pattern: /SAML([0-9])/ + - expected: satis + pattern: /\bsatis\b/i + - expected: SBクリエイティブ + pattern: /ソフトバンク クリエイティブ|ソフトバンククリエイティブ/ + - expected: Scheme + pattern: /\bScheme\b/i + - expected: Selenium WebDriver + pattern: /\bSelenium WebDriver\b/i + - expected: Selenium + pattern: /\bSelenium\b/i + - expected: Sequel + pattern: /\bSequel\b/i + - expected: Serverspec + pattern: /\bServerspec\b/i + - expected: Serverspec + pattern: /\bserver spec\b/ + - expected: Servlet $1 + pattern: /Servlet([0-9])/ + - expected: Shift_JIS + pattern: /Shift-JIS|SHIFT-JIS|SHIFT_JIS|\bShift JIS\b|Shift_JIS|シフトJIS/ + - expected: Shrine + pattern: /\bShrine\b/i + - expected: Shrpx + pattern: /\bShrpx\b/i + - expected: shebang + pattern: /シバン|シェバン|\bShebang\b/ + - expected: Silverlight + pattern: /\bSilverLight\b|\bSilver Light\b/ + - expected: SimpleTest + pattern: /\bSimpleTest\b/i + - expected: Sinatra + pattern: /\bSinatra\b/i + - expected: SkeedCast + pattern: /\bSkeedCast\b/i + - expected: $1Smalltalk$2 + pattern: /(?:([^/.])スモールトーク)|(?:スモールトーク([^/.]))/ + - expected: Smalltalk + pattern: /\bSmalltalk\b/i + - expected: Socket.IO + pattern: /\bSocket.IO\b/i + - expected: $1SourceForge + pattern: /([^/.])\bsourceforge\b/ + - expected: Sorryサーバ + pattern: /ソーリーサーバ/ + - expected: SPDY indicator + pattern: /\bSPDY indicator\b/i + - expected: Spdycat + pattern: /\bSpdycat\b/i + - expected: Spdyd + pattern: /\bSpdyd\b/i + - expected: Spdylay + pattern: /\bSpdylay\b/i + - expected: StudioPress + pattern: /\bStudioPress\b/i + - expected: SPDY/$1 + pattern: /SPDY([0-9])/ + - expected: SPDY/$1 + pattern: /SPDY ([0-9])/ + - expected: SpiderMonkey + pattern: /\bSpiderMonkey\b/i + - expected: SQL $1 + pattern: /SQL([0-9])/ + - expected: SQLite + pattern: /\bSQLite\b/i + - expected: Squid + pattern: /\bSquid\b/i + - expected: StaticMock + pattern: /\bStaticMock\b/i + - expected: $1Subversion$2 + pattern: /(?:([^/.])\bSubVersion)|(?:SubVersion\b([^/.]))/ + - expected: $1Subversion$2 + pattern: /(?:([^/.])\bsubversion)|(?:subversion\b([^/.]))/ + - expected: Sun + pattern: /\bSun\b/i + - expected: SunRPC + pattern: /\bSun RPC\b/ + - expected: SUSE + pattern: /\bSUSE\b/i + - expected: SWFファイル + pattern: /\bSWFファイル\b/i + - expected: Swift + pattern: /\bSwift\b/i + - expected: Swift + pattern: /\bSwfit\b/ + - expected: Symfony + pattern: /\bSymphony\b|\bsymphony\b|\bSynphony\b|\bsynphony\b|\bsynfony\b|\bSynfony\b|\bsymfony\b/ + - expected: Symfony2 + pattern: /Symfony 2/ + - expected: tDiary + pattern: /\btDiary\b/i + - expected: test-unit + pattern: /\btest-unit\b/i + - expected: Tips + pattern: /\bTips\b/i + - expected: Tomcat $1 + pattern: /Tomcat([0-9])/ + - expected: Twitter + pattern: /\bTwitter\b/i + - expected: Twitter + pattern: /ツイッター/ + - expected: ToDo + pattern: /\bToDo\b/i + - expected: Tokyo Cabinet + pattern: /\bTokyoCabinet\b/ + - expected: Tokyo Dystopia + pattern: /\bTokyoDystopia\b|Kyoto *Dystopia/ + - expected: Tokyo Promenade + pattern: /\bTokyoPromenade\b|Kyoto *Promenade/ + - expected: Tokyo Tyrant + pattern: /\bTokyoTyrant\b|Kyoto *Tyrant/ + - expected: Travis CI + pattern: /\bTravisCI\b|Travis-CI/ + - expected: Treasure Data + pattern: /\bTreasure Data\b/i + - expected: Triglav + pattern: /\bTriglav\b/i + - expected: Tritonn + pattern: /\bTriton\b|\btriton\b|\btritonn\b/ + - expected: TypeScript + pattern: /\bTypeScript\b/i + - expected: Ubuntu + pattern: /\bubuntu\b|\bUbuntsu\b|\bubuntsu\b|\bUbuntu Linux\b/ + - expected: Unicode + pattern: /\bunicode\b|ユニコード/ + - expected: UnixBench + pattern: /\bUnixBench\b/i + - expected: UNIX + pattern: /\bUnix\b/ + - expected: UTF-8 + pattern: /UTF8|UTF 8|utf8/ + - expected: Veritrans + pattern: /ベリトランス/ + - expected: VirtualBox + pattern: /\bVirtual Box\b/ + - expected: Vim $1 + pattern: /vim([0-9])/ + - expected: Vimスクリプト + pattern: /\bvim script\b/ + - expected: Visual Basic + pattern: /\bVisualBasic\b|\bVB\b/ + - expected: Visual Studio .NET + pattern: /Visual Studio\.NET/ + - expected: VMware + pattern: /\bVMware\b/i + - expected: WARファイル + pattern: /\bWARファイル\b/i + - expected: Web + - expected: Web + pattern: + - ウェブ + - ウェッブ + - expected: Web API + pattern: /\bWebAPI\b|\bWEBAPI\b|\bWEB API\b/ + - expected: Web UI + pattern: /\bWebUI\b/ + - expected: Webhook + pattern: /Webフック|\bWebHook\b/ + - expected: WebLogic + pattern: /\bWeb Logic\b/ + - expected: webpack + pattern: /\bwebpack\b/i + specs: + - from: Webpack + to: webpack + - expected: WebSphere + pattern: /\bWeb Sphere\b/ + - expected: Wi-Fi + pattern: /\bWiFi\b|Wi-fi|wi-fi/ + - expected: Windows 2000 + pattern: /Windows2000/ + - expected: Windows 2000 Server + pattern: /Windows Server 2000/ + - expected: Windows 3. + pattern: /Windows3\./ + - expected: Windows 7 + pattern: /Windows7/ + - expected: Windows 95 + pattern: /Windows95/ + - expected: Windows 98 + pattern: /Windows98/ + - expected: Windows Me + pattern: /\bWindowsMe\b|\bWindowsME\b/ + - expected: Windows NT + pattern: /\bWindowsNT\b/ + - expected: Windows Server 2003 + pattern: /Windows 2003 Server/ + - expected: Windows Server 2008 + pattern: /Windows 2008 Server/ + - expected: Windows Vista + pattern: /\bWindowsVista\b/ + - expected: Windows XP + pattern: /\bWindowsXP\b/ + - expected: WordPress + pattern: /\bWordPress\b/i + - expected: $1Word + pattern: /([^ースォ])ワード/ + - expected: xAuth + pattern: /\bxAuth\b/i + - expected: Xcode + pattern: /\bXcode\b/i + - expected: Xdebug + pattern: /\bXdebug\b/i + - expected: XML Schema + pattern: /\bXML Schema\b/i + - expected: Yahoo!$1 + pattern: /YAHOO!([^!.])|\bYahoo\b([^!.])|\bYAHOO\b([^!.])|ヤフー([^!.])/ + - expected: Yahoo!ウィジェット + pattern: /\bYahooWidget\b|Yahoo!Widget|Yahoo! Widget|\bYahooGadget\b|Yahoo!Gadget|Yahoo! Gadget/ + - expected: YouTube + pattern: /\bYoutube\b|\byoutube\b/ + - expected: YSlow + pattern: /\bYSlow\b/i + - expected: ZIPファイル + pattern: /\bZIPファイル\b/i + - expected: オライリー・ジャパン$1 + pattern: /オライリージャパン([^・])|オライリー([^・])/ + - expected: ピアソン・エデュケーション$1 + pattern: /ピアソンエデュケーション([^・])/ + - expected: を + pattern: /をを/ + - expected: に + pattern: /にに/ + - expected: が + pattern: /がが/ + - expected: する + pattern: /するする/ + - expected: て + pattern: /てて/ + - expected: で + pattern: /でで/ + - expected: $1の + pattern: /([^も])のの/ + - expected: は + pattern: /はは/ diff --git a/prh-rules/media/techbooster.yml b/prh-rules/media/techbooster.yml new file mode 100644 index 0000000..651c0f6 --- /dev/null +++ b/prh-rules/media/techbooster.yml @@ -0,0 +1,358 @@ +# Rules for TechBooster +meta: + reviewer: + - vvakame + - mhidaka + related: http://techbooster.org/ + rules: https://github.com/prh/rules + +# techbooster editor lint!! +# C89 冬コミ開始前時点で利用している版だよ! +version: 1 +imports: + - ../languages/ja/typo.yml + - ../terms/android.yml + - ../terms/javascript.yml + - ../terms/software.yml + - ../terms/review.yml + - ../terms/trademark.yml +rules: + - expected: TechBooster + pattern: てっくぶーすたー + prh: 警告メッセージのカスタマイズができるよ! + # 記号 + # 半角括弧を全角括弧に + - expected: ($1) + pattern: /\((.+?)\)/ + specs: + - from: そうですね(笑) + to: そうですね(笑) + - from: (@{test}) + to: (@{test}) + - from: "(ほげ)ほげ)" + to: "(ほげ)ほげ)" + prh: 半角カッコの代わりに全角カッコを使うこと。文字のバランスが崩れるためです + # TODO 英単語の前後の空白を殺す + + # 開き + - expected: いえ + pattern: 言え + prh: 漢字で書かず、ひらがなで書くと読みやすくなります + - expected: いう + pattern: 言う + prh: 漢字で書かず、ひらがなで書くと読みやすくなります + - expected: いわ + pattern: 言わ + prh: 呼ぶ、で代替するか漢字で書かず、ひらがなで書くと読みやすくなります。 + - expected: さまざま + pattern: 様々 + prh: 漢字で書かず、ひらがなで書くと読みやすくなります + - expected: よい + pattern: /良い(?!例)/ + prh: 良し悪しを評価する表現は"良い"、しなくていい、など評価でない表現は"よい"を使います + specs: + - from: 良い + to: よい + - from: 良い例 + to: 良い例 + - expected: さらに + pattern: /(変)?更に/ + regexpMustEmpty: $1 + prh: 漢字で書かず、ひらがなで書くと読みやすくなります + specs: + - from: 変更に + to: 変更に + - expected: もつ + pattern: 持つ + prh: 漢字で書かず、ひらがなで書くと読みやすくなります + - expected: とおり + pattern: 通り + prh: 漢字で書かず、ひらがなで書くと読みやすくなります。"どおり"のケースもありえます + - expected: ひととおり + pattern: /(一|ひと)通り/ + prh: 漢字で書かず、ひらがなで書くと読みやすくなります。 + - expected: すでに + pattern: 既に + prh: 漢字で書かず、ひらがなで書くと読みやすくなります + - expected: すべて + pattern: /(全て|総て)/ + prh: 漢字で書かず、ひらがなで書くと読みやすくなります + - expected: たとえ + pattern: /(例えば|例え)/ + prh: 漢字で書かず、ひらがなで書くと読みやすくなります + - expected: 他の + pattern: ほかの + prh: ひらがなで書かず、漢字で書くと読みやすくなります + - expected: 分かる + pattern: わかる + prh: ひらがなで書かず、漢字で書くと読みやすくなります + - expected: $1中 + pattern: /(その)なか/ + prh: ひらがなで書かず、漢字で書くと読みやすくなります + - expected: きれい + pattern: 綺麗 + prh: 漢字で書かず、ひらがなで書くと読みやすくなります + - expected: こと + pattern: /(記|大|仕|返|無|食|見|議|関心)?事(?!情|件|前|後|象|例|実|体|態|項|務|業|柄)/ + regexpMustEmpty: $1 + prh: 漢字で書かず、ひらがなで書くと読みやすくなります + specs: + - from: ある事 + to: あること + - from: 記事 + to: 記事 + - from: 事件 + to: 事件 + - from: 事象 + to: 事象 + - from: 事柄 + to: 事柄 + - expected: $1とき + pattern: /(の)時(?!点|代|々|間)/ + specs: + - from: その時 + to: そのとき + - from: その時点 + to: その時点 + - from: その時代 + to: その時代 + - from: それまでの時間 + to: それまでの時間 + - from: 同時 + to: 同時 + - from: 実行時 + to: 実行時 + - from: 利用時 + to: 利用時 + - from: 開発時 + to: 開発時 + - from: 執筆時 + to: 執筆時 + - from: 時点 + to: 時点 + - from: 時代 + to: 時代 + - from: 時間 + to: 時間 + prh: 漢字で書かず、ひらがなで書くと読みやすくなります + - expected: でき$1 + pattern: /出来(る|て|た|ま|上が)/ + specs: + - from: 出来上がった + to: でき上がった + prh: 漢字で書かず、ひらがなで書くと読みやすくなります + - expected: したがって + pattern: 従って + prh: 漢字で書かず、ひらがなで書くと読みやすくなります + - expected: $1ように + pattern: /(の)様に/ + prh: 漢字で書かず、ひらがなで書くと読みやすくなります + - expected: 次$1 + pattern: /(?:以下|下記)(の|に)/ + specs: + - from: 以下の + to: 次の + - from: 以下に + to: 次に + - from: 次回 + to: 次回 + - from: 下記の + to: 次の + prh: 書籍の場合は、以下ではなく次を利用します(常に下にあるとは限らないため) + - expected: かかわらず + pattern: /関わ?らず/ + specs: + - from: 関わらず + to: かかわらず + - from: 関らず + to: かかわらず + prh: 漢字で書かず、ひらがなで書くと読みやすくなります + - expected: なる + pattern: 成る + prh: 漢字で書かず、ひらがなで書くと読みやすくなります + - expected: お勧め + pattern: おすすめ + prh: ひらがなで書かず、漢字で書くと読みやすくなります + - expected: $1あとで + pattern: /(して|した|、)後で/ + specs: + - from: して後で + to: してあとで + prh: 漢字で書かず、ひらがなで書くと読みやすくなります + - expected: あらかじめ + pattern: 予め + prh: 漢字で書かず、ひらがなで書くと読みやすくなります + - expected: なぜ + pattern: 何故 + prh: 漢字で書かず、ひらがなで書くと読みやすくなります + - expected: ゆえに + pattern: 故に + prh: 漢字で書かず、ひらがなで書くと読みやすくなります + - expected: うまく + pattern: 巧く + prh: 漢字で書かず、ひらがなで書くと読みやすくなります + - expected: もっぱら + pattern: 専ら + prh: 漢字で書かず、ひらがなで書くと読みやすくなります + - expected: はやる + pattern: 流行る + prh: 漢字で書かず、ひらがなで書くと読みやすくなります + - expected: のよう + pattern: の様 + prh: 漢字で書かず、ひらがなで書くと読みやすくなります。 + - expected: まったく + pattern: 全く + prh: 漢字で書かず、ひらがなで書くと読みやすくなります。 + - expected: さきほど + pattern: 先程 + prh: 漢字で書かず、ひらがなで書くと読みやすくなります。 + - expected: あるいは + pattern: 或いは + prh: 漢字で書かず、ひらがなで書くと読みやすくなります。 + - expected: はじめて + pattern: 初めて + prh: 漢字で書かず、ひらがなで書くと読みやすくなります。 + - expected: どういうとき + pattern: どういう時 + prh: 漢字で書かず、ひらがなで書くと読みやすくなります。 + - expected: いくつか + pattern: 幾つか + prh: 漢字で書かず、ひらがなで書くと読みやすくなります。 + - expected: 下げ + pattern: さげ + prh: ひらがなで書かず、漢字で「下げ」と読みやすくなります。 + - expected: もら$1 + pattern: /貰(う|い)/ + prh: 漢字で書かず、ひらがなで書くと読みやすくなります。 + specs: + - from: 貰う + to: もらう + - from: 貰い + to: もらい + - expected: いったん + pattern: 一旦 + prh: 漢字で書かず、ひらがなで書くと読みやすくなります。 + + # 通常は、ひとつ。数詞は1つ、漢数字は数えられる固有名詞を指す場合に利用 + - expected: ひとつ + pattern: 一つ + prh: 通常は、ひとつ。数詞は1つ、漢数字は数えられる固有名詞を指す場合に利用します + - expected: ふたつ + pattern: 二つ + prh: 通常は、ふたつ。数詞は1つ、漢数字は数えられる固有名詞を指す場合に利用 + - expected: もっとも + pattern: 最も + prh: 漢字で書かず、ひらがなで書くと読みやすくなります + - expected: ちょうど + pattern: /(丁度|調度)/ + prh: 漢字で書かず、ひらがなで書くと読みやすくなります + - expected: いずれ + pattern: 何れ + prh: 漢字で書かず、ひらがなで書くと読みやすくなります + - expected: ほとんど + pattern: 殆ど + prh: 漢字で書かず、ひらがなで書くと読みやすくなります + - expected: 、 + pattern: , + prh: カンマとコンマではなく句点読点を使います。 + - expected: 。 + pattern: . + prh: カンマとコンマではなく句点読点を使います。 + - expected: すべて + pattern: 全て + prh: 漢字で書かず、ひらがなで書くと読みやすくなります + - expected: あえて + pattern: 敢えて + prh: 漢字で書かず、ひらがなで書くと読みやすくなります + - expected: いくつ + pattern: 幾つ + prh: 漢字で書かず、ひらがなで書くと読みやすくなります + - expected: 使$1 + pattern: /つか(う|って|った)/ + prh: ひらがなで書かず、漢字で書くと読みやすくなります。 + - expected: あとから + pattern: 後から + prh: 漢字で書かず、ひらがなで書くと読みやすくなります + - expected: よく + pattern: 良く + prh: 漢字で書かず、ひらがなで書くと読みやすくなります + - expected: みつけ + pattern: 見つけ + prh: 漢字で書かず、ひらがなで書くと読みやすくなります + - expected: ある + pattern: 有る + prh: 漢字で書かず、ひらがなで書くと読みやすくなります + - expected: ない + pattern: 無い + prh: 漢字で書かず、ひらがなで書くと読みやすくなります + - expected: わかりづらい + pattern: 分かり辛い + prh: 漢字で書かず、ひらがなで書くと読みやすくなります + - expected: やすい + pattern: 易い + prh: 漢字で書かず、ひらがなで書くと読みやすくなります + - expected: すべて + pattern: 全て + prh: 漢字で書かず、ひらがなで書くと読みやすくなります + - expected: 移る + pattern: うつる + prh: ひらがなで書かず、漢字で書くと読みやすくなります + - expected: 同じ + pattern: おなじ + prh: ひらがなで書かず、漢字で書くと読みやすくなります + - expected: 続け + pattern: つづけ + prh: ひらがなで書かず、漢字で書くと読みやすくなります + - expected: 選$1 + pattern: /えら(ぶ|んだ)/ + prh: ひらがなで書かず、漢字で書くと読みやすくなります + + + # footnoteの末尾は読点を使わない + # タイトル見出しの末尾は読点を使わない + # 表、コード見出しの末尾は読点を使わない + # 箇条書きの末尾は読点を使わない + # 箇条書きの末尾は体言止め、または動詞でとめる、が統一されているか + # footnoteの参照は名詞、または末尾にかかっているか(原則、動詞にかからない) + # 表、ソースコードへの参照が本体より前に配置されているか。 + # 文末の参照は(@{id})。となっているか。@{id}。などはNG + # 。(@{manifest_gradle}) などもNG + # だいたい、ほとんど、など:曖昧語への注意喚起Lintしたい + # listnum記法などあんまり積極的に使いたくない記法を喚起したい + + # 本文中の半角スペースは排除したい + + # 横文字 + - expected: ライブラリ + pattern: ラブライブ # C87でやらかした人がいましたね? + prh: C87でざきさんがやらかした思い出 + - expected: ユーザー + pattern: /ユーザ(?!ー)/ # 末尾に長音記号がつかない「ユーザ」だけ拾う + prh: ユーザーでもユーザでもいいんだけどユーザーで統一してる + + - expected: Wi-Fi + pattern: WiFi + - expected: JSON + pattern: Json + - expected: JUnit 5 + pattern: JUnit5 + - expected: JUnit 4 + pattern: JUnit4 + - expected: 引数 + pattern: 引き数 + - expected: おおむね + pattern: 概ね + - expected: パラメータ + pattern: パラメーター + - expected: 仕組み + pattern: しくみ + - expected: そう + pattern: 沿う + - expected: そって + pattern: 沿って + #- expected: Firebase + # pattern: Firebas + # その他 + - expected: コード補完 + pattern: コード保管 + prh: コード補完の間違いと思われます。 diff --git a/prh-rules/package.json b/prh-rules/package.json new file mode 100644 index 0000000..6b143ba --- /dev/null +++ b/prh-rules/package.json @@ -0,0 +1,25 @@ +{ + "name": "prh-rules", + "private": true, + "version": "1.0.0", + "description": "", + "scripts": { + "test": "node test.js" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/prh/rules.git" + }, + "author": "vvakame", + "license": "MIT", + "bugs": { + "url": "https://github.com/prh/rules/issues" + }, + "homepage": "https://github.com/prh/rules#readme", + "dependencies": { + "prh": "^1.0.3" + }, + "devDependencies": { + "glob-expand": "^0.2.1" + } +} diff --git a/prh-rules/terms/android.yml b/prh-rules/terms/android.yml new file mode 100644 index 0000000..2cf6b99 --- /dev/null +++ b/prh-rules/terms/android.yml @@ -0,0 +1,34 @@ +# Rules for Android +meta: + reviewer: + - vvakame + - mhidaka + related: https://www.android.com/ + rules: https://github.com/prh/rules + +version: 1 +rules: + - expected: API Level + pattern: API level + prh: APIドキュメントでも揺れてますが、Levelで統一してます + - expected: Android Studio + pattern: AndroidStudio + prh: 正式名称はAndroid Studioです + - expected: JUnit 5 + pattern: JUnit5 + - expected: JUnit 4 + pattern: JUnit4 + - expected: View + pattern: Vew + - expected: Deep Link + pattern: DeepLink + - expected: Jetpack + pattern: JetPack + - expected: aidl + pattern: /adil/ + - expected: AIDL + pattern: /ADIL/ + - expected: Safe Args + pattern: safeargs + - expected: Navigation + pattern: Navigtion diff --git a/prh-rules/terms/javascript.yml b/prh-rules/terms/javascript.yml new file mode 100644 index 0000000..c790baa --- /dev/null +++ b/prh-rules/terms/javascript.yml @@ -0,0 +1,20 @@ +# Rules for JavaScript +meta: + reviewer: + - vvakame + related: http://www.ecma-international.org/publications/standards/Ecma-262.htm + rules: https://github.com/prh/rules + +version: 1 +rules: + - expected: jQuery + - expected: Angular 2 + pattern: Angular2 + - expected: Web Components + pattern: WebComponents + - expected: Custom Elements + pattern: CustomElements + - expected: Shadow DOM + pattern: ShadowDOM + - expected: Incremental DOM + pattern: IncrementalDOM diff --git a/prh-rules/terms/review.yml b/prh-rules/terms/review.yml new file mode 100644 index 0000000..a9f84a4 --- /dev/null +++ b/prh-rules/terms/review.yml @@ -0,0 +1,18 @@ +# Rules for Re:VIEW +meta: + reviewer: + - vvakame + - mhidaka + related: https://github.com/kmuto/review/ + rules: https://github.com/prh/rules + +version: 1 +rules: + - expected: "@<$1>{$2}" # 先頭 @ はyaml的にアレなのでダブルクォートで囲む + pattern: /@([^{<>]+)\{([^}]+)\}/ + specs: + - from: "@list{foo}" + to: "@{foo}" + - from: "@{foo}" + to: "@{foo}" + prh: Re:VIEW記法の書き方を間違えていませんか? diff --git a/prh-rules/terms/software.yml b/prh-rules/terms/software.yml new file mode 100644 index 0000000..e352923 --- /dev/null +++ b/prh-rules/terms/software.yml @@ -0,0 +1,12 @@ +# Rules for Software Development +meta: + reviewer: + - vvakame + - mhidaka + rules: https://github.com/prh/rules + +version: 1 +rules: + - expected: Web + - expected: superset + pattern: super set diff --git a/prh-rules/terms/trademark.yml b/prh-rules/terms/trademark.yml new file mode 100644 index 0000000..98d3593 --- /dev/null +++ b/prh-rules/terms/trademark.yml @@ -0,0 +1,35 @@ +# Rules for Trademark +meta: + reviewer: + - vvakame + - mhidaka + rules: https://github.com/prh/rules + +version: 1 +rules: + - expected: Re:VIEW + pattern: /ReVIEW/ + specs: + - from: ReVIEW + to: Re:VIEW + - from: review + to: review + - expected: 技術書典 + pattern: /技術書(店|…|点|展|てん)/ + specs: + - from: 技術書点 + to: 技術書典 + - from: 技術書展 + to: 技術書典 + - expected: American Express(Amex) + pattern: + - アメリカン・エキスプレス + - アメックス + - アメリカンエクスプレス + - expected: 技術書典 + pattern: /技術書(店|…|点|展|てん)/ + specs: + - from: 技術書点 + to: 技術書典 + - from: 技術書展 + to: 技術書典 diff --git a/prh-rules/test.js b/prh-rules/test.js new file mode 100644 index 0000000..757183f --- /dev/null +++ b/prh-rules/test.js @@ -0,0 +1,22 @@ +"use strict"; + +const expand = require("glob-expand"); +const prh = require("prh"); +const assert = require("assert"); + +const ymlList = expand({ filter: "isFile", cwd: __dirname }, [ + "**/*.yml", + "!node_modules/**", +]); + +ymlList.forEach(yml => { + try { + const engine = prh.fromYAMLFilePath(yml); + const changeSet = engine.makeChangeSet("./README.ja.md"); + } catch (e) { + console.log(`processing... ${yml}\n`); + throw e; + } +}); + +console.log("😸 done"); diff --git a/rebuild-css.sh b/rebuild-css.sh new file mode 100755 index 0000000..1a01eca --- /dev/null +++ b/rebuild-css.sh @@ -0,0 +1,3 @@ +#!/bin/sh +npm install --no-save node-sass +$(npm bin)/node-sass ./articles/ --output ./articles/ diff --git a/redpen-conf-ja.xml b/redpen-conf-ja.xml new file mode 100644 index 0000000..bd1cf0d --- /dev/null +++ b/redpen-conf-ja.xml @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/setup.sh b/setup.sh new file mode 100755 index 0000000..c406b11 --- /dev/null +++ b/setup.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +set -eux + +# 真にクリーンビルドを有効にする場合は下記を有効に +#rm -rf node_modules + +# --unsafe-perm はrootでの実行時(= docker環境)で必要 非root時の挙動に影響なし +npm install --unsafe-perm