闇の精霊の感応(仮)開発日誌 #1 (2017/12/08)

絶賛開発中です。

これからしばしば開発に関しての日誌を投稿していこうと思います。
というわけで今回は第一回。

前回の記事で紹介した通り、開発中のボス情報管理BOT「闇の精霊の感応(仮)」は
C#でコーディングし、Linux上でも動作するように.NET Core 2.0 フレームワークを使用しています。
DiscordのAPI操作についてはC#ラッパーの「Discord.Net」を使っているのですが、
「Discord.Net」は基本的に非同期処理で動作する為、制作しているBOTも非同期処理が大半を占めることになっています。
少し説明が難しいですが、非同期処理(Asynchronous Processing) とは今までプログラムを動作させるメインの仕事人(その仕事人をMain Thread(メインスレッド)と言います。)が
全て1人でやっていた作業を仕分けて雇った他の仕事人(サブスレッド、など)に仕分けた作業を分担させることです。
ゲームのプログラミングでも近年重要視されてます。(ゲームのマルチスレッド最適化がクソとか聞いたことないでしょうか?)
反対に従来の処理を同期処理と言いますが、これには問題がありました。
何が問題かというと、すべての処理が1つのメインスレッドで行われると、
処理1つずつを順に処理することになり、その中に大容量データのDLなど非常に時間の掛かる処理があるとその処理が終わるまでプログラムの処理が中断されてしまうことです。
分かりやすく言うと、従来の同期処理でプログラミングし、例えばボタンが1つあるGUIを作ってそのボタンを押すと5GBのデータをDLする、というアプリを作った場合、
ボタンを押すとそのままデータをDLし終わるまでアプリがフリーズします。(Windowsからは応答なし判定される)

GUIの描画などの処理もメインスレッドが行っているからです。
ボタンを押すとフリーズしてしばらく待たないとアプリ内の他の機能が使えないのであれば、
ユーザーにとっては非常に不便です。
なので非同期処理でデータDLなどの時間の掛かる処理を他のスレッドにさせて、
メインスレッドはGUI描画などに専念すればデータをDLしながら他の機能を提供したりできる、というわけです。

同期処理はGUIの描画処理だけでなく、ゲームプログラミングでも問題になっており、
処理が詰まってフレームレート(fps)のガタ落ちなどの問題を起こすため、
非同期処理でマルチスレッド化していかにプログラム上のボトルネックをなくしフレームレートを向上させるかがゲーム開発では大きな課題になっています。
・・・ピンときた方も居るかもですが黒い砂漠も例外ではありません。
いくら高性能なCPUやグラボを積んでも大規模な占領戦やワールドボスなどでfpsが稼げない原因は
その辺のコードの非同期処理(マルチスレッド化)が不足しているためのプログラム上のボトルネックである可能性が高いです。

が、非同期処理は同期処理より気にすべきことが多いなど、基本的にプログラミングの難易度は高いです。
仕事をしている部下を管理するように、仕事を与えたスレッドから処理結果を受け取ったり、
スレッドに仕事を与えたりなどはプログラマーがやらないといけないからです。

しかしながら、僕が使用している「Discord.Net」は天才の方が設計したおかげでそれを使用するプログラマーの自分の負担はかなり減っています。
がやはり非同期処理ならではのクセも出てしまうもので、例えばプログラム上で問題が発生した場合に起こる「例外(Exception)」の処理。
従来の同期処理では例外を適切に処理しなかった場合、アプリがクラッシュします。
そしてアプリをVisual Studioなどの統合開発環境(IDE)でデバッグして例外でクラッシュすると、コード上の問題の箇所が表示されて修正が容易くなっています。
が非同期処理だとメインスレッドで例外が発生しない限り、メインスレッド以外で例外が発生してもアプリは動き続けます。
例外が発生したことはデバッグ終了後に表示されて原因箇所のコードも表示してくれないので修正に少し手間取りました。
一見メインスレッドが無事でアプリが動き続けても、他のスレッドで例外が発生すると対象スレッドの処理が強制終了となるため、
データ不整合などのバグの温床になります。そのため、try, catchなどの例外処理のコーディングがやはり必要、というかバグの箇所も特定するために必須事項になります。

今回の日誌は実際に開発中に遭遇したバグと修正方法を紹介します。

1、マゴリアchだけボス状態を更新できない

原因:chグループの判定に頭文字1文字を使うのに2文字を入れてしまった凡ミス

現在、コマンドでのchグループの判定にSubstring()関数を使い頭文字1文字のアルファベットでchの種類(バレノス・セレンディア・カルフェオン・メディア・バレンシア・マゴリア・カーマス)を判定しています。
if (BossChannel.Substring(0, 1) == "s")

“BossChannel”は関数に渡されたコマンドでのチャンネル名(例:s4 = セレンディア4ch)、そしてそれをSubstring(0,1)とすることで頭文字1文字が”s”だったらセレンディアchというわけで
次の処理に移っているのですが、マゴリアchは頭文字がメディアchと被るので”ma”と2文字にしていました。
がSubstringの処理を”頭文字2文字が~~だったら(Substring(0,2))と変えるのを忘れていた為に、頭文字1文字で判定するのに2文字入ってるじゃん!となりif文の中の処理を行えずボス情報の更新処理ができなかったというわけです。
Substringの処理をマゴリアchのみ変更して正常動作しました。

2、沸き報告後、ボス状況の生成処理中に例外が発生しボス情報の更新ができなくなる

原因:ボス状況を保存するための一時バッファの初期化処理内の不適切なコーディング

ボスの状況を保存するために、またボスの死亡判定を実装するためにボスの種類とchごとに1つずつバッファとなる変数を定義し、ボス状況の表示が正しくなるように
そのバッファ群の値を初期値(100)に初期化する関数(InternalBufferInit() )を作っていたのですがその関数内でそのまま一気にそれらのバッファ群を初期化するようにコーディングを行った結果、
ボス状況を管理するList(BossChannelMapTable)へ値を参照しバッファとなる変数へ値を渡す過程で存在しないListの配列(List内の配列はボス出現時に生成するため、例えばクザカの配列は存在しても他のボスの配列は沸き報告が確認され状況の生成処理が行われない限り存在しない。にもかかわらず問題のバッファの初期化関数では存在しない配列番号まで参照してしまった)へ参照することになり例外が発生したのが原因でした。
対策としては、バッファのボス種類ごとに「そのボスが沸いているか?」のif文を設け、存在する配列のみ参照することにして解決しました。

(isKzarkaAlreadySpawnedが正だと既にクザカは沸いた、ということでバッファ初期化処理開始)

(問題の一つとなったListへ値を参照して取得した値が0の場合は”×”、それ以外はそのまま文字列に変換して返り値に返すValueDefine()関数)

 

次回お楽しみに(´・ω・`)ノ
PS:ブログ内のメニューに日本鯖用ボス情報共有Discordのリンクを追加しました。
闇の精霊の感応(仮)も完成の暁にはそこで稼働します