列車接近灯を作ろう

①概要

 駅や線路脇に置いてあり、列車が接近すると点滅しだすアレです。おそらく保線作業員さんなどに列車の接近を伝えるためにあるのだと思います。 今までのBVEでは、「ストラクチャを動かす・変更する=他列車を応用する」という考えが一般的でしたが、BveEXの登場後その必要はなくなりました。

②準備

 まず、ストラクチャを準備します。今回はFCS鉄道工房様の色灯式信号 ver 2.1を使用させていただきました。
 次に、BveEXプラグイン用のプロジェクトを作ります。おーとまさんのホームページやBveEXの解説本を読んでプロジェクトを作成し、 Nugetからパッケージのインストールを済ませました。

③コードを書く

ファイル・フォルダ類の準備ができたところで、早速プラグインを作っていきます。まず、using句AssemblyPluginBaseから。 これで、BveEXに対してこのコードがBveEXプラグインであることを知らせることができます。

using System;
using System.Drawing;

using BveTypes.ClassWrappers;

using BveEx.PluginHost;
using BveEx.PluginHost.Plugins;
using Zbx1425.DXDynamicTexture;

using System.IO;
using System.Reflection;

namespace BveEx.Samples.MapPlugins.ApproachLamps
{
    [Plugin(PluginType.MapPlugin)]
    public class ApproachLamps : AssemblyPluginBase
    {
     ***
    }
}

 次に、classの中身を実装していきます。今回は画像の読み込みや点滅動作を行うので、以下の要素を上記コードの***の部分に加えます。 以後特に注意のない限りはpublic class ApproachLamps : AssemblyPluginBase{ }の中を編集しているものとします。

namespace BveEx.Samples.MapPlugins.ApproachLamps
{
    [Plugin(PluginType.MapPlugin)]
    public class ApproachLamps : AssemblyPluginBase
    {
        public ApproachLamps(PluginBuilder builder) : base(builder)
        {
        
        }

        public override void Dispose()
        {

        }

        private void OnScenarioCreated(ScenarioCreatedEventArgs e)
        {
  
        }

        public override void Tick(TimeSpan elapsed)
        {
        
        }
    }
}
//※命名方法など一般的でない部分があるかもしれませんが大目に見てください…!

 型ができたらいよいよ自分でコードを書いていきます。まず、private関連とpublic ApproachLamps(PluginBuilder builder) : base(builder){ }から。

namespace BveEx.Samples.MapPlugins.DXDynamicTextureTest
{
    [Plugin(PluginType.MapPlugin)]
    public class ApproachLamps : AssemblyPluginBase
    {
        private TextureHandle TextureHandle1;
        private GDIHelper GDIHelper1;
        private Bitmap OnBitmap1;
        private Bitmap OffBitmap1;

        private TextureHandle TextureHandle2;
        private GDIHelper GDIHelper2;
        private Bitmap OnBitmap2;
        private Bitmap OffBitmap2;

        public ApproachLamps(PluginBuilder builder) : base(builder)
        {
            BveHacker.ScenarioCreated += OnScenarioCreated;
            string baseDirectory = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);

            OffBitmap1 = new Bitmap(System.IO.Path.Combine(baseDirectory, "../../Structures/FCS_TM/SwitchSig/nowhite_LED.png"));
            OnBitmap1 = new Bitmap(System.IO.Path.Combine(baseDirectory, "../../Structures/FCS_TM/SwitchSig/WhiteLED.png"));
            OffBitmap2 = new Bitmap(System.IO.Path.Combine(baseDirectory, "../../Structures/FCS_TM/SwitchSig/noFlareWhite.png"));
            OnBitmap2 = new Bitmap(System.IO.Path.Combine(baseDirectory, "../../Structures/FCS_TM/SwitchSig/FlareWhite.png"));
        }

        public override void Dispose()
        {
            BveHacker.ScenarioCreated -= OnScenarioCreated;
        }

        private void OnScenarioCreated(ScenarioCreatedEventArgs e)
        {
            
        }

        public override void Tick(TimeSpan elapsed)
        {
        
        }
    }
}

 いろいろ検索しながら、見よう見まねで書きました。PIのdllが置かれる場所からの相対パスでファイルを指定します。これで、ストラクチャ類をPIから読み込むことができます。 OnScenarioCreatedDisposeするのを忘れずに。イベントの購読というらしいです。
 この後はBVE内で点滅させる動作を作る工程に入ります。そもそも点滅をどうやって作るかですが、簡単に言うと「xファイル内のテクスチャを入れ替えする」です。 よって、「PIからBVEのStructure Keyを取得すること」や「PIでテクスチャを入れ替えすること」ができれば作ることができます。まず、OnScenarioCreated{ }の中身から。

private void OnScenarioCreated(ScenarioCreatedEventArgs e)
{
    Model targetModel = e.Scenario.Map.StructureModels["sigib_on_led"];
    TextureHandle1 = targetModel.Register("WhiteLED.png");
    TextureHandle2 = targetModel.Register("FlareWhite.png");

    GDIHelper1 = new GDIHelper(TextureHandle1.Width, TextureHandle1.Height);
    GDIHelper2 = new GDIHelper(TextureHandle2.Width, TextureHandle2.Height);
}

 イベントOnScenarioCreatedのタイミングでBVEのStructureリストから「sigib_on_led」を取得します。そのテクスチャである「WhiteLED.png」と「FlareWhite.png」をTextureHandleとして置いておきます。 また、ここら辺の仕組みはよく理解していないのですが、点滅動作はDXDynamicTextureを使用して行うため、GDIHelperとして上で取得したテクスチャたちを置いておきます。 もう一度言いますが、ここら辺の仕組みはよく理解していないので見よう見まねでGDIHelperを使っています。また、プログラミング素人にはどういう表現が正しいかわからないので、 変数プロパティを「置く」という表現をしています。ご了承ください。
 ここまでできたら、ついに点滅動作です。点滅は常に動かしておきたいので、Tick{ }に記述します。

private TimeSpan Passedtime = TimeSpan.Zero;
private bool isLighting = false;

public override void Tick(TimeSpan elapsed)
{
    if (elapsed.TotalSeconds < 1)
    {
        Passedtime = Passedtime + elapsed;
    }
    if (Passedtime.TotalMilliseconds > 400)
    {
        Passedtime = Passedtime - TimeSpan.FromMilliseconds(400);
        GDIHelper1.BeginGDI();
        GDIHelper2.BeginGDI();
        {
            if (isLighting)
            {
                GDIHelper1.DrawImage(OffBitmap1, 0, 0);
                GDIHelper2.DrawImage(OffBitmap2, 0, 0);
                isLighting = false;
            }
            else
            {
                GDIHelper1.DrawImage(OnBitmap1, 0, 0);
                GDIHelper2.DrawImage(OnBitmap2, 0, 0);
                isLighting = true;
            }
        }
        GDIHelper1.EndGDI();
        TextureHandle1.Update(GDIHelper1);
        GDIHelper2.EndGDI();
        TextureHandle2.Update(GDIHelper2);
    }
    BveHacker.MainFormSource.Text = Passedtime.ToString();

 初心者には仕組みもプログラムもとても難しいですが、書いてみると何となく理解はできます。bool型のcode>isLightingを作っておき、if(isLighting = true)else(isLighting = false)を繰り返して点滅動作を実行しています。true(点灯)ならOffBitmapで消灯し状態をfalseに、 false(消灯)ならOnBitmapで点灯し状態をtrueに…という感じです。点滅の間隔はTotalMillisecondsFromMillisecondsで変えられます。 だいたい400msくらいが私はちょうどよいと思いました。ここは路線や場所によって変えるとよいと思います。

④完成

 さて、これで完成です。ビルドして動作させてみましょう。これであなたの路線もピカピカです!!ピカピカというとキレイになったみたいですね。一応、私の完成形を置いておきます。

using System;
using System.Drawing;
using BveTypes.ClassWrappers;
using BveEx.PluginHost;
using BveEx.PluginHost.Plugins;
using Zbx1425.DXDynamicTexture;
using System.IO;
using System.Reflection;

namespace BveEx.Samples.MapPlugins.KeioAL
{
    [Plugin(PluginType.MapPlugin)]
    public class ApproachLamps : AssemblyPluginBase
    {
        private TextureHandle TextureHandle1;
        private GDIHelper GDIHelper1;
        private Bitmap OnBitmap1;
        private Bitmap OffBitmap1;
        private TextureHandle TextureHandle2;
        private GDIHelper GDIHelper2;
        private Bitmap OnBitmap2;
        private Bitmap OffBitmap2;

        public ApproachLamps(PluginBuilder builder) : base(builder)
        {
            BveHacker.ScenarioCreated += OnScenarioCreated;
            string baseDirectory = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);

            OffBitmap1 = new Bitmap(System.IO.Path.Combine(baseDirectory, "../../Structures/FCS_TM/SwitchSig/nowhite_LED.png"));
            OnBitmap1 = new Bitmap(System.IO.Path.Combine(baseDirectory, "../../Structures/FCS_TM/SwitchSig/WhiteLED.png"));
            OffBitmap2 = new Bitmap(System.IO.Path.Combine(baseDirectory, "../../Structures/FCS_TM/SwitchSig/noFlareWhite.png"));
            OnBitmap2 = new Bitmap(System.IO.Path.Combine(baseDirectory, "../../Structures/FCS_TM/SwitchSig/FlareWhite.png"));
        }

        public override void Dispose()
        {
            BveHacker.ScenarioCreated -= OnScenarioCreated;
        }

        private void OnScenarioCreated(ScenarioCreatedEventArgs e)
        {
            Model targetModel = e.Scenario.Map.StructureModels["sigib_on_led"];
            TextureHandle1 = targetModel.Register("WhiteLED.png");
            TextureHandle2 = targetModel.Register("FlareWhite.png");

            GDIHelper1 = new GDIHelper(TextureHandle1.Width, TextureHandle1.Height);
            GDIHelper2 = new GDIHelper(TextureHandle2.Width, TextureHandle2.Height);
        }

        private TimeSpan Passedtime = TimeSpan.Zero;
        private bool isLighting = false;
        public override void Tick(TimeSpan elapsed)
        {
            if (elapsed.TotalSeconds < 1)
            {
                Passedtime = Passedtime + elapsed;
            }
            if (Passedtime.TotalMilliseconds > 400)
            {
                Passedtime = Passedtime - TimeSpan.FromMilliseconds(400);
                GDIHelper1.BeginGDI();
                GDIHelper2.BeginGDI();
                {
                    if (isLighting)
                    {
                        GDIHelper1.DrawImage(OffBitmap1, 0, 0);
                        GDIHelper2.DrawImage(OffBitmap2, 0, 0);
                        isLighting = false;
                    }
                    else
                    {
                        GDIHelper1.DrawImage(OnBitmap1, 0, 0);
                        GDIHelper2.DrawImage(OnBitmap2, 0, 0);
                        isLighting = true;
                    }
                }
                GDIHelper1.EndGDI();
                TextureHandle1.Update(GDIHelper1);
                GDIHelper2.EndGDI();
                TextureHandle2.Update(GDIHelper2);
            }
            BveHacker.MainFormSource.Text = Passedtime.ToString();
        }
    }
}