20190413のC#に関する記事は7件です。

【Unity】斜方投射の式を使って目標までの仰角を自動調整

はじめに

 Unityの物理エンジンとC#で目標までの距離に応じて仰角を自動で変えてくれる的なプログラムです。
独学なので、ソースコードの書き方等かなりガバがあるかもしれません。ご了承ください。

使用した式

斜方投射ーWikipedia
Wikipediaの斜方投射に関する式からお借りしました。
イラストが下手すぎてすみません。目標までの距離と初速度と重力加速度から、仰角θが求まるのでそれを使います。
ほうぶつ.png

ソースコードと実行結果

Luncher.csとBulletSc.csというそれぞれ発射台と弾につける二つのコードを書いてます。
乗せてるのはclassの中身だけで、とりあえず使ってないStart関数、Update関数をそれぞれ省いてます。
ランチャー側でいろいろ計算して、取得した弾インスタンスに値を渡しています。

Luncher.cs
    public GameObject Bullet;
    public float _Velocity_0;

    float _gravity = 9.8f;//重力加速度

    void Update(){
        //ランチャーの移動================================
        Vector2 pos = transform.position;
        pos.x += 0.1f * Input.GetAxisRaw("Horizontal");
        transform.position = pos;
        //================================================


        //スペースキーで弾の発射
        if (Input.GetKeyDown(KeyCode.Space)) {
            //ランチャーの座標取得
            Vector2 LuncherPos = transform.position;
            //ターゲットの座標取得
            Vector2 TargetPos = GameObject.Find("Target").transform.position;
            //ランチャーとターゲットの距離Lを取得
            float L = Vector2.Distance(LuncherPos, TargetPos);

             //Asinの中身を計算
            float AsinX = (L * _gravity) / (_Velocity_0 * _Velocity_0);
            if (AsinX >= 1) AsinX = 1.0f;//Asinの中身が1を超えるとまずいので

            //θ算出
            float _theta = 0.5f * Mathf.Asin(AsinX);
            //ターゲットとの位置関係で発射方向反転
            if (LuncherPos.x > TargetPos.x) _theta = Mathf.PI - 0.5f * Mathf.Asin(AsinX);


            //弾インスタンスを取得し、初速と発射角度を与える
            GameObject Bullet_obj = (GameObject)Instantiate(Bullet, transform.position, transform.rotation);
            BulletSc bullet_cs = Bullet_obj.GetComponent<BulletSc>();
            bullet_cs.Velocity_0 = _Velocity_0;
            bullet_cs.theta = _theta;
        }    
    }
BulletSc.cs
public float Velocity_0, theta;

    Rigidbody2D rid2d;
    void Start() {
        //Rigidbody取得
        rid2d = GetComponent<Rigidbody2D>();
        //角度を考慮して弾の速度計算
        Vector2 bulletV = rid2d.velocity;
        bulletV.x = Velocity_0 * Mathf.Cos(theta);
        bulletV.y = Velocity_0 * Mathf.Sin(theta);
        rid2d.velocity = bulletV;
    }

実行結果GIF 赤い四角がターゲットになります。
Qiita_1.gif
ターゲットとの位置関係で発射方向の切り替え
Qiita_2.gif

最後

発射した後の弾の処理はまったく考慮してないので、そのあたりはご了承ください。
今回の処理自体、汎用的というより用途はかなり局所的なものだと思います。何かの役に立ててもらえれば幸いです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

プログラム勉強_01_ITパスポート終了試験フィードバックとC#プログラム_20190413

0. 前書き

0.1 ITパスポート終了試験1回目のフィードバック

  • 独習ゼミ(サイト)を利用し、終了試験を受け、今三回やったが、全部不合格である。
  • 明日、4回目に受けるつもりであり、今日は1回目のフィードバックをする。

0.2 C#プログラム勉強のまとめ_20190413

1. ITパスポートのフィードバック

  • 終了テスト 20190225
問3 UNIXのファイル管理システムについて、適切なものはどれか。
間違い 所有者だけがホームディレクトリへのアクセス権があるため、複数のユーザでファイルを共有する場合は、ホームディレクトリは使用できない。
正解 ファイルシステム全体を1つのディレクトリツリーで管理しているため、ネットワーク経由のリモートファイルもディレクトリを指定してアクセスする。
問5 JANコードの特徴を説明したものはどれか。
間違い メーカでの製造工程で、コードを商品に印刷するインストアマーキング方式である。
正解 国際的な共通商品コードで、エラー検出用のチェックディジットを持っている。
問8 図のDFDで示された業務Aに関し、次の記述中のaに入れる字句として、適切なものはどれか。ここで、データストアBの具体的な名称は記載していない。
業務Aでは、出荷の指示を行うとともに、aなどを行う。
間違い 部品関連のデータストアから注文のあった製品の構成部品情報を得て、必要部品の所要量の算出。
正解 製品関連のデータストアから、注文のあった製品の価格情報を得て、顧客の注文ごとの売上の集計。

fullsizeoutput_644.jpeg

問14 次の関係データベースの操作のうち、射影はどれか。
間違い 複数の表からの抽出結果を結合して、1つの表にする。
正解 表の中から特定の列を指定して抜き出す操作。
問16 MACアドレスに関する記述のうち、適切なものはどれか。
間違い 国別情報が含まれており、同じアドレスをもつ機器は各国に1つしか存在しないように割り当てられる。
正解 同じアドレスをもつ機器は世界中で1つしか存在しないように割り当てられる。
問19 下記の前期実績に対し、当期は仕入原価の低減によって変動費率が5%下がり、経費節減などで固定費を500万円削減できた。
当期利益を2000万円とする目標売上高は何万円か。
間違い 8800
正解 7500
問19 前期実績
売上高 8000万円
総費用 65000万円
(内訳 変動費:2000万円、固定費:4500万円)
利益 1500万円
  • 売上高の計算式
    • 売上高 = 変動費 + 固定費 + 利益
    • 売上高 = (売上高 * 変動費率) + 固定費 + 利益
問26 サービスサポートにおける管理機能のうち、ハードウェア、ソフトウェアといったIT資産を網羅的に洗い出し、IT資産の管理台帳に記録し管理するものはどれか。
間違い リリース管理
正解 構成管理
問27 マトリックス組織を説明したものはどれか。
間違い 特定の課題に必要な人材を各部門から集めて編成し、期間と目標を定めて活動する一時的かつ柔軟な組織。
正解 構成員が、特定の事業を遂行する部門と自己の専門とする機能部門の両方に所属する組織。
問31 バランススコアカード(BSC)の4つの視点のうち、財務、内部業務プロセス、学習と成長の他に、もう1つはどれか。
間違い コミュニケーション
正解 顧客
問35 関係データベースで管理している”入館履歴”表と”建物”表から、建物名’東館’を条件に抽出した結果を日付の降順でソートしたとき、2番目のレコードの社員番号はどれか。
間違い S0003
正解 S0004

fullsizeoutput_645.jpeg

問40 フェールセーフに関する記述のうち、適切なものはどれか。
間違い ユーザが間違った使い方をしても、システムが誤作動しないようにする。
正解 機器の故障やユーザが操作ミスをしても、安全だけは確保する。
問46 フェールセーフの説明として、適切なものはどれか。
間違い 障害が発生した際に、正常な部分だけを動作させ、全体に支障を来さないようにする。
正解 故障や操作ミスが発生しても、安全が保てるようにしておく。
問48 セルB2~D100に学生の成績が科目ごとに入力されている。セルB102~D105に成績ごとの学生数を科目別に表示したい。
セルB102に計算式を入力し、それをセルB102~D105に複写する。セルB102に入力する計算式はどれか。
間違い 条件付個数($B2~$B100,=A$102)
正解 条件付個数(B$2~B$100,=$A102)

fullsizeoutput_643.jpeg

問49 組み立て生産される製品W, X, Y, Zの1個当たりの利益、1個当たりの組立作業時間、組み立て作業1分当たりの利益、1週間の最大生産可能数は表のとおりである。
1週間の利益を最大にするように生産計画を立てるとき、製品Zの生産個数は幾つか。ここで、1週間の総組立作業時間は40時間であり、製品W, X, Y, Zの全てを生産する必要はなく、同時には1つの製品しか組み立て生産できないものとする。
間違い
正解 20

fullsizeoutput_647.jpeg

2. C#プログラムに関するまとめ_20190413

2.1 代表的な標準書式指定文字

書式指定文字 表示方法
C(またはC) 通貨
D(またはd) 整数の10進数表示
E(またはe) 浮動小数点数の指数表示
F(またはf) 標準の浮動小数点数表示
G(またはg) EまたはFの短い表示
N(またはn) 桁で区切られた浮動小数点数表示
P(またはp) パーセント表示
X(またはx) 整数の16進表示

2.2 C#の整数型

ビット長 範囲
sbyte 8 -128~127
byte 8 0~255
short 16 -32768~32767
ushort 16 0~65535
int 32 -2147483648~2147483649
uint 32 0~4294967295
long 64
ulong 64

2.3 MathクラスのPowメソッド

  • Microsoftにより、「指定の数値を指定した値で累乗した値を返す」である。
  • public static double Pow (double x, double y);
  • パラメータ
    • X:累乗対象の倍精度浮動小数点数
    • y:累乗を指定する倍精度浮動小数点数
    • 返却値:数値XをYで累乗した値

2.4 decimal型を取り扱い注意

  • decimal型の変数に値を代入するとき、「m」または「M」を数値につける必要がある。

2.5 char型を取り扱い注意

  • char型はUnicode文字型である。
  • 文字リテラル(文字そのもの)は、シングルクォート(’)で囲む。

2.6 ToStringメソッド

  • ToStringメソッドは、オブジェクト(変数)の内容を表す文字列を返すものである。
  • .Net Frameworkの全てのクラスが備えている。

2.7 リテラル

  • 整数型サフィックス
データ型 サフィックス
long Lまたはl
uint Uまたはu
ulong ULまたはul
  • 実数型サフィックス
データ型 サフィックス
データ型 サフィックス
float Fまたはf
double Dまたはd
decimal Mまたはm
  • Object.GetTypeメソッド
    • Microsoftにより、「現在のインスタンスの Type を取得する」である。
    • public Type GetType ();
    • 戻り値:Type(現在のインスタンスの型を取得する)

PS:

  • Markdownスクリーンショットの貼り方

スクリーンショット 2019-04-15 18.59.58.png

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

プログラム勉強_01_ITパスポート終了試験フィードバックとC#プログラム_20190413(未完成)

0. 前書き

0.1 ITパスポート終了試験1回目のフィードバック

  • 独習ゼミ(サイト)を利用し、終了試験を受け、今三回やったが、全部不合格である。
  • 明日、4回目に受けるつもりであり、今日は1回目のフィードバックをする。

0.2 C#プログラム勉強のまとめ_20190413

1. ITパスポートのフィードバック

  • 終了テスト 20190225
問3 UNIXのファイル管理システムについて、適切なものはどれか。
間違い 所有者だけがホームディレクトリへのアクセス権があるため、複数のユーザでファイルを共有する場合は、ホームディレクトリは使用できない。
正解 ファイルシステム全体を1つのディレクトリツリーで管理しているため、ネットワーク経由のリモートファイルもディレクトリを指定してアクセスする。
問5 JANコードの特徴を説明したものはどれか。
間違い メーカでの製造工程で、コードを商品に印刷するインストアマーキング方式である。
正解 国際的な共通商品コードで、エラー検出用のチェックディジットを持っている。
問8 図のDFDで示された業務Aに関し、次の記述中のaに入れる字句として、適切なものはどれか。ここで、データストアBの具体的な名称は記載していない。
業務Aでは、出荷の指示を行うとともに、aなどを行う。
間違い 部品関連のデータストアから注文のあった製品の構成部品情報を得て、必要部品の所要量の算出。
正解 製品関連のデータストアから、注文のあった製品の価格情報を得て、顧客の注文ごとの売上の集計。
問14 次の関係データベースの操作のうち、射影はどれか。
間違い 複数の表からの抽出結果を結合して、1つの表にする。
正解 表の中から特定の列を指定して抜き出す操作。
問16 MACアドレスに関する記述のうち、適切なものはどれか。
間違い 国別情報が含まれており、同じアドレスをもつ機器は各国に1つしか存在しないように割り当てられる。
正解 同じアドレスをもつ機器は世界中で1つしか存在しないように割り当てられる。
問19 下記の前期実績に対し、当期は仕入原価の低減によって変動費率が5%下がり、経費節減などで固定費を500万円削減できた。
当期利益を2000万円とする目標売上高は何万円か。
間違い 8800
正解 7500
問26 サービスサポートにおける管理機能のうち、ハードウェア、ソフトウェアといったIT資産を網羅的に洗い出し、IT資産の管理台帳に記録し管理するものはどれか。
間違い リリース管理
正解 構成管理
問27 マトリックス組織を説明したものはどれか。
間違い 特定の課題に必要な人材を各部門から集めて編成し、期間と目標を定めて活動する一時的かつ柔軟な組織。
正解 構成員が、特定の事業を遂行する部門と自己の専門とする機能部門の両方に所属する組織。
問31 バランススコアカード(BSC)の4つの視点のうち、財務、内部業務プロセス、学習と成長の他に、もう1つはどれか。
間違い コミュニケーション
正解 顧客
問35 関係データベースで管理している”入館履歴”表と”建物”表から、建物名’東館’を条件に抽出した結果を日付の降順でソートしたとき、2番目のレコードの社員番号はどれか。
間違い S0003
正解 S0004
問40 フェールセーフに関する記述のうち、適切なものはどれか。
間違い ユーザが間違った使い方をしても、システムが誤作動しないようにする。
正解 機器の故障やユーザが操作ミスをしても、安全だけは確保する。
問46 フェールセーフの説明として、適切なものはどれか。
間違い 障害が発生した際に、正常な部分だけを動作させ、全体に支障を来さないようにする。
正解 故障や操作ミスが発生しても、安全が保てるようにしておく。
問48 セルB2~D100に学生の成績が科目ごとに入力されている。セルB102~D105に成績ごとの学生数を科目別に表示したい。
セルB102に計算式を入力し、それをセルB102~D105に複写する。セルB102に入力する計算式はどれか。
間違い 条件付個数($B2~$B100,=A$102)
正解 条件付個数(B$2~B$100,=$A102)
問49 組み立て生産される製品W, X, Y, Zの1個当たりの利益、1個当たりの組立作業時間、組み立て作業1分当たりの利益、1週間の最大生産可能数は表のとおりである。
1週間の利益を最大にするように生産計画を立てるとき、製品Zの生産個数は幾つか。ここで、1週間の総組立作業時間は40時間であり、製品W, X, Y, Zの全てを生産する必要はなく、同時には1つの製品しか組み立て生産できないものとする。
間違い
正解 20

2. C#プログラムに関するまとめ_20190413

2.1 代表的な標準書式指定文字

書式指定文字 表示方法
C(またはC) 通貨
D(またはd) 整数の10進数表示
E(またはe) 浮動小数点数の指数表示
F(またはf) 標準の浮動小数点数表示
G(またはg) EまたはFの短い表示
N(またはn) 桁で区切られた浮動小数点数表示
P(またはp) パーセント表示
X(またはx) 整数の16進表示

2.2 C#の整数型

ビット長 範囲
sbyte 8 -128~127
byte 8 0~255
short 16 -32768~32767
ushort 16 0~65535
int 32 -2147483648~2147483649
uint 32 0~4294967295
long 64
ulong 64

2.3 MathクラスのPowメソッド

  • Microsoftにより、「指定の数値を指定した値で累乗した値を返す」である。
  • public static double Pow (double x, double y);
  • パラメータ
    • X 
    • y
    • 返却値
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ASP.NET CoreとVue.jsとHTTP Streamingでオンラインゲーム作ってみたおはなし

あすかです。ニャーン。

ちょっとなんか作っていたので、技術的にどうのこうのってのをちょっとお話してみようと思いますー。

作ったもの

三国志NET KMY Versionというゲームです。

10〜13年前にも同名のゲームがありましたが、あれはあすかが中学生の時に原作から引っ張って運営してた別のやつです。
今回はイチから作り直して、システム的には昔とはほぼ別物になってます。

後述しますが、三国志NETは昔はやっていたゲームです。当時がなつかしいなーと思って、思わず作ってしまいました。
今は他のサーバで仲良くなった人や、旧KMYをプレイしていた人にテストをお手伝いしてもらってる感じですー。

ソースコードはGitHubで公開しています。

分類 GitHubリポジトリ 言語 フレームワーク 開発環境
サーバサイド GitHub C# ASP.NET Core 2.1、EntityFrameworkCore VS4M '17/'19
クライアントサイド GitHub TypeScript Vue.js VSC

最初はDDDで作ろうと思ってましたが、DDDでデータベースを扱うのは難しいなーと思ったので、途中からトランザクションスクリプトに変えてあります。
コードにはいろいろ見苦しいところが多いのです‥‥。

image.png

image.png

CGIゲームとしての三国志NETとは

三国志NETはもともと、maccyu氏の制作した、Perlで記述された2000年代のCGIゲームです。
協力型・コマンド先行入力型のオンライン戦略シミュレーションゲームです。

コマンド先行入力型って今どきのゲームではめったに見かけませんよね。
例えば、「2018年4月1日19時00分」という項目に「農業開発」コマンドを設定します。
すると、その日付と時刻になったら、コマンドが実行され、都市の農業が上がります。
コマンドは、あるところでは60分、あるところでは30分などといった間隔で入力することができます。
KMYは10分間隔です。(俗に10分鯖と呼ばれます)

コマンドをあらかじめ入力することができるので、忙しくても1日3〜5分程度のログインで十分プレイ可能だったりする反面、
コマンド実行までタイムラグがあるため、先の結果が予想しづらい、人によっては待たされている感じがする部分もあるかもしれません。
また、仕組み的にログイン時間の長い人(俗にONの多い人、廃ON、玉葱)が有利になるので、武将能力以上にONが重要となる状況も出てきたりしてて、それもデメリットの1つに数えられます。
10分更新だと展開も速くなるので、本当に忙しい人にはつらかったりしますが。。

今の世代の人たちがどれだけ知っているかはわかりませんが、
当時を知る人たちにとってはなつかしいんじゃないでしょうか。

そのゲームスクリプトは誰でもダウンロードできるものでしたから、
それぞれがダウンロードして、思い思いの改造をして公開していた感じです。

2006年から2009年ころにかけて、一部の人の間で流行していました。
今も残っている伝説秘密Nika、今は人数ほぼないけどリベラ、一休、黒子、そのほかにも、幻想、NEO、蒼天、圭、狂、大人、そしてあすかが10〜13年前くらいに運営していた旧KMYなどなど、いろいろなサーバがあり、それぞれが独自の改造をして世界を創っていました。

似たようなもの(配布型CGIゲーム)に、TOWN、商人物語、罪と罰などなどがありました。
当時はCGIゲームが人気の時代でした。

今回の三国志NETを技術的に以前のものと比較すると

まず、今回作った三国志NETは、CGIゲームではないです。

maccyu氏の制作したスクリプト(以下「原作」)を参考に、ゲームシステムやいろいろな計算式をもってきはしましたけど、プログラム自体は一から組み直したものになります。
ASP.NET Coreアプリケーションサーバとして、MariaDBとつなげて開発しました。

開発

HTTP Streaming

今回作ったゲームの売りは、リアルタイム更新かもですねー。他の三国志NETにはないというか、CGIだとそもそもここまで柔軟なものは作れないと思います。
リアルタイム更新といっても、プレイヤーが動きたいときに自由に動き回れるという意味ではなく、コマンドが10分に1回ずつ実行されるんですが、その実行結果がすぐ画面に反映される。都市の内政が、画面を手動で更新しなくても反映されるんです。
ゲーム内チャット(手紙)でも同じです。誰かがチャットに書き込んだら、その中身が他の人に配信される。
戦争も同じです、誰かが攻め込んだらその結果が全員に配信されます。都市が落ちたら、他の人達の画面の地図も変わります。

三国志NETをリアルタイム更新にした理由

上にも書きましたが、三国志NETはコマンド先行入力型ゲームです。10分更新と言いましたね。あれ、1人の武将あたりなんです。
実は武将ごとに、更新時刻が違います。同じターンでも、武将Aは19時2分更新(俗に2分更新)、武将Bは19時4分更新(俗に4分更新)と、ばらばらになっています。
1人の武将は10分に1回しかコマンドが実行されなくても、たくさんの武将がいると、それだけのコマンドが順に実行されるわけです。

なので、例えば戦争中の場合、次に戦局が変わるのは10分後とは限りません。特に、国同士が潰し合って残り2国になったあとでおこなわれる最後の戦争(ラス戦)では、参加する武将の数も多いので、人数にもよりますが、30秒〜2分ごとに戦局が変わるわけです。
誰かが都市を支配して地図が変わった場合など、戦局に応じて実行するコマンドが変わる場合もあります。作戦も変わる場合があります。そのうえに、作戦を決める人は、新しい作戦を、たくさんの武将に伝える必要があります。
そういうのを加味すると、三国志NETって、常に最新の情報をとってこなければいけない。

三国志NETだけでなく当時のCGIゲーム全般の問題ですが、最新の状況を知るためには、手動で更新ボタンを押さなければいけません。
更新ボタンを押すと毎回HTML描画します。必然的に、ページのリロードが大量発生します。
それに、たとえば最新の手紙(チャット)を読みたいと思っても、それ以外のデータ、地図とか武将情報とかもいちいちダウンロードしなければいけない。このオーバーヘッドが非常に邪魔なわけです。
それに、

  • リロードした瞬間に戦局が変わったらどうするのか?
  • 新しい作戦を書こうと思って手紙を書いている途中に状況が変わっても気づかないのではないか?
  • みんなの意見を聞きながら作戦を決めるのは、10分鯖くらい展開が速すぎるとやりづらいのではないか?
  • ただでさえ長時間のONを要求されるのに、ながら作業がやりづらいのは負担に感じるのではないか?(作業途中にいちいちブラウザまでマウスカーソル移動して更新ボタン押さないといけない)

いろいろあって、三国志NETというシステム、特に展開の速い10分鯖には、リアルタイム更新が非常によく合うと思いました。

pushとpolling

サーバのデータをクライアントでリアルタイムに表示する手段として、サーバとクライアントを同期する手段は、大きく分けて2つあります。
pushとpollingです。

polling

クライアントが、3秒間隔とかでサーバにいちいち最新情報を問い合わせに行きます。TCP接続によるコストだけでなく、サーバはいちいちデータベース接続を初期化したりしなければいけないので大変です。負荷かかります。

push

サーバが自分からクライアントにデータを送る感じです。サーバが選択した必要最低限の情報しか回線に乗らないので、ネット通信のコストも抑えられます。クライアントからのリクエストもあまりないので、サーバのCPU負荷はかかりにくいですが、同時接続数を食うので、それだけメモリ消費が大きくなります。

今回はpushのほうを採用しました。ていうかもう、人数が5人とか10人とか決まっている場合はpollingでもある程度効率的なシステムは組めたと思うのですが、今回は

  • 参加人数が不定
  • 更新間隔も不定
    • 1人の更新は10分間隔だけど、人によって更新時刻が異なるため、次に状況が変わるのが何分後なのか読めない
    • 宣戦布告や同盟締結、チャットの新しいメッセージなどの情報は武将が手動で操作するため、最新情報を取得するのにpollingだと限界がある

などの条件から、pollingとして実装するメリットは1つもないと思いました。ていうかpollingに手軽・シンプル・レガシーなやつが使える以外のメリットってあったっけ。

Comet?pollingやんあれ(´・ω・`)

今回のpush通信の実装

SignalRなんてなかった

ASP.NETでリアルタイム通信と言えばSignalR!ってのがいろんなとこで言われてると思うんですが、
開発のための調査の段階では、存在そのものに気づかなかった(´・ω・`)
半年くらい下積みして、さあ作ろうってなった直前に発表された感じのやつです。

存在自体はずっと前からあったようですが、
ASP.NET Coreの機能としてMSDNに載ったのは昨年11月ころ?ってGoogleやGitHubの履歴に描いてありました。
それ以前でもどっかにドキュメント転がってたはずなんですが、
あすかの探し方が悪かったのか、まったく上がってきてなかったです。

で、今年に入ってSignalRの存在を知った頃には、もうリアルタイム更新のシステムはほぼ固まってしまったし
今のままでも安定して動作しているようですし、
今更わざわざSignalRに切り替える理由もないじゃろなーと思って今に至ります。

この手のシステムって、リアルタイム更新のコア部分を真っ先に作るんですよね‥‥汗
ただ、車輪の再発明(今回当てはまるか分からんけど)にはデメリットも多いので、いつか検討しなくちゃなーって思うところはあります。

WebSocketなんてなかった

リアルタイム通信をHTTP Streamingで実装しちゃった後に知ったんですよねこれ。
下調べの時何やってたの、としか。今回のシステムは双方向で通信をするものなので、WebSocketのほうが都合がいいかなーと思ったんですが、もういいわこれめんどいと思ってぽいしました。
会社で同じような案件が来たら挑戦してみたい‥‥。こういう案件永遠に来ないと思うけど。

SignalRは内部でHTTP Streaming、WebSocket、その他にもいろいろな通信手法から環境にあわせて選択してくれるらしいので、SignalR使えば同時に解決できる問題だと思うんですけどねー。
みんなは下調べちゃんとしよう!(教訓)

event-streamなんてなかった

今回のシステム、HTTP HeaderにはContent-Type: event-stream;なんて書いちゃっているんですけども、
event-streamってあれじゃないですか、

start:
data: [でーた]
data: [でーた]

みたいに、データの前にdata:ってつくのが本来の仕様なんですよね。
今回、経緯はよく分かりませんけど、開発の過程の中のどこで間違えたのか、data:を省略してしまい、

{"type": 12, ...}
{"type": 12, ...}

みたいに、ひたすらJSONだけを流す感じになってしまいました。
別にこのままでも使えるんですけど、event-stream本来の様式ではないので、Node.jsの各種すごいライブラリとか、あとはevent-streamを想定したHTML5 APIとかが使えないのは痛かったかなーとおもいます。

クライアント側の実装

この手の接続って、特殊なんですよね。
何時間もかけて大きなファイルをダウンロードしているのと同じ状態でして、まだ接続が完了していない状態で、それまでに送られてきた情報を見なくちゃいけない
これ、jQueryやaxiosではもちろんできません。対応していません。
じゃあクライアントではどうやって受信しているかというと、やっぱりXMLHttpRequest直打ちしかなかったです。

    // ※シンプルに読めるようにするため、あえて古いバージョンのソースを載せています。
    // 最新の完全版コードはGitHubから
    // https://github.com/kmycode/sangokukmy-client/blob/master/src/api/streaming.ts

    // リクエストを受け取っていないか確認
    let length = 0;
    const ajaxTimer = setInterval(() => {
      if (this.ajax != null) {
        if (length !== this.ajax.responseText.length) {
          const updatedText = this.ajax.responseText.slice(length);
          length = this.ajax.responseText.length;

          // JSONになおしてイベント発行
          const lines = updatedText.split('\n');
          lines.filter((line) => line).forEach((line) => {
            this.output(line);
          });
        }
      } else {
        clearInterval(ajaxTimer);
      }
    }, 100);

データを改行で区切って、1行ずつ読んでいます。
しかもこれ、サーバから送られてくるデータはXMLHttpRequest.responseTextの中に文字列としてまとめて入っているんですが、
「古い情報はいらないよ、新しい情報だけ送ってくれればそれでいいよ」みたいなことはしてくれなかったです。
常に、接続開始当初からのすべてのデータがいっぺんに入れられているんですね。

なので、ストリーミングの接続時間が伸びれば伸びるほど、このresponseTextに入っている文字列もえらい長くなって、
それをsliceするときとか、えらい重くなるんじゃないかなーって心配してたりします。
sliceが内部ではヒープ確保・メモリコピーではなく、単にC#の(名前忘れた)みたいに長い文字列の一部分をさすポインタみたいなやつだったらいいんですけどなー。そうゆう仕様だったらすみません。。

今の所特にこれといった障害もなく動いてくれているのはいいんですけど、いつか送信内容を本来のevent-streaming仕様にして、それを想定したライブラリ使えるようにしたいなーってのが本音です。

サーバ側の実装

クライアントと比べると簡単です。
ストリーミングのメソッドはここにあります。https://github.com/kmycode/sangokukmy/blob/master/SangokuKmy/Controllers/SangokuKmyStreamingController.cs

まず、コントローラのメソッドにasyncをつけます。
そもそもストリーミング自体、同時に複数接続していることを前提にしているシステムですから、asyncがないと話にならない。

    public async Task StatusStreamingAsync()

次に、HTTPヘッダを送ります。

      // HTTPヘッダを設定する
      this.Response.Headers.Add("Content-Type", "text/event-stream; charset=UTF-8");
      this.Response.Headers.Add("Cache-Control", "no-cache, no-store, must-revalidate");

この手の接続ではキャッシュをとらせないようにするのが作法です。ついてにいうと、クライアント側でも乱数とか日付とかつこて毎回適当にURL変えるべきです。でないと、PCは大丈夫でもスマホで引っかかりまくりますから。(実話)

データを文字列として送信する時は、

      await this.Response.WriteAsync(initializeData.ToString());

というふうに、ResponseWriteAsyncを使います。
そしてあとは、今回のシステム内で別途作ったストリーミングクラスに接続情報を渡して、接続が閉じるまでひたすら待機します。
コントローラのメソッドをすぐに終わらしてはいけません。そんなことをしたら接続が完了しておしまいです。

      // ストリーミング対象に追加し、対象から外れるまで待機する
      var isRemoved = false;
      StatusStreaming.Default.Add(this.Response, this.AuthData, chara, () => isRemoved = true);
      while (!isRemoved)
      {
        await Task.Delay(5000);
      }

このwhileが終わったら、接続切れた後に必要な処理があればそれを実行して終わりです。
ちなみに接続が切れたかどうかを検出するには、

Response.HttpContext.RequestAborted.IsCancellationRequested

が使えます。これがtrueになっていれば、切断が検出されたということです。

ただ、これ、ユーザが実際に接続を切ってから切断が検出されるまでにいくらか空きがありまして、
あすかのスマホ(iOSのSafari)ですと、スマホの電源を切ってから(※スリープではない)、切断状態が検出されるまで10分くらい空きがありました。
過信は禁物らしいです。

余談ですが、スマホからだと、ページを開いている間、常にローディングのくるくるが回り続けるんですよね‥‥。
仕組み上仕方ないものではありますが、問い合わせがあった時にきちんと答えられるか今から不安なのです。

Vue.jsの選択

これだけのものを作るためには、まずモデルとUIをバインディングできることが重要かなーと考えました。
データを変更するだけで、画面に反映される。画面描画の手間が省略できるので、大量のデータを扱うには必要不可欠です。
特に、リアルタイム更新だと、サーバから絶え間無くデータが送られてくるわけですから、プログラマはそれをデータに保存するだけで、実際の画面処理はフレームワークがやる。
というわけで、Vue.jsに白羽の矢がたちました。

なぜjQueryだとだめだったのか

バインディング

大量のデータを扱うのにjQueryだと限界がある。jQueryだと、データが来るたびに各コントロールの更新処理を一気にやらなければいけないんです。その更新処理が非常に厄介。
例えば、誰かがどこかの都市を支配し、新しい都市データが配信されるとします。それはすなわち、その都市の所属国が変わることを意味します。
画面描画だけでも、

  • 地図上の都市の色を変える
  • その都市に所在している武将の画面では、
    • 都市情報タブの色を変える
    • 都市情報タブのタイトル、中身を変える
    • 新しい中身にあわせて、都市情報の農業・商業とかの内政進捗をあらわす青いバーの横幅を変える
    • 都市を支配している国情報タブの中身も同じように変える

というふうに、いろいろやることがあります。
しかもこれ、都市データに限った話です。実際は都市データだけではなく、情勢を伝えるログ(マップログ)、支配した武将の場合は武将行動ログ、というように、いろいろな種類のデータが一気に来るのです。それらに対して、上みたいな大変な処理をいちいちやらなければいけない。
これはもう、モデルバインディングでないと無理だと思いました。ていうか、Vue.jsじゃないと2ヶ月でここまで作れないし、今みたいな1〜2万行規模のクライアントなんてとてもできない。

セレクタ

jQueryのセレクタはたしかに便利です。特定のクラス名を持った要素を参照する、その子要素を参照する、などなど。
小規模アプリを作るならVue.jsよりもjQueryのほうが早いというのは、私はそう思います。ただ、大規模アプリを開発する時はVue.jsかなーと思います。
その理由ですが、jQueryだと、せっかくセレクタで要素をとってきても、それが期待通りのDOM構造でないと誤動作起こすんです。jQueryコードとHTML(DOM)が密接に結びつくことになるんです。これだと、HTMLを変えたくなってもなかなか自由に変えられない。
そのぶん、Vue.jsはバインディングによってデータを反映させるわけですから、HTMLの中にちょっと埋め込むだけ

<div id="current-display">
  <span class="number">{{ model.gameDate.year }}</span><span class="unit"></span>
  <span class="number">{{ model.gameDate.month }}</span><span class="unit"></span>
</div>

で、ここに年を埋め込んでくれる、ここに月を埋め込んでくれる、と決めることができます。
これだと、HTML構造を柔軟に変えられますし、DOMとjQueryのコードをいちいち見比べながら修正する必要もない。HTMLとJavaScriptの分業がしやすいです。修正コストが明らかに下がったと感じています。

リスト

配列を反復してDOM要素を生成するの、jQueryでもHTMLの要素をcloneすることによってできますね。
ただ、今回作るアプリでは、その場面が多すぎるんです。例えば。

コマンドリスト。

image.png

手紙。

image.png

ちょっと発展的な例としては、会議室。(スレッドフロート型掲示板)
スレッドもレスも全部リストです。

image.png

さらに発展的な例としては、この地図も一次元配列をバインディングすることで作っています。
都市ごとに座標を設定していますので、それをposition:absoluteとか使って配置しています。

image.png

というように、このゲームにおいては配列から作る要素があまりにも多すぎる。jQueryでもゴリ押しで作れないことはないんですけど、Vue.jsだとかなり効率的に作れると判断しました。
具体的には、コマンドリストの場合、

    <div class="command-list">
      <div v-for="command in list.commands"
            :key="command.commandNumber"
            :class="{ 'command-list-item': true, 'selected': command.isSelected, 'disabled': !command.canSelect }"
            @click="onCommandSelected(command, $event)">
        <div class="number">{{ command.commandNumber }}</div>
        <div class="command-information">
          <div class="command-helper"><span class="gamedate">{{ command.gameDate | gamedate }}</span><span class="realdate" v-if="command.commandNumber > 1">{{ command.date | realdate }}</span><span class="rest" v-if="command.commandNumber === 1">実行まであと<span class="rest-time">{{ list.secondsOfNextCommand }}</span></span></div>
          <div class="command-text">{{ command.name }}</div>
        </div>
      </div>
    </div>

みたいな感じ。v-forを書くだけのお手軽バインディングです。
jQueryみたいに、DOM要素にいちいちIDを指定する、cloneしたものをどっかに置いておく、テンプレートのHTML構成が変わったときのコード修正などを気にする必要がないです。だって変数をそのままHTMLに埋め込んで終わりですから。

このように、jQueryよりもVue.jsを選ぶメリットがあまりにも多すぎるので、短期間で手っ取り早く作るなら、jQueryと比べるならVue.jsのほうが格段にいいと判断しました。

実はVue.jsでの開発経験なかったです。会社でもなかったです。でもWPFやUWPやXamarin.Formsの経験がありますので、配列のバインディングはそれほど抵抗感じませんでした。

なぜAngularJSだとだめだったのか

何年か前挑戦して挫折したからですorz
いまだに苦手意識が‥‥

アニメーション

jQueryにはアニメーション機能もありましたよね。Vue.jsにはこれ、ないんです。
ただ、CSS3のtransitionkeyframeで大体事足りるので、結果としてjQueryなくてもいいんじゃないかなと。

jQueryに拘ってる人は、よっぽと複雑なアニメーションでもしない限り、拘ってる理由がよくわからない。
たまに、CSSでできるレベルのアニメーションをjQueryで書いている人がいるんですけど、CSSとJavaScriptの分業(画面表示と振る舞いの分離)ができたほうが何かと便利かなーと思いました。ぴよ。

Ajax

これもVue.jsにはないけどaxiosがry

Bootstrap

今回のUI制作では、Bootstrapも大きな役割を果たしています。
今回、jQueryは使っていないので、Bootstrap Nativeを使っています。CSSをCDNでつこている感じです。

レスポンシブとスマホ対応

見てのとおりです。

image.png
image.png
image.png

スマホからでも快適に見れるよう意識したんですが、ここで落とし穴が2つ‥‥

まず、このページでは、あちこちに

.outer {
  overflow: auto;
  height: 50vh;
}

みたいなCSSを入れて、ページの一部分をスクロールさせているところがたくさんあるんですけど、そこ、そのままだとスマホでスムーズにスクロールできないんです。惰性がつかない。
そういった場合には、

.outer {
  overflow: auto;
  height: 50vh;
  -webkit-overflow-scroll: touch;
}

をつけなければいけないんです。これちょっとはまりました。

もう1つですけど、iOSのSafariだけの問題かもしれませんが、ページの一部分だけをスクロールできるようにした場合、その部分をスクロールする時にちょっとしたコツが必要みたいな感じです。
いずれposition: stickyをつこて、一部分だけスクロールそのものをなくそうとは思っているんですが、いやーちょっと不便ですね。慣れが必要です。

タッチデバイス考慮したUI

徴兵画面を例にとりますけど、今までの三国志NETで、徴兵画面のUIはこうなっています。

image.png

これ、PC使うんなら最悪このままでも問題はないかなーと思うんですけど、
スマホからだと、キーボード入力がめんどい。(あすかだけかもしれませんが)
とにかくキーボード入力がめんどい。
いやもうキーボード入力がめんどい。

キーボード入力なしで大抵のことができたりしないかなーと思って考えたのがこれです。

image.png

兵種を選択して、
三国志NETって、徴兵には大体5パターンくらいありまして、

  • ALL徴兵(徴兵できる数は武将によって変わるが、その武将が徴兵できるぶんだけを全部徴兵する)
  • 末尾9徴兵(最大141人徴兵できる場合は139とかにする。徴兵で都市民忠が減少するが、その計算式を考慮した徴兵方法)
  • 9人徴兵(民忠が全く減らない最大の数)
  • 1人徴兵(多人数でいわゆる「守備ループ」を行うときに多用)
  • ALLからちょっと控えめの徴兵(所持金が残り少ない時によくやる)

このうち最後はさすがにどうしようもないですが、それ以外の4つについては、ボタンをワンタッチで選べるようにしたみたいな感じです。

しかしスマホの文字入力やっぱり慣れない‥‥

がちのデザイナから見るとこれでもツッコミ所満載かもしれませんが、とにかくこういう工夫をしましたよと。
こういうUIも簡単に作れます。Bootstrapならね。

ゲームシステムの工夫(特筆点のみ)

Infinite Scroll

ツイッターとかで、下までスクロールすると古いのが自動でロードされて、そのままスクロール続くあれです。
Infinite Scroll(無限スクロール)という名前があります。

あれ、最近のUIのはやりなんでしょうか、PCからもスマホからもわりと便利です。
古いログを見るボタンを押さなくても、スクロールするだけでいい、お手軽です。

最初はこんなだったのが

image.png

下へスクロールし続けていくとこうなったり。スクロールバーの長さが変わってますねー。

image.png

見た目難しそうですが、わりと簡単に実装できます。
Vue.jsですと、scrollイベントと連動させます。

<div ... @scroll="onCharacterLogScrolled($event)">

実装の方はこないなってます。

  private onCharacterLogScrolled(event: any) {
    if (this.isScrolled(event)) {
      this.model.loadOldCharacterLogs();
    }
  }

  private isScrolled(event: any): boolean {
    // スクロールの現在位置 + 親(.item-container)の高さ >= スクロール内のコンテンツの高さ
    return (event.target.scrollTop + 50 + event.target.offsetHeight) >= event.target.scrollHeight;
  }

スクロールイベントと連動させて、モデルのメソッドを呼び出す感じです。
モデルのほうも、Ajaxでデータとってきて配列の末尾にpushするだけです。Vue.jsのバインディング便利。

AI国家

AIといっても、さすがに最近はやりの機械学習や深層学習ではないです。手書きです。
三国志NETはもちろん人間が主体になってやるゲームですが、災害やイベントとかで発生するものにつけてみたかった、みたいなやつです。
具体的に言うと、農民反乱、異民族、蛮族。

農民反乱と異民族は、あすかが中学生の時に運営していた旧KMYにもありましたが、
あれは、単に都市が無所属になるだけで終わりです。何の面白みもないかなーと思いました。
なので、今回はAIを作って乗せてみました。AIが自動で宣戦布告して、がおー☆しちゃう感じです。

戦略シミュレーションゲームのAIって、30年以上戦略シミュレーションゲーム作ってる某企業の製品でもいろいろ批判されたりするあたり、
人間に勝てるものを作るのはプロでも難しいのかなーって印象持っています。

三国志NETももちろん例外ではなく、コンピュータに人間の行動をある程度プログラミングしているものの、
状況に応じた柔軟な行動ができない、いつもワンパターンになってしまうのが難点です。
なので、今回はアルゴリズム以前に、アルゴリズムを組むための考え方を簡単に紹介するだけにとどめます。

今回の戦略シミュレーションAIは、処理の流れが大きく分けて2つあります。

  • 国全体の思考
  • 個人の思考

この2つに分けて説明してみます。

国全体の思考

その国が、全体として何を目標にしてどの行動をとるべきかを考えます。
個人の状態にととまらない、できる限り広い視点で考えます。
といっても現在は、(あすかの技術力不足や開発時間不足もあって)これしか決めていないのが現状です。

  • 自国のメイン都市
  • 自国の前線都市
  • 相手国の目標都市
  • 相手国の次に攻める都市

HoI4のアレを参考にAI作ってみました的なやつです。
戦争の時、まず相手国で一番大きいというか、栄えていると思われる都市を目標に設定します。(もちろんこの都市を奪っただけで戦争が終わるとは限らないので、その時はまた別の目標を設定します)
そして、その目標都市が自国と隣接しているとは限らないので、経路探索して隣接していない場合は、別の都市を「次に攻める都市」に指定します。
そして、そこに隣接する自国の都市(正確に言うと、メイン都市から目標都市までの最短経路上に乗る都市)を、自国の前線都市に指定します。

自国の武将は、前線都市に集まって、次に攻める都市を攻めていく形になります。

これ、欠点としては、三国志NETってゲリラ戦もよく発生するんですよね、あとは「裏抜き」といって、相手国の目標都市以外の城壁を削って、相手の逃げ道を塞ぎやすくする作戦。それに対応しぎれてないかなーみたいなとこがあります。(裏抜きは旧KMYでは聞いたこと無いので、もしかしたら翼だけの用語かもしれない)
また、堀といって、あえて人口の極端に少ない都市を作る作戦ありますよね、それもやっていません。それらは三国志NETのプレイヤーの間ではもう手法として確立されていて、AIとして実装するのは時間の問題やろみたいなところがあるので、今後の課題です。

これは国全体の行動に結びつくので、目標都市が頻繁に変わるようなことがあってはいけません。
作戦をDBに保存しといて、不定期的に見直すことで、なるべく行動に一貫性をもたせ、かつ最新の戦局に対応できるようにしています。

個人の思考

国全体の思考の結果を踏まえまして、今度は個人レベルでコマンドを決めます。
国全体の目標をまず見ますが、個人って、所持金とか兵士数とか管理しなければいけなものがいろいろある。
自分は今、指定された都市に侵攻できる状況になっているのか。兵士が足りなければどこで徴兵するか、そもそも戦争中でなければどのコマンドを実行するのか。

国全体の方針は結構、それにあわせて自分はどのように動くべきか。それを決めて、具体的な行動に落とし込みます。

まとめますと、まず「全体」で物事を考えて、次第に考える範囲を狭めていく感じのロジックになっています。

地図の自動生成

※画像は手動で作った地図です><
image.png

三国志NETって、どこかの国が統一した時にリセットということが行われます。すべての武将、国、ログなどが初期化され、また一からやり直しになります。リセットしてから統一するまでのサイクルを、「期」と呼んでいます。第1期、第2期、‥‥のように言います。
で、リセットしても、地図上の国は消えますけど、地図の形は同じです。毎回同じなんです。なので、この都市は守りやすいとかで、建国される都市が毎回偏ってしまうわけです。建国位置がワンパターンになることは、外交関係や、毎回の戦争の作戦などもある程度規則に縛られてしまう。
もちろん建国位置は毎回微妙に変わってきますので、この状態でもリプレイアブルかといえばそうなんですけど、やっぱり地形も毎回変えたい。
でも、地形を毎回自分で考えるのも大変。ということで、機械に任せることにしました。Civとかでもある地図の自動生成です。

今回のプログラムでは、「とりあえずランダムに都市を配置しておいて、全体ができた後から、この地図でいけるかどうか判定する。だめだったら最初から作り直し」みたいなやり方にしています。(あとあとのアプデで変わることもあります)

三国志NETに必要な地図

三国志NETの経験のある方はお分かりだと思いますけど、例えば

image.png

こういう都市配置は、攻勢にとっては嫌われる。なぜなら、守る方は中央の都市に全員集まってひたすら守ったり攻撃できたりするのに対し、中央の都市を攻める方はこの都市を攻めるだけでなく、カウンター(自国の都市が奪われた時に取り返すこと)も意識しなければいけない。
守るほうが有利で、攻めるほうが不利。この地形になっただけで、戦争の勝率が最初からはっきりしてしまっている感じ。

地図生成でこのような都市ができないようにするため、このルールを一般化しまして、

  • 隣接都市グループは1つのみとする

とおきます。
ここで、「ある都市に隣接する都市集合において、互いに隣接している都市集合を1グループ」と定義します。
つまり、上の場合、

image.png

中央の都市には、橙色と緑色の2つの隣接都市グループが存在します。2つのグループがあるわけですので、これはだめ、ということになります。
このルールに従うと、

image.png

こういう、都市が2つ以上直線につながった地形もできなくなりますよね。
ただ、これはこれでいいんですけど、例えば

image.png

こういう地形でやりたくなることもたまにあるんですよねー。個人的な好みですけど。
この場合、中央から見て上・右・左・下が守りやすい都市になってしまいますし、上の条件も満足しないけれど、なんか見てて面白そうだなーと思ったので例外的にこれはOKにすることにしました。
これを一般化すると、

  • すべての隣接都市グループが、共通の隣接都市を持つ

となりました。

image.png

赤い都市の隣接都市グループは、橙、緑の2つありますね。この2つのグループが、共通の隣接都市である黄を持ちます。この場合は例外的にOK、というプログラムを組みました。

このほかにも、

  • 都市が6以上9以下の場合、自分を除いた存在するすべての都市に隣接する都市は存在しない

だめな例
image.png

  • すべての都市において、隣接都市が8未満である

だめな例
image.png

といった条件を設けています。
やっぱり三国志NETって都市配置が重要ですよねー。ぴよ。

こんな感じで作った地図がこちら。

image.png
image.png
image.png

都市少ないほど条件が相対的に厳しくなるので仕方ないっちゃ仕方ないんですけど、パターンが限られてますね。他にもいくつかパターンはありますけど。あとはこれを回転したり反転したりする程度。

都市名

都市名は悩みました。機械学習の導入も検討しましたが、最後にはもう適当でいいやってことにしました。
適当なので、

image.png

これをそのまま、生成した地図にかぶせます。
地図は生成した後中央に寄せられるので、雒陽(洛陽。後漢の光帝が五行における水の気を嫌い雒陽と改称したが、魏の時代に戻された)周辺は常に出てくるかんじになりますー。
これも後から改良するかもしれないししないかもしれない。ぴよ。

三国志の地図って正方形ではなく、雒陽の左上あたりがぽっかりあいてるんですよねー。無理矢理正方形におさめました。はい。思いっきり歪んでるけどな。
朝歌で商復興プレイしたいのう‥‥

配置(継続的インテグレーション)

これって、アプリケーションサーバなんですよね。CGIは作ったらもうFTPにぽいで終わりですけど、今回はASP.NET Coreで作ってますから

  • アプリをビルド
  • 現在稼働中のサーバを停止
  • アプリをサーバ(EC2)に配置
  • サーバを起動

みたいなのをいちいち踏まなければいけない。これ、毎回手でやっていたら死にます。まぢです。
特にあすか、WebアプリとしてはCGI時代の三国志NETくらいしか運営経験ないんですよねー。毎回いちいちめんどいデプロイ処理を手動でやっていたら、いつか飽きるかなーと思いました。
そら企業ではなく個人の趣味としてやってるわけですから、誰かに投げられるわけでもないですし。

そこで、今回は継続的インテグレーション(CI)をつこてみました。

AWS CodePipeline

サーバサイドではこれを使いました。
詳しくはASP.NET CoreアプリをGitHubにpushしたら自動でAWSにデプロイされるようにする(EC2、CodeBuild|Deploy|Pipeline)に書いてありますけれども、この他にも

  • DBサーバのポート番号変更
  • DBサーバをSSL化して外部と接続
  • Let's Encrypt自動更新対応
  • Apacheとリバースプロキシ
  • ApacheをSSL化

などなどの設定をしています。
いやー便利。

クライアントにも、あらかじめ「ストリーミング接続が切断されたら、自動で再接続を試みる」処理を入れていたおかげで、
アプデの時に鯖落ちしても、自動でつながるようになりました。
ユーザに事前にいちいちページのリロード促さなくてもよくなるのと、その促す書き込みすらユーザは気づかないかもしれないのでー、リアルタイムで更新されるタイプのシステムでは必須の処理かもしれません。

Netlify

クライアントサイドではこれ使いました。まぢ一発で便利。お手軽。

保守

実際にあった事例

MacのEFCoreでDBにawaitで接続するとなんか遅い

実環境はAWS EC2のUbuntuですが、開発環境はmacです。
開発しているわけなんですが、どうにもasyncawaitをつこたDB接続が遅い。

DBへの接続、開発当初は同期接続で、後から非同期接続にしたんですが、
非同期接続にしてから、とにかく遅い。単純なCRUD程度の簡単な処理に最低でも1秒待たされる。
ブラウザの複数タブを開いて同時に接続しようとしたら、その分だけ(3〜5秒、ひどい時は10秒)待たされる。

Windowsや実環境ではなぜか高速に動くんですよね。
あれ何ででしょう。設定全く同じなのに‥‥。
Ubuntuでは、アプリもDBもサービスとして動いているんですが、関係あるのかな‥‥。むむ。

実環境に配置するまでわからないという知見でした。

リアルタイム更新ができていないバグを知識のない人に説明するのは意外と大変だった

運営している折、

援軍機能というのがあって、君主と軍師の2人は、特定の武将に援軍要請を送ることができます。
援軍要請した武将のところには、「援軍要請撤回」というボタンが現れて、それでこの武将にはすでに援軍を要請済だってのがわかるようになっています。

で、君主が援軍要請をしたら、君主と軍師の画面で同時に援軍要請撤回ボタンが現れる、っていうのが本来の仕様なんですけど、
君主が援軍要請しても、軍師の画面にボタンが現れない。
それで軍師が、この武将にはまだ援軍要請していないと勘違いして、要請ボタンを押したらエラーが出てきたという。

この現象の原因そのものではなく、どうしてこういう表示になってしまうかの理屈を、リアルタイム更新に慣れているみなさんに説明するのが意外と大変でした。
あすかの観測範囲だとリアルタイム更新なブラウザゲームなんてあまり見かけないし、あっても大抵は企業が運営するゲームで、隅から隅までしっかりテストされているからでしょうか。
みなさんにとっては、これはもうめったに見かけないバグだったのかもしれないです。

async、awaitの書き忘れによるデータベースエラー

ある日、武将更新処理の途中に、こういうエラーが出て更新が止まりました。
わかりやすいように改行しています。

2019-04-08 03:22:14.2514||ERROR|SangokuKmy.Startup|更新処理中にエラーが発生しま>した 
System.InvalidOperationException: A second operation started
on this context before a previous operation completed. This
is usually caused by different threads using the same instance
of DbContext, however instance members are not guaranteed to
be thread safe. This could also be caused by a nested query
being evaluated on the client, if this is the case rewrite
the query avoiding nested invocations.

DBサーバを再起動したら更新また動くようになったのですが、またしばらくしたらこのエラーが起こる‥‥1日に1回位の頻度で起こる。
というわけで、最初はDBが原因だと思ってしまったんですが、やっぱ上のエラーメッセージに書いてあるとおり、プログラムが原因でした。

こうゆう非同期エラー系は、どこが原因でこうなったのか具体的に書いていない場合が多いんです。
今回のエラーもその一例でして、このエラーが最初に出た時刻は大体分かっていましたから、その直近のコミットを調べてみましたところ‥‥
こんなプログラムがありました。(Someメソッドは自作したOptional的なやつです)

      (await repo.Town.GetByIdAsync(character.TownId)).Some(async (town) =>
      {
        // 自国の都市であれば、同じ国の武将に、共有情報を通知する(それ以外の都市は諜報経由で)
        if (town.CountryId == character.CountryId)
        {
          await StatusStreaming.Default.SendCountryAsync(ApiData.From(town), character.CountryId);
        }

        // 同じ都市にいる他国の武将にも通知
        // (自分が他国の都市にいる場合は、都市データ受信のさいは、自分もここに含まれる)
        var charas = (await repo.Town.GetCharactersAsync(town.Id))
          .Where(c => c.CountryId != town.CountryId);
        await StatusStreaming.Default.SendCharacterAsync(ApiData.From(town), charas.Select(c => c.Id));
      });

      await repo.SaveChangesAsync();

      // なんとかの処理

      repo.Dispose();

なるほどこれは。asyncのついたラムダ式を待機してなかったせいで、repo.SaveChangesAsync()repo.Town.GetCharactersAsyncが同時に実行される可能性がありますね。それであんなエラーが出たわけです。

これね、対応は探すのに時間がかかった割には単純で、11文字追加するだけでした。

      // 修正前
      (await repo.Town.GetByIdAsync(character.TownId)).Some(async (town) =>

      // 修正後
      await (await repo.Town.GetByIdAsync(character.TownId)).SomeAsync(async (town) =>

非同期処理って、なかなか的確な位置を示してくれないので対応に困ることも多いんですよねー。
今回は、このエラーの初めて出た時期が把握できていたのでgitのコミット履歴見て、思ったよりもわりとあっさり解決できましたが、これからまたこういう問題が出たらと思うと心配‥‥。なのだ。

EFのAddRangeAsyncでデータが順番に追加されるとは限らない?(現在進行中)

戦闘って、最大50ターンの間、こういうログが続けて出るわけです。
image.png
この順番がおかしい、ばらばらになることがある、との報告をわりと頻繁にいただきます。
調べてみたんですが、どうにもDBの並びからしておかしい!
と思っていろいろやってみたんですが、

    await this.Context.CharacterLogs.AddRangeAsync(logs);

このAddRangeAsyncメソッドは、与えられたデータを順番通りにDBに追加することを保証しない疑惑が出てまいりました。

ドキュメント調べたんですが、いまだにそのような記述は発見できず‥‥
でも、今はとりあえず、そういう前提で対策考えることにしました。

ログには時刻も記録されてるので、時刻でソートできないかなーと思っているとこであります。(まだ対応完了してない)
時刻でだめだったら、ログにOrderカラムをつけるしかないのかなあ。

まとめ

技術の発達により、10年前は作れれなかったものが、今は誰でも手軽に作れるようになってるなーと感じます。
みなさんもこの機会に、昔やっていたゲームとかを今の技術で組み直すとかやってみては。なの。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

MicroBatchFrameworkを用いたAzure Functionsの実装例と良いところ紹介

はじめに

MicroBatchFrameworkを用いてAzure Functionsにアプリをデプロイしてみたのでその方法を紹介し、MicroBatchFrameworkの良いところについても記載してみます。

MicroBatchFrameworkとは

詳細は上記参照なのですが、GUIを作成するためのフレームワークとしてPrism等があるように、CUIを作成する際にもフレームワークを用いましょうという感じです。

ざっくりやりたいこと

image.png

上図のように(定期的に)ネット上から情報取得して整形フィルタリングしてネット上のどこかに出力(通知)したいなと考えていました。
情報元の数だけコンソールアプリケーション(バッチスクリプト)を用意してcronにでも仕掛ければ実現できる内容です。

MicroBatchFrameworkを採用した理由

MicroBatchFramework – クラウドネイティブ時代のC#バッチフレームワークから引用

すでにC#にはコマンドライン引数の解析ツールはたくさんあります。とはいえ、そもそもそういうツールを使う時は「コマンドライン引数の解析」がしたいわけではなくて、「パラメータバインディング」をしたいのが一般的と思われます。ということで、『MicroBatchFramework』はウェブフレームワークのようにメソッドを呼び出してくれる仕様にしました。

という点にとても共感しました。これを用いればひとつのコンソールアプリケーションで複数のユースケースを使い分けられると思いました。

ざっくりエントリーポイントの紹介

class Program
{
    public static async Task Main(string[] args)
    {
            await BatchHost
                .CreateDefaultBuilder()
                .RunBatchEngineAsync(args);
    }
}

コンソールアプリのエントリーポイントに上記内容を記述するだけで使えます。

サーバーレスアーキテクチャへの変更

cronのためだけにサーバを起動し続けるのももったいないのでサーバーレスアーキテクチャを採用してみることにしました。
Azure Logic Appsの繰り返しトリガーHttpトリガーのAzure Functionの組み合わせでcronちっくことが実現できました。
デザイナーで見ると
image.png
こんな感じでjsonでコンソールアプリケーションに入力する内容を伝えれば済みます。

Azure Function例

    public static class Function1
    {
        [FunctionName("Function1")]
        public static async Task<IActionResult> Run(
            [HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = null)] HttpRequest req,
            ILogger log)
        {
            await Program.Main(new[] { (string)req.Query["exec"] });

            return (ActionResult)new OkObjectResult("OK");
        }
    }

コンソールアプリケーションといってもエントリーポイントは文字列配列を引数としたstaticメソッドなので外部から呼び出せます。
jsonで取得した情報を文字列配列に置きなおしてキックすれば動きます。
これの何がすごいかって、実装やテストはコンソールアプリケーションで行ってデプロイする際は軽くラップするだけで済むってところです。
いちいちサーバ上やローカルにAzure Functionとして配置して動作確認しなくても良いのです。

さらに良かった点はMicrosoft.Extensions.Logging.ILoggerの使い回し。
Azure Functionのエントリーポイントで渡されるILoggerをMicroBatchFrameworkに設定しなおせばデフォルトではコンソールに出力していた情報もApplication Insights等に出力させることができます。
具体的には以下な感じ

ILoggerProviderを継承したクラスを実装
    public class MyLoggerProvider : ILoggerProvider
    {
        private ILogger Logger { get; }
        public MyLoggerProvider(ILogger _logger) => Logger = _logger;
        public ILogger CreateLogger(string categoryName) => Logger;
        public void Dispose() { }
    }
MyLoggerProviderを生成してコンソールアプリケーションに渡す
    public static class Function1
    {
        [FunctionName("Function1")]
        public static async Task<IActionResult> Run(
            [HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = null)] HttpRequest req,
            ILogger log)
        {
            await Program.MainFunction(new[] { (string)req.Query["exec"] }, new MyLoggerProvider(log));

            return (ActionResult)new OkObjectResult("OK");
        }
    }
コンソールアプリのエントリーポイントとは別に入り口を用意
        public static async Task MainFunction(string[] args, ILoggerProvider _loggerProvider)
        {
            await BatchHost.CreateDefaultBuilder()
                .ConfigureLogging((ILoggingBuilder l) => { l.ClearProviders(); l.AddProvider(_loggerProvider); })
                .RunBatchEngineAsync(args);
        }

ConfigureLoggingにて(既存のプロバイダを削除して)MyLoggerProviderを追加してあげればAzure Functionで指定しているログ場所へ出力されるようになります。
これでコンソールアプリの時では発生しないAzure Function固有の問題の調査も容易になる思います。

MicroBatchFrameworkを採用することで良くなると思うところ

コンソールアプリ(バッチスクリプト)を使い捨てにしなくて良くなる

業務系だと

  • 黒魔術で書かれたスクリプト
  • ソースがなく挙動がブラックボックスなコンソールアプリ

みたいなのがちらほらあって、いざサーバをリプレースするぞって時に困るんですよね。(動作確認とか作り直しとか)
MicroBatchFrameworkを採用してコンテナ化して運用すれば環境にもよらなくなるしソースもあるしひとつのアプリで済むので管理が楽になると思います。

CUI <-> GUI の垣根がなくなる

例えばGUIで実装していた機能(ユースケース)をCUIとして提供したいというときに

  • GUIでもフレームワーク採用(Prismとか)
  • DDD
  • SOLID
  • オニオンアーキテクチャ(クリーンアーキテクチャ)

を意識してちゃんと機能(ユースケース)単位で注入できるように設計していたならばMicroBatchFrameworkに乗せ換えることは容易だと思うんですよね。
逆も然りなんですけど。
簡単なバッチ処理でもちゃんとユビキタス言語を用い業務知識として蓄えておけば、いざというとき二度手間をしなくても良くなる気がします。

まとめ

CUI側にもフレームワークを導入することでGUIと機能の共通化が行えるのはとても魅力的です。
今後はMicroBatchFramework固有の機能についても紹介できればと思います。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

連番を省略表記する/省略表記した連番をリストに戻す (C#で)

連番の省略表記

わかりにくい表現ですが、
1,2,3,4,5,7,8,9
という数値のリストを
1-5,7-9
という文字列に変換します。あるいはその逆変換をします。

どういうことに使えるかというと、例えば一連番号のあるものの中から抽出したものを人間が読みやすいようにするなんてことに使えます。
読み上げさせる時に、1から5 7から9と読めば、1 2 3 4 5 7 8 9と読ませるよりも人間にはどれが抽出され、どれが抽出されていないのかというのがすぐに分かります。

SequenceString.cs
using System;
using System.Collections.Generic;
using System.Runtime.Remoting.Messaging;
using System.Linq;

namespace SequenceString
{
    class ListConverter
    {
        public static string ListToSequenceString(List<Int64> myList,
                                              Char SeparaterCharacter = ',',
                                              Char NaverCharacter = '-')
        {
            string SequenceString = "";
            List<Int64> innerList = myList;
            Int64 Position = -999;
            bool isLastNumber = true;
            try
            {
                innerList.Sort();
            }
            catch
            {
                return "I can not sort this List.";
            }
            foreach (Int64 val in innerList)
            {
                if (Position + 1 == val)
                {
                    if (isLastNumber) SequenceString += NaverCharacter;
                    isLastNumber = false;
                }
                else
                {
                    SequenceString +=
                        String.Format(isLastNumber ? "{1}{2}" : "{0}{2}{1}{2}",
                                      Position, val, SeparaterCharacter);
                    isLastNumber = true;
                }
                Position = val;
            }
            if (!isLastNumber) SequenceString += String.Format("{0}", Position);
            SequenceString =
                SequenceString.Replace(SeparaterCharacter.ToString() + NaverCharacter.ToString(),
                                       NaverCharacter.ToString());
            SequenceString = SequenceString.TrimEnd(SeparaterCharacter);
            return SequenceString;
        }

        public static List<Int64> SequenceStringToList(string SequenceString,
                                                  Char SeparaterCharacter = ',',
                                                  Char NaverCharacter = '-'
                                                  )
        {
            List<Int64> innerList = new List<Int64>();
            try
            {
                foreach (String PartString in SequenceString.Split(SeparaterCharacter))
                {
                    if (PartString.Contains(NaverCharacter.ToString()))
                    {
                        String[] PartStrings = PartString.Split(NaverCharacter);
                        if (PartStrings.Count() == 2)
                        {
                            for (Int64 i = Convert.ToInt64(PartStrings[0]); i <= Convert.ToInt64(PartStrings[1]); i++)
                                innerList.Add(i);
                        }
                    }
                    else
                    {
                        innerList.Add(Convert.ToInt64(PartString));
                    }
                }
            }
            catch (Exception ex)
            {
                throw (ex);
            }
            return innerList;
        }
    }
}

これをこんな形で呼び出します。

MainClass.cs
    class MainClass
    {
        public static void Main(string[] args)
        {
            List<Int64> Test = new List<Int64>();
            Int64[] ar = { 1, 2, 3, 5, 7, 8, 9 };
            Test.AddRange(ar);
            String SequenceString;
            SequenceString =
                ListConverter.ListToSequenceString(
                    Test,
                    SeparaterCharacter: ',',
                    NaverCharacter: '-');
            List<Int64> New = ListConverter.SequenceStringToList(SequenceString, SeparaterCharacter: ',', NaverCharacter: '-');
            Console.WriteLine(SequenceString);
            foreach (int i in New)
                Console.Write(i.ToString() + " ");
        }
    }

Int64で帰ってくるのはちょっとアレかもしれません。ジェネリックでやろうと思ったら、Convert.ToInt64()のところをうまく処理する必要があります。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【C#】拡張メソッドの作り方

やりたいこと

仕事で、自作クラスではなく一般的なクラスなのに、msdocsにのっていないクラスメソッドのようなものが出てきて、なんだこれ?となった。何しているのか調べるついでに自分も作れるようになりたい。

拡張メソッド

既存の型やクラスに、元の型の変更や継承をすることなく、新たなメソッドを追加できる。

用途としては、たぶん、自分で作ったクラスであれば自分で中にメソッドを追加して機能拡張できるが、そうそういじることができない既存のクラスに機能を追加したい時などに使うと思われる。

拡張メソッドの定義側サンプル

拡張メソッドの定義部分はこのような感じ。

using System.Collections.Generic;
using System.Linq;

namespace MyExtension
{
    public static class MyExtensionClass
    {
        /// <summary>
        /// int変数の累乗を求める
        /// </summary>
        public static int MyExp(this int me, int exp)
        {
            int ret = 1;

            for (int i = 0; i < exp; i++)
            {
                ret *= me;
            }

            return ret;
        }

        /// <summary>
        /// stringのリストを、一つのstringにつなげて出力する。
        /// </summary>
        public static string MyOutputListString(this List<string> strlist)
        {
            string ret = "";

            foreach (string s in strlist)
            {
                ret += s;
                ret += " ";
            }

            return $"{ret}以上の {strlist.Count()} 項目を拡張メソッドで出力しました。";
        }

        /// <summary>
        /// 文字列voiceをloop回数繰り返し、最後にendingを付加する
        /// </summary>
        public static string MyMakeOraora(this string voice, int loop, string ending)
        {
            string ret = "";

            for (int i = 0; i < loop; i++)
            {
                ret += voice;
            }

            return ret + ending;
        }
    }
}

拡張メソッドのルール

  • staticなクラスの中に拡張メソッドを作る。
  • 拡張メソッド自身もstaticにする。
  • 引数の一つ目を「this 型 引数名」にする。
  • その一つ目の引数が、拡張メソッドを使う値自身のことをさすことになる。
  • 拡張メソッド内では、そのクラスのprivate変数にはアクセスできない。

拡張メソッド使用側サンプル

使う側は、このような感じになる。

using System;
using System.Collections.Generic;
using MyExtension; // 拡張メソッドのnamespaceを入れておく

namespace ConsoleApp2
{
    class Program
    {
        static void Main(string[] args)
        {
            // サンプル1個目
            int number = 5;
            Console.WriteLine("5の3乗は:" + number.MyExp(3));
            Console.WriteLine("11の2乗は:" + 11.MyExp(2));

            // サンプル2個目
            var slist = new List<string>();
            slist.Add("str1");
            slist.Add("str2");
            slist.Add("str3");
            Console.WriteLine(slist.MyOutputListString());

            // サンプル3個目
            Console.WriteLine("オラ".MyMakeOraora(10, "ァァァァァ!"));
            Console.WriteLine("アリ".MyMakeOraora(10, "アリーヴェデルチ!"));

            Console.ReadLine();
        }
    }
}

出力

image.png

クラスや型の中身をいじらずに、簡単に機能を追加できた。

冒頭の「なんだこれ?」となったときのコードでは、UIの部品(UIElement)のクラスの見た目を、画像のクラスに変換するというときに、拡張メソッドで変換機能を追加してる感じだった。

  • 画像クラス = UI部品.画像に変換メソッド();

のようなイメージ。

補足

image.png

  • 引数1個目は、「自分自身」のイメージ。使うときにはカッコの中ではなく前に出ている。
  • 2つ目以降の引数は、通常通りカッコの中に引数を書く。

LINQ

LINQは、実は拡張メソッド。

usingにSystem.Linqを入れていると、
image.png
List等のうしろで「.」を打つと、「Where」等Linqで使うやつらがインテリセンスで出てくるが、
image.png
usingにLinqがないと、
image.png
Whereが出てこないし、手で.Whereと打ったとしても使うことはできない。
image.png

参考

https://docs.microsoft.com/ja-jp/dotnet/csharp/programming-guide/classes-and-structs/extension-methods

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む