数学が苦手なはずのゲーム作者でも無意識のうちに使っている「微積分」について、実際に Windows 標準環境で動かすことのできる PowerShell スクリプト付きで分かりやすく解説していきます。
目次
- まえがき (対象読者など)
- 本来の意味での「微積分」とは?
- 【実例あり】引き算の繰り返しで y = x^2 を y = 2x に変える
- 【実例あり】足し算の繰り返しで y = 2x を y = x^2 に変える
- 本当は易しい「微積分」
- 微積分を理解することのメリット
- 補足 1 : 次のレベルまでの経験値を「数式の変形」で推定する方法
- 補足 2 : 報酬計算時の「積分」と「内積」の関係について
- おわりに
まえがき (対象読者など)
「微積分なんて意味不明」「数学は苦手」
そう思っているゲーム作者の方も多いはずです。
しかし、ゲームでよく使われる次の処理は「微積分」だと言われたら、あなたは信じられるでしょうか?
- 例 1 : 「累計経験値」から、「次のレベルの経験値」を求める処理は「微分」
- 例 2 : 報酬の「単価」と「入手数」から「総報酬額」を求める処理は「積分」
え? これが本当に微積分なの?
安心して下さい。そう思ってしまうのは、高校数学 (受験数学) の詰め込み教育に問題があるからです。
上記が微積分だと分からなかった方が「誤解」していること
先述の例が微積分だと理解できない方の頭の中にあるイメージは、次のようなものでしょう。
- 受験教育の犠牲者による微分のイメージ
- 受験教育の犠牲者による積分のイメージ
これらは間違いではありませんが、これしか思い浮かばないのであれば、ほぼ「受験教育の犠牲者」確定です。入試で点数を取るためには、このテクニックの方が重要だからです。
本来の微積分は、もっと簡単な概念で、ゲーム作者なら容易に理解できるものですので、本記事をきっかけに受験教育による洗脳を打ち払い、ゲーム制作の役に立てていきましょう。
本来の意味での「微積分」とは?
本来の意味での微積分は、次のとおりです。
- 微分 = 引き算の繰り返し
- 積分 = 足し算の繰り返し
これを踏まえて、もう一度「まえがき」に挙げた例を見てみましょう。
これは、「レベル n+1 とレベル n の経験値を引く」ことを繰り返していますので、紛うことなき微分です。
これは、小計を繰り返し足し算していますので、紛うことなき積分です。
じゃあ、「受験教育の犠牲者のイメージ」の方はどうなの?
そう言うと、反論として、次の例は「引き算」や「足し算」の繰り返しになってないじゃないかと思われるかもしれません。
- どう見ても「引き算の繰り返し」のようには見えない微分法
- どう見ても「引き算の繰り返し」のようには見えない積分法
実は、これらは、「引き算」「足し算」の繰り返しを行った結果、どのような関数に変わるのかを数式で示した「単なる結果論」に過ぎません。
つまり、大学受験で勝つためだけに、あれだけ多数の微積分の公式 (それぞれ導関数、原始関数と呼びます) を暗記させられたのです。
【実例あり】引き算の繰り返しで y = x^2 を y = 2x に変える
これから示す PowerShell スクリプトは、Windows 標準搭載機能ですので、Windows PC をお持ちの方は (古すぎなければ) 誰でも動かすことが可能です。
- Windows PC をお持ちでない方も、この後、いくつかの実行結果の例が説明とともに出てきますので、ご安心ください。
動かし方の詳細は、別記事「Windows 標準機能のみでグラフをプロットする方法」をご確認下さい。(※ 本例でも、実際にグラフのプロットを行っています)
PowerShell スクリプト
diff.bat (スクリプト開始用バッチファイル)
PowerShell -NoProfile -ExecutionPolicy Unrestricted .\diff.ps1
diff.ps1 (PowerShell スクリプト本体)
# ■ グラフ描画関連の初期化 using namespace System.Drawing using namespace System.Drawing.Imaging using namespace System.Windows.Forms using namespace System.Windows.Forms.DataVisualization.Charting Add-Type -AssemblyName System.Windows.Forms Add-Type -AssemblyName System.Windows.Forms.DataVisualization # ■■■■■■■■■■ 条件設定 ここから ■■■■■■■■■■ $x_start = 0 # 開始 $x_end = 4.001 # 終了 $h = 0.5 # 分解能 # ■■■■■■■■■■ 条件設定 ここまで ■■■■■■■■■■ # ■ y = x^2 を数値微分しながらグラフデータの系列を作成 $series = New-Object Series for ($x = $x_start; $x -le $x_end; $x = $x + $h) { # f(x) $f_x = $x * $x # f(x + h) $f_x_plus_1 = ($x + $h) * ($x + $h) # 数値微分 (f(x + h) - f(x)) / h $f_x_diff = ($f_x_plus_1 - $f_x) / $h # 計算結果をグラフの系列に追加 [void]$series.Points.AddXY($x, $f_x_diff) } $series.ChartType = [SeriesChartType]::Line # 折れ線グラフ $series.BorderWidth = 2 # 線の太さ 2 $series.MarkerSize = 8 # マーカーの大きさ 4 $series.MarkerStyle = [MarkerStyle]::Square # 四角形マーカー # ■ グラフ本体を作成 $chartArea = New-Object ChartArea $chartArea.AxisX.Title = "x" $chartArea.AxisX.Minimum = $x_start $chartArea.AxisX.Maximum = $x_end $chartArea.AxisX.Interval = 1 # 見やすくするため 1 刻みにする $chartArea.AxisY.Title = "y" $chartArea.AxisY.Interval = 1 # 見やすくするため 1 刻みにする $chart = New-Object Chart $chart.Series.Add($series) $chart.ChartAreas.Add($chartArea) [void]$chart.Titles.Add( "y = x^2 の数値微分結果 (分解能 h = " + $h + ")") $chart.Size = New-Object Size(640, 360) # ■ グラフの中身を画像ファイル (PNG) に保存 $bitmap = New-Object Bitmap( $chart.Size.Width, $chart.Size.Height) $chart.DrawToBitmap($bitmap, (New-Object Rectangle( 0, 0, $chart.Size.Width, $chart.Size.Height))) $bitmap.Save($PSScriptRoot + "\diff.png", [ImageFormat]::Png) $bitmap.Dispose() # ■ グラフの中身をウィンドウとして表示 $form = New-Object Form $form.ClientSize = $chart.Size $form.Controls.Add($chart) [void]$form.ShowDialog()
簡単な説明
本コードは、実際に y = x^2 の計算結果同士を引き算することで、y = 2x になることをグラフにプロットして確認するプログラムになっています。
実際に計算している箇所は以下です。
# ■ y = x^2 を数値微分しながらグラフデータの系列を作成 $series = New-Object Series for ($x = $x_start; $x -le $x_end; $x = $x + $h) { # f(x) $f_x = $x * $x # f(x + h) $f_x_plus_1 = ($x + $h) * ($x + $h) # 数値微分 (f(x + h) - f(x)) / h $f_x_diff = ($f_x_plus_1 - $f_x) / $h # 計算結果をグラフの系列に追加 [void]$series.Points.AddXY($x, $f_x_diff) }
また、その手前に、計算時の条件 (範囲、細かさ) を指定するための箇所があります。
# ■■■■■■■■■■ 条件設定 ここから ■■■■■■■■■■ $x_start = 0 # 開始 $x_end = 4.001 # 終了 $h = 0.5 # 分解能 # ■■■■■■■■■■ 条件設定 ここまで ■■■■■■■■■■
まずは、とりあえず上記の条件のまま実行してみましょう。
分解能 h = 0.5 のときの結果
x が 1, 2, 3, ... のときの y 軸の値を見てみると、2.5, 4.5, 6.5, ... のようになっています。
あれ? y = 2x のグラフになるんじゃなかったの?
実は、まだ説明していなかった条件があります。
それは、分解能 h は、できるだけ細かい方が良いということです。
高校数学の微分の授業でおそらく習ったはずの以下の式を思い出してみましょう。
実は、この式こそが「微分の極意」で、引き算の繰り返しにもなっています。
(残念ながら試験問題ではあまり重視されないため、忘れてしまった方も多いと思います…)
つまり、f(x) = x^2 について、h = 0.5 (x = 0, 0.5, 1, 1.5, ...) だと粗すぎるのだということを、この式は言っています。
h を限りなくゼロに近づけなさいということは、例えば h = 0.2 (x = 0, 0.2, 0.4, 0.6, ...) にした方がマシだと言っているになります。
分解能 h = 0.2 のときの結果
それでは、x = 0, 0.2, 0.4, 0.6, ... となるように計算してみましょう。
PowerShell スクリプト上は、以下の部分を 0.5 から 0.2 に書き換えるだけです。
# ■■■■■■■■■■ 条件設定 ここから ■■■■■■■■■■ $x_start = 0 # 開始 $x_end = 4.001 # 終了 $h = 0.2 # 分解能 # ■■■■■■■■■■ 条件設定 ここまで ■■■■■■■■■■
すると、先ほどよりもマシな結果になるはずです。
まだちょっとずれてはいますが、だいぶ y = 2x に近づいたと言えるでしょう。
分解能 h = 0.05 のときの結果
計算は全部 PowerShell 先生に任せればよいので、さらに細かくしてみましょう。
# ■■■■■■■■■■ 条件設定 ここから ■■■■■■■■■■ $x_start = 0 # 開始 $x_end = 4.001 # 終了 $h = 0.05 # 分解能 # ■■■■■■■■■■ 条件設定 ここまで ■■■■■■■■■■
ほぼ、y = 2x になったと言って良さそうな形状になりました。
コンピューターにおける「微分」の制限
しかし、数学上の厳密な微分の定義は、あくまでも以下です。
でも、これは「分解能を無限にしろ」という意味なので、デジタルでは無理です。
なので、「ゲームソフト」も含むコンピューター分野では、分解能 h (キャラのレベルの細かさ、画像の解像度など) を必要十分なところで打ち切っているのです。
これを、数値微分 (Numerical Differentiation) と言います。
なお、分母の h は、不必要な分野であれば省略することもあります。
(例えば本記事で例に挙げた経験値テーブルの場合が該当)
【実例あり】足し算の繰り返しで y = 2x を y = x^2 に変える
それでは、次は、逆に「足し算の繰り返し」による積分を試してみましょう。
PowerShell スクリプト
integ.bat (スクリプト開始用バッチファイル)
PowerShell -NoProfile -ExecutionPolicy Unrestricted .\integ.ps1
integ.ps1 (PowerShell スクリプト本体)
# ■ グラフ描画関連の初期化 using namespace System.Drawing using namespace System.Drawing.Imaging using namespace System.Windows.Forms using namespace System.Windows.Forms.DataVisualization.Charting Add-Type -AssemblyName System.Windows.Forms Add-Type -AssemblyName System.Windows.Forms.DataVisualization # ■■■■■■■■■■ 条件設定 ここから ■■■■■■■■■■ $x_start = 0 # 開始 $x_end = 3.001 # 終了 $h = 0.5 # 分解能 # ■■■■■■■■■■ 条件設定 ここまで ■■■■■■■■■■ # ■ y = 2x を数値積分しながらグラフデータの系列を作成 $series = New-Object Series $integ = 0 # 数値積分 (Σ) 先の合計用変数 for ($x = $x_start; $x -le $x_end; $x = $x + $h) { # f(x) $f_x = 2 * $x # 数値積分 Σf(x) $integ += $f_x # 数値積分結果をスケーリング hΣf(x) $f_x_integ = $h * $integ # 計算結果をグラフの系列に追加 [void]$series.Points.AddXY($x, $f_x_integ) } $series.ChartType = [SeriesChartType]::Line # 折れ線グラフ $series.BorderWidth = 2 # 線の太さ 2 $series.MarkerSize = 8 # マーカーの大きさ 4 $series.MarkerStyle = [MarkerStyle]::Square # 四角形マーカー # ■ グラフ本体を作成 $chartArea = New-Object ChartArea $chartArea.AxisX.Title = "x" $chartArea.AxisX.Minimum = $x_start $chartArea.AxisX.Maximum = $x_end $chartArea.AxisX.Interval = 1 # 見やすくするため 1 刻みにする $chartArea.AxisY.Title = "y" $chartArea.AxisY.Interval = 1 # 見やすくするため 1 刻みにする $chart = New-Object Chart $chart.Series.Add($series) $chart.ChartAreas.Add($chartArea) [void]$chart.Titles.Add( "y = 2x の数値積分結果 (分解能 h = " + $h + ")") $chart.Size = New-Object Size(640, 360) # ■ グラフの中身を画像ファイル (PNG) に保存 $bitmap = New-Object Bitmap( $chart.Size.Width, $chart.Size.Height) $chart.DrawToBitmap($bitmap, (New-Object Rectangle( 0, 0, $chart.Size.Width, $chart.Size.Height))) $bitmap.Save($PSScriptRoot + "\integ.png", [ImageFormat]::Png) $bitmap.Dispose() # ■ グラフの中身をウィンドウとして表示 $form = New-Object Form $form.ClientSize = $chart.Size $form.Controls.Add($chart) [void]$form.ShowDialog()
簡単な説明
本コードは、実際に y = 2x の計算結果同士を細かく足し算することで、y = x^2 になることをグラフにプロットして確認するプログラムになっています。
実際に計算している箇所は以下です。
# ■ y = 2x を数値積分しながらグラフデータの系列を作成 $series = New-Object Series $integ = 0 # 数値積分 (Σ) 先の合計用変数 for ($x = $x_start; $x -le $x_end; $x = $x + $h) { # f(x) $f_x = 2 * $x # 数値積分 Σf(x) $integ += $f_x # 数値積分結果をスケーリング hΣf(x) $f_x_integ = $h * $integ # 計算結果をグラフの系列に追加 [void]$series.Points.AddXY($x, $f_x_integ) }
また、その手前に、計算時の条件 (範囲、細かさ) を指定するための箇所があります。
# ■■■■■■■■■■ 条件設定 ここから ■■■■■■■■■■ $x_start = 0 # 開始 $x_end = 3.001 # 終了 $h = 0.5 # 分解能 # ■■■■■■■■■■ 条件設定 ここまで ■■■■■■■■■■
まずは、とりあえず上記の条件のまま実行してみましょう。
分解能 h = 0.5 のときの結果
x が 1, 2, 3, ... のときの y 軸の値を見てみると、期待値である 1, 4, 9, ... よりも明らかに大きい結果になっています。
微分と同様、積分を数値的に計算する場合でも、やはり分解能が重要になってきます。
分解能 h = 0.2 のときの結果
それでは、x = 0, 0.2, 0.4, 0.6, ... となるように計算してみましょう。
PowerShell スクリプト上は、以下の部分を 0.5 から 0.2 に書き換えるだけです。
# ■■■■■■■■■■ 条件設定 ここから ■■■■■■■■■■ $x_start = 0 # 開始 $x_end = 3.001 # 終了 $h = 0.2 # 分解能 # ■■■■■■■■■■ 条件設定 ここまで ■■■■■■■■■■
すると、先ほどよりもマシな結果になるはずです。
まだちょっとずれてはいますが、だいぶ y = x^2 に近づいたと言えるでしょう。
分解能 h = 0.05 のときの結果
計算は全部 PowerShell 先生に任せればよいので、さらに細かくしてみましょう。
# ■■■■■■■■■■ 条件設定 ここから ■■■■■■■■■■ $x_start = 0 # 開始 $x_end = 3.001 # 終了 $h = 0.05 # 分解能 # ■■■■■■■■■■ 条件設定 ここまで ■■■■■■■■■■
ほぼ、y = x^2 になったと言って良さそうな形状になりました。
コンピューターにおける「積分」の制限
微分と同様、積分もコンピューター上では「分解能」による制限を受けます。必要十分な分解能を見定めて打ち切りする必要のあるところは、数値微分と同様です。
実際の積分の式は、以下のように「インテグラル」記号をもちいています
が、コンピューター上で行う数値積分 (Numerical Integration) では
のように「シグマ」記号を使います。(高校数学の「Σ」と同じ意味です)
つまり「∫」は分解能が無限、「Σ」は分解能が有限の積分を意味します。
本当は易しい「微積分」
以上までで、実際に微積分が、単なる「足し算」と「引き算」の繰り返しであることを示しました。
つまり、受験数学で多くの人が微積分アレルギーになってしまう理由は、結果論 (公式) をひたすら暗記して、パズルのように数式を変形しなければいけなかったからと言えます。
本当の微積分は、(分解能の概念はあるものの) 足し算と引き算だけでできる、誰にでも易しい、敷居の低いアルゴリズムなのです。
高校数学 (受験数学) でひたすらやった式の変形の重要性について
もちろん、高校数学 (受験数学) でやるような式の変形は、重要であることには変わりません。
一般的に知られている微分・積分可能な関数であれば「式を変形するだけで結果が分かる」ため、無駄な繰り返し処理をコンピューターにやらせなくても、導関数や原始関数の式をそのままプログラムのコード中に埋め込めば処理が軽くなることを意味します。
例えば、本記事で示したような y = 2x や y = x^2 のような単純な式であれば、わざわざループ処理を行うまでもなく、導関数や原始関数の公式を使えば良いだけなのです。
(そういう意味では、受験数学も、ゲーム制作に役に立つこともあります)
数値微分・数値積分の重要性について
逆に、今回のように「ループ処理」が必要になるのは、処理したい対象データが単純な式では表せない場合です。
例えば
- 音楽 CD に記録した音楽データ
- デジカメで撮影した写真
は、y = 2x とか、y = x^2 のような単純な式で表すのはまず無理があります。
世の中には、そのような「式の変形」だけでは対処できない事象にも微積分が必要があるケースが多々ある (むしろこっちの方がメインなくらい) ので、数値微分・数値積分が活躍するのです。
ゲーム作者が微積分を理解することのメリットについて
特に 3D ゲームやオーディオの分野では、微積分を含む難しそうなアルゴリズムが多々存在することは、ゲーム作者ならご存じでしょう。
- 例えば、オーディオの周波数解析や耳コピで使われる「離散フーリエ変換」は、まさに「数値積分」が使われているアルゴリズムです。
しかし、微積分が単に「足し算や引き算の繰り返し」であることさえ理解しておけば、そのようなアルゴリズムの理解・実装に挑戦するときに、恐れる必要が無くなるといえます。
微積分を理解しておくことの真のメリットは、先人達が開発し、微積分の式として論文などに残してくれた豊富なアルゴリズムが存在するおかげで、一見複雑で難しそうな処理が、実は「足し算」「引き算」のループ処理だけで実現できることが多いという紛れもない事実です。
数学が苦手だと感じているゲーム作者にとって、非常に勇気づけられる事実のはずです。
なんかインテグラルとかシグマとかいっぱい出てきてよく分からないんだけど……と尻込みせずに、落ち着いて数式を読んで、for 文による足し算・引き算のループに落とし込むことさえできれば、今までの不可能が可能になるのです。
補足 1 : 次のレベルまでの経験値を「数式の変形」で推定する方法
本記事の最初に紹介した下記の例は、実はポケモンで使われている経験値タイプ 100 万の式で、累計経験値が y = x^3 の式 (x = ポケモンのレベル) になっています。
この事例の場合、「数式の変形」だけでも、次のレベルまでの経験値を「概算」できます。
y = x^3 を、高校数学のときのように導関数の公式を使って y' = 3 * x^2 と変形すると…
- x = 50.5 のとき、y' = 3 * 50.5 * 50.5 = 7650.75 (約 7651)
- x = 51.5 のとき、y' = 3 * 51.5 * 51.5 = 7956.75 (約 7957)
- x = 52.5 のとき、y' = 3 * 50.5 * 50.5 = 8268.75 (約 8269)
微妙に端数が出るのは、そもそもポケモンのレベル自体が「不連続」(分解能が 1) だからです。純粋に y = x^3 ではなく、飛び飛びになった y = x^3 の経験値テーブルなので、数式の変形 (導関数) で計算するには限界があると言えます。
余談:経験値曲線で 3 次曲線が使われることが多い理由
上記のポケモンの事例からもなんとなく推測できると思いますが、経験値曲線は、累計経験値を微分した後 (つまりレベル毎の経験値) も曲線で増加している必要があるからです。
次のレベルに必要な経験値が直線で比例するようでは、特にゲームの後半、取得経験値が増えてくると、「レベルを上げて物理で殴る」ことが容易になってしまうのは明らかです。
つまり、RPG だと
- 累計経験値曲線を 3 次曲線の式で作っておき、微分して 2 次曲線になるようにする。(例:ポケモン)
- 次のレベルまでの経験値を 2 次曲線の式で作っておき、積分して 3 次曲線になるようにする。(例:エクスペリエンス社の DRPG)
のいずれかの手法がとられることが多いです。
補足 2 : 報酬計算時の「積分」と「内積」の関係について
本記事の最初に紹介した下記の例は、「積分」であるのと同時に、「内積」でもあります。
これ、実は高校数学の「ベクトル」の単元で扱う「内積」と同じものです。
- 単価ベクトル = (100, 80, 120, 150)
- 入手数ベクトル = (3, 5, 2, 1)
つまり「単価ベクトル」と「入手数ベクトル」の内積は…
- 100×3 + 80×5 + 120×2 + 150×1 = 1090
実は、この「内積」は、数学的には「積分」の一種であるのと同時に、ゲームソフトで最も頻繁に使われる高校数学の内容の 1 つです。
例えば、戦闘が終わった後の経験値の計算も、「経験値ベクトル」と「討伐数ベクトル」の内積です。
他にも、大抵の報酬計算は、やはり同じように何らかの「係数」が乗算されてから合計されますから、「内積」であることが多いです。
専門的には、このような「掛けてから全部足す」操作を行う演算のことを、積和演算と言います。
積和演算は、非常に多くのアルゴリズムで採用されている典型的な積分法の 1 つですので、覚えておいて損はありません。例えば、先ほどオーディオの世界で使われる例として挙げた「離散フーリエ変換」も、内積 (積和演算) を使用したアルゴリズムの 1 つです。
おわりに
本記事では、微積分が、難しいように思えて実は非常にシンプルなアルゴリズムであることを示しました。
微積分を複雑で厄介なものにしているのは、大量の公式と、それによるパズルに偏重した受験数学による功罪が大きいと筆者は考えています。
厳密な微積分では、分解能が無限だから (限りなくゼロに収束させなければいけないから) という理由もあるかもしれませんが、実用分野 (コンピューター関係) では分解能を有限として扱う「数値微分」「数値積分」が主流ですから、「ゼロ」や「無限大」に恐れる必要もありません。
「足し算と引き算の繰り返しだけ」でできる微積分は、ゲーム作者も普段何気なく使っているくらいにありふれたアルゴリズムであるのと同時に、使い方によってはかなり複雑で強力なアルゴリズムも実現できるため、微積分について理解を深めておくのは、我々ゲーム作者にとっても価値が大きいはずです。