作業時間を自動で記録できるアプリを作りました(Windows向け)

だいぶ前の記事でこういうの作りたいと書きました。

kanchi0914.hatenablog.com

あれから少しずつですがC#+WPFで開発を進め、ようやく公開できるレベルのものが作れたので、GithubでReleaseするに至りました。

github.com

主な仕様は以下のようになっています。

  • 秒単位で起動しているアプリの実行時間が記録可能で、最小化したものは記録しないなどの設定が可能
  • 開いているファイル名ごとに作業時間の記録が可能
  • 一定時間操作がないと計測を中断(作業していないのに記録され続けることがない)
  • 時間管理系WebサービスTogglのAPIを利用し、Web上で日々の記録を確認できる

そもそもこれどういうモチベーションで開発したかというと、名前の元ネタになっている1万時間の法則という言葉があるんですね。

lineblog.me

この言葉が正しいかどうかはさておいて、なにか一つのことを極めようとする人にとって、自分が今までそれにどの程度時間を費やしたかというのは、例えば創作活動における一つのモチベーションや自信につながるのではないかと思っていて。実際に自分がそうなので、そういう用途のためにこれを開発しました。 ほとんど自分が使いたいからそうなっているのですが、主なターゲットはイラストや漫画を制作する方で、特に自分が使っているClip Studio Paintにおいて、特定のイラストを仕上げるのにかかった時間を自動で記録したいと感じている人は少なくないだろうと思ったので、そこの部分に力を入れています。 実際、ググってみるとやっぱりそういう要望は多く、しかし未だに公式で実装されていないので、だったら自分でつくろうというのが開発のきっかけです。

www.clip-studio.com

ask.clip-studio.com

例えば自分はできるだけ毎日ペンを持つようにしているのですが、そういった日々の積み重ねがデータとして残るのはやっぱり楽しいというか、自己満足に過ぎませんが「やっている」感が出て気持ちいいです。 Togglというサービスは個人的にかなり好きなのですが、あまり知られていないっぽいのがもったいないと思っているので、これをきっかけに少しでもユーザーが増えて欲しいという希望もあります。当アプリはまだまだ開発段階で今後もアップデート予定ですが、よければぜひ使っていただいてバグ報告やフィードバック等いただければと思います。 また今後の記事では知識の整理も兼ねて、開発で得られた知見について技術的な面で書ければと思っています。

(ちなみにこの記事を書くのにかかった時間は以下の通りです) f:id:Kanchi0914:20191106223840p:plain

【C#】ナップサック問題の最適値を満たす商品の番号を求める(動的計画法)

最近、蟻本と呼ばれる本を使って競技プログラミングの勉強をしています。 動的計画法ナップサック問題を解く部分で、最適値ではなく最適解(選ぶ品物の番号)を求めるコードが書かれていなかったので、少し調べてみました。 各品物が使われるかどうかを、作成したDPテーブルを逆順に辿って調べる必要があるらしく、意外なことにその部分に関してあまりコードが転がってなかったので、少し自分で考えてみました。

    public static class KnapsackSolver
    {
        static int[] weights = new int[] { 2, 2, 4, 3, 2};
        static int[] values = new int[] { 1, 2, 4, 2, 3};

        static int numOfItems = weights.Length;
        static int weightLimit = 4;

        //DP_tabel[i,w]はi+1番目からn番目の荷物のみを使って、残り容量wのナップサックに詰め込める価値の最大値
        static int[,] DP_tabel = new int[weights.Length + 1, weightLimit + 1];

        public static void Main()
        {
            Solve();
            var answer = GetOptimalItems();
            PrintAnswer(answer);
        }

        public static void Solve()
        {
            for (int i = numOfItems - 1; i >= 0; i--)
            {
                for (int j = 0; j <= weightLimit; j++)
                {
                    if (j < weights[i])
                    {
                        DP_tabel[i, j] = DP_tabel[i + 1, j];
                    }
                    else
                    {
                        var value1 = DP_tabel[i + 1, j];
                        var value2 = DP_tabel[i + 1, j - weights[i]] + values[i];
                        DP_tabel[i, j] = Math.Max(value1, value2);
                    }
                }
            }
        }
        
        static int[] GetOptimalItems()
        {
            var answer = new int[numOfItems];
            var enableWeight = weightLimit;

            for (int i = 0; i < numOfItems; i++)
            {
                if (enableWeight >= weights[i])
                {
                    //入れる場合に可能な最大価値
                    var value1 = DP_tabel[i + 1, enableWeight - weights[i]] + values[i];
                    //入れない場合に可能な最大価値
                    var value2 = DP_tabel[i + 1, enableWeight];

                    if (Math.Max(value1, value2) == value1)
                    {
                        answer[i] = 1;
                        enableWeight -= weights[i];
                    }
                    else
                    {
                        answer[i] = 0;
                    }
                }
            }
            return answer;
        }

        static void PrintAnswer(int[] answer)
        {
            for (int i = 0; i < answer.Length; i++)
            {
                if (answer[i] == 1) Console.WriteLine($"品物{i + 1}、重さ{weights[i]}、価値{values[i]}");
            }
        }
    }

注意しなければいけない点として、結果を逆算するときのDPテーブルの走査順は作成時と逆になります。 今回の場合は蟻本に乗ってるのと同様に i = numOfItems - 1から降順にDPテーブルを作成していますが、i = 0から昇順にDPテーブルを作成した場合は、最適解を求める際にi = numOfItems - 1からforループを始める必要があるということです。

参考:https://mathwords.net/napusaku#i-4

【C#】Unityで一からギャルゲー(ノベルゲー)作るためのサンプルコード

一応は前回の記事の続きにあたりますが、コードの内容はほとんど別物です。 前回の記事ではゲームの方針として脱出ゲームのようなものを考えていましたが、今回はやや方向性を変えギャルゲーっぽいものを作ることを想定します。

f:id:Kanchi0914:20191002174115g:plain

今回の記事でやりたいことは以下になります。

1.シナリオをスクリプト外のデータから読み込み、それに合わせて処理を行う
2.キャラクターの立ち絵に簡単なアニメーションをつける
3.テキストの文字送り機能の実装

コードは以下に上げてあります。 github.com

テキストベースのシナリオ処理

まず上で述べた1に関してです。データの保存形式にはcsvjsonなどいろいろありますが、調べてみるとメタ文字を設定したテキストデータで記述するのが一般的らしいので、その例に倣います。今回は#charaタグで立ち絵を登場させ、#imageタグでそのグラフィックを変更するなどを行いました。今回は実装していませんが他には背景を変えたり、BGMや効果音などのタグを用意しそれに合わせて音を鳴らすなどもできそうです。

#scene=001
#speaker=主人公
{あ~ 今日もいい天気だなぁ}
#speaker=???
{お~い 待ってよ~~~}
#speaker=主人公
{ん?この声は…}
#chara=hiroko
#image_hiroko=smile
#speaker=ヒロ子
{じゃ~ん あたしだよ~~~
一緒に学校行こうよ~~~}

1シナリオの流れは、シーンという小単位に区切って管理するものとします。 1シーンに相当する文字列を保持しておくSceneクラスを用意し、 別途パーサークラスを用意して一行ずつ読み取らせることで処理を行います。

public class Scene
{
    public string ID { get; private set; }
    public List<string> Lines { get; private set; } = new List<string>();
    public int Index { get; private set; } = 0;

    public Scene(string ID = "")
    {
        this.ID = ID;
    }
    ...

読み取った位置行ごとの文字列に応じて、実際に処理させるクラスのメソッドを呼び出します。 例えば現在の行が#speakerタグのついた位置に達したときにはSetSpeakerメソッドが、#imageタグの位置に来たときはSetImageメソッドが呼び出される等です。

    public void SetSpeaker(string name = "")
    {
        gui.Speaker.text = name;
    }

    public void SetImage(string name, string ID)
    {
        var character = Characters.Find(c => c.Name == name);
        character.SetImage(ID);
    }

前回はデリゲートメソッドを用いてボタンクリック時に実行されるメソッドを指定することで 選択肢を実装していましたが、今回は#optionというタグを付けることで 選択した先のシーンに遷移し、その先で処理を行うものとします。

#speaker=ヒロ子
{そういえば、今日はテストだったよね~勉強した?}
#options={
{した,002}
{してない,003}
}

#scene=002
#speaker=主人公
{したに決まってるだろ}
#image_hiroko=aseri
#speaker=ヒロ子
{ほんと~?あたし全くしてないよ~~~}
#speaker=主人公
{おまえはいつもそうだな~早く学校行くぞ}
#next=004

#scene=003
#speaker=主人公
{してない。面倒だから}
#image_hiroko=smile
#speaker=ヒロ子
{ほんと?やった~あたしと同じだ~}
#speaker=主人公
{そんなんで喜ぶなよ…早く学校行くぞ}
#next=004

用意されたタグ以外の処理を行いたいときはどうすればいいのか考えたのですが、MethodInfoクラスというものを用いることで実現できるようです。これによりテキストデータ内にメソッド名を直接書いてしまうことができるので実装の幅が広がります。自分も調べて初めて知りましたがかなり便利そうです。

    if (line.Contains("method"))
    {
        line = line.Replace("method=", "");
        var type = actions.GetType();
        MethodInfo mi = type.GetMethod(line);
        mi.Invoke(actions, new object[] { });
    }

これを用いることで、「○○というフラグが立っていればシーン00Xに遷移し、そうでなければシーン00Yに遷移する」という、いわゆるif-gotoの処理を行うことができます。フラグを立てる事自体もこれでできるでしょう。

    public void HasKey()
    {
        if (ExistsKey()) sceneController.SetScene("00X");
        else sceneController.SetScene("00Y");
    }

DOTweenを使ったアニメーション

上記の2.3についてですが、これはアセットストアで配信されているDoTweenというアセットを導入することでごく簡単に実装することができます。簡単に説明するとUnityのtransformに対して時間経過で色々できる便利なやつで、Unityのアニメーションクリップを使わずともお手軽にアニメーションを導入できるので、複雑な動きを必要としない場合はこれだけで十分間に合うと思います。

f:id:Kanchi0914:20191002174028g:plain

詳しい説明は他サイト様にお任せするとして、例えばメッセージテキストのクリックを促す下矢印のアニメーションは、スクリプトから以下のように記述するだけでいいし、キャラクター登場時のエフェクトなんかもDoTweenでやっています。

    public GameObject Delta;

    private void Start()
    {
        Delta.transform.DOMoveY(-0.2f, 1.0f).SetRelative().SetEase(Ease.InCubic)
            .SetLoops(-1, LoopType.Restart);
    }

なおDoTweenはFree版と有料のPro版が存在しますが、多分基本的な機能に違いはないと思います。自分はPro版導入してるのでもしFree版で動かなかったらすみません。

また3のテキストの文字送りに関して、本来であればUpdate関数内で一文字ずつ処理を行ったりしなければならないのですが、これもDoTweenのDOTextというメソッドを使うことでこれだけ簡単にできます。テキストのアニメーションを完了させることで文字送りのスキップも行なえます。 (参考:https://gist.github.com/anzfactory/da73149ba91626ba796d598578b163cc)

    public void SetText(string text)
    {
        tempText = text;
        if (textSeq.IsPlaying())
        {
            textSeq.Complete();
        }
        else
        {
            gui.Text.text = "";
            textSeq = DOTween.Sequence();
            textSeq.Append
                (gui.Text.DOText
                (
                    text,
                    text.Length * messageSpeed
                ).SetEase(Ease.Linear));
        }
    }

結論

だいぶかいつまんで説明しましたが、今回の記事で自分が言いたかったのは

  • MethodInfoを使えばだいたい何でもできそう
  • DoTweenは便利

の2点に尽きます。実際今回のコードに少し手を加えるだけでそれなりに見れるゲームは作れると思います。 実際には他に優れたUnity向けのツクール系エディタやアセットが存在するのでそれを使えばいいだけの話なのですが

その他

お借りした背景素材: きまぐれアフター 様

【C#/Unity】出現率から項目を一つルーレット選択で選ぶ拡張メソッド

RPGなどで、出現率(選択確率)などからある項目を一つ選びたい場面がしばしばあると考えられます。 例えば、敵の各行動が行われる確率を以下のようにDictionary形式で表すとするとき

    var enemyActionProbs = new Dictionary<string, int>()
    {
        { "attack", 60 },
        { "guard", 30 },
        { "escape", 10 },
    };

attack,guard,escapeの各項目が、それぞれ60%、30%、10%の確率で得られるメソッドかなにかが欲しいわけです。 こういった場合、おそらく遺伝的アルゴリズムなどで用いられるルーレット選択を用いて得ることが一般的であると思われます(実際にはどうかわかりません。調べてもあまり出てこなかったので)。

簡単に説明すると、それぞれが選択される確率の総和をとり、0からその和までの乱数の値を決定し、 その値に応じた値を選択するのがルーレット選択です。 以下は適当に作った図ですが、例えば乱数の数が65だった場合、上の例だとguardが選ばれます。

f:id:Kanchi0914:20190930053935p:plain:w600

なおこの例では各確率の総和が100になるようにしていますが、別にそうしなくても構いません。 例えばアイテムの出現割合を記したDictionaryから一つを選びたいときなどにも使えます。

自分は今まではクラス毎に実装していたのですが、流石に効率が悪いのでジェネリックを用いた拡張メソッドを使って実装することにしました。

using System.Collections.Generic;

public static class DictExtensions
{
    public static T GetByRouletteSelection<T>(this Dictionary<T, int> dict)
    {
        var sum = 0;
        T selected = default;

        foreach (var key in dict.Keys)
        {
            sum += dict[key];
        }

        int tempSum = UnityEngine.Random.Range(0, sum);

        foreach (var key in dict.Keys)
        {
            tempSum -= dict[key];
            selected = key;
            if (tempSum < 0)
            {
                break;
            }
        }

        return selected;
    }
}

以下のように使います。キーの型は何でも構わないので自作クラス等をキーにしたDictionaryにも使うことができます。

    public void DictTest()
    {
        var enemyActionProbs = new Dictionary<string, int>()
        {
            { "attack", 60 },
            { "guard", 30 },
            { "escape", 10 },
        };
        
        for (int i = 0; i < 50; i++)
        {
            Debug.Log(enemyActionProbs.GetByRouletteSelection());
        }
    }

(実行結果)

attack
guard
attack
attack
attack
escape
attack
guard
guard
attack
attack
...

【C#】Unity 2Dでできるだけ簡単にアドベンチャーゲーム(ノベルゲーム)を作る その2

kanchi0914.hatenablog.com

前回の記事の続きです。前回の記事では、クリックでテキスト送りの部分まで実装しましたが、当然それだけではゲームとして成立しません。今回は選択肢を表示させ、コマンドを実行することで最低限ゲームとして遊べるようにします。完成すると下のようになります。

f:id:Kanchi0914:20190723065603g:plain

まずはコマンドを選択するため、動的にコンポーネントの数が変化するPanelを作ります。HierarchyビューのCanvasに適当にPanelを配置し、PanelのInspectorビューからVertical Layout Groupを追加、以下のように設定します。なお今回脱出ゲームっぽい感じにしたかったので、それにあわせて背景も変えてあります。 f:id:Kanchi0914:20190723064530p:plain f:id:Kanchi0914:20190723064326p:plain

次に、今用意したボタン用のPanelに子要素としてButtonを追加し、ButtonのInspectorビューからLayout Elementを追加、Min Heightの値を適当に設定します。 f:id:Kanchi0914:20190723064420p:plain

設定した後、ButtonをAssetsフォルダにドラッグ&ドロップし、Buttonをプレハブ化します。 f:id:Kanchi0914:20190723064546p:plain

 これでUnityエディタ側の準備は完了です。では次にスクリプトを用意していきましょう。
 まずは以下のようにコマンド選択のためのOptionクラスを用意します。

    class Option
    {
        public string Text;
        public Action Action;
        public Func<bool> IsFlagOK = () => { return true; };
    }

 ここでActionFuncというのは、デリゲートと呼ばれる型の一種です。詳しい説明は省きますが、メソッドの参照をそのまま渡せる型だと思ってください。つまり、これを使ってさっき作ったボタンにメソッドの参照を渡すことで、ボタンでコマンドを選択→特定の処理を実行、という流れができるわけですね。  またScenarioクラスに、このOptionクラスのListを追加したりするなど修正を加えます。

    class Scenario
    {
        public string ScenarioID;
        public List<string> Texts = new List<string>();
        public List<Option> Options = new List<Option>();
        public string NextScenarioID;
    }

実際にはどう使うかというと、ScenarioクラスのOptionsに選ばせたいコマンドの数だけOptionクラスのインスタンスを追加し、Textにコマンドの内容、Actionにそのコマンドを実行した際の処理となるメソッドへの参照を代入します。

        scenario02 = new Scenario()
        {
            ScenarioID = "scenario02",
            Texts = new List<string>()
            {
                "どうする?",
            },
            Options = new List<Option>
            {
                new Option()
                {
                    Text = "辺りを見渡す",
                    Action = LookAround
                },
                new Option()
                {
                    Text = "鍵を拾う",
                    Action = TakeKey,
                    IsFlagOK = () =>
                    {
                        return isCheckedKey && !items.Contains("Key");
                    }
                },
                new Option()
                {
                    Text = "扉を開ける",
                    Action = OpenDoor,
                }
            }

        };
    public void LookAround()
    {
        var scenario = new Scenario();
        scenario.NextScenarioID = "scenario02";
        if (!items.Contains("Key"))
        {
            scenario.Texts.Add("足元に鍵が落ちている");
            isCheckedKey = true;
        }
        else
        {
            scenario.Texts.Add("足元には何もない");
        }
        SetScenario(scenario);
    }

    public void OpenDoor()
    {
        var scenario = new Scenario();
        if (items.Contains("Key"))
        {
            scenario.Texts.Add("鍵を使って扉を開いた");
            scenario.Texts.Add("クリアー!");
        }
        else
        {
            scenario.Texts.Add("鍵がかかっていて開かない");
            scenario.NextScenarioID = "scenario02";
        }
        SetScenario(scenario);
    }

    public void TakeKey()
    {
        var scenario = new Scenario();
        scenario.Texts.Add("鍵を拾った");
        scenario.NextScenarioID = "scenario02";
        SetScenario(scenario);
        items.Add("Key");
    }

またIsFlagOKは選択肢を出現させるかどうかのbool値で、基本的にはTrueをとりますが、これを特定の条件を満たしているかどうかで分けることができます。 ここでは、「”辺りを見渡す”コマンドを選択した」&「まだアイテム”Key”を拾っていない」状態でのみ、「鍵を拾う」コマンドが出現するようになっています。

                new Option()
                {
                    Text = "鍵を拾う",
                    Action = TakeKey,
                    IsFlagOK = () =>
                    {
                        return isCheckedKey && !items.Contains("Key");
                    }
                },

IsFlagOKの右辺のような書き方をラムダ式と呼び、簡単に言えばメソッドの宣言を省略して書くことが出来ます。 これと先程のデリゲートを組み合わせることで、かなり自由にコードを書くことができるようになります。

あとはコマンドに応じて先程プレハブ化したボタンを設定する処理を書いたりします。

    void SetOptions()
    {
        foreach (Option o in currentScenario.Options)
        {
            if (o.IsFlagOK())
            {
                Button b = Instantiate(optionButton);
                Text text = b.GetComponentInChildren<Text>();
                text.text = o.Text;
                b.onClick.AddListener(() => o.Action());
                b.onClick.AddListener(() => ClearButtons());
                b.transform.SetParent(buttonPanel, false);
            }
        }
    }

buttonPaneloptionButonは先程用意したパネルとボタンへの参照です。前回のText同様GameControllerのInspector上でアタッチしておきます。このように、Unityチュートリアルなどで見るボタンの処理には、先程のデリゲートとラムダ式が用いられていたわけですね。 その他色々修正したコード全体は以下に上げておきます。

github.com

これで、コマンド選択の結果によってフラグの設定を切り替えたり、あるいはその内容に応じてコマンドを変更したりすることができるようになりました。 とりあえずここまでできれば、簡単なノベルゲーム程度なら作れるようになったのではないでしょうか。本スクリプトではcurrentScenarioにScenarioクラスのオブジェクトをセットすることでメッセージが表示されるため、例えば画面上の特定の点をクリックしたときにメッセージや選択肢を表示させたりすることで、脱出ゲーム的なのも作れると思います。

今回は処理を行うタイミングがコマンドを実行した場合に限られましたが、例えばC# ver7.0から導入されたタプル機能を使い、ScenarioクラスのTextsを以下のように変更することで、テキストを一行送る度に処理を行うことができるようになります。なおUnityでC# 7.0を使えるのは特定のバージョン以降に限られます(多分)。

public List<(string, Action)> Texts = new List<(string, Action)>();

こうすることで、例えばメッセージに併せてキャラクターの画像を変更できたりという、いわばギャルゲーっぽい処理を簡単に実装することができます。これについては今後時間があれば書きたいと思います。

【C#】Unity 2Dでできるだけ簡単にアドベンチャーゲーム(ノベルゲーム)を作る その1

だいぶ前に書いた書きかけの記事があったので、追記して更新します。 この記事は、「Unityでアドベンチャーゲーム作りたいけど、どうやったらいいのか分からない」という方のために書かれた、 Unity 2Dで可能な限り最小限のコードのみでアドベンチャーゲームを作る、超入門チュートリアルです。

  • はじめに
  • ゲームの設計
  • プロジェクトの基本設定
    • プロジェクトの作成
    • メッセージウィンドウの追加
  • C#スクリプト(文字送り編)
  • コード解説

続きを読む

【WPF】ContextMenuに表示される一部項目を、場合に応じて選択できなくする

意外と書いてあるサイトが少なかったのでメモしておきます。 WPFで、通常右クリック時に開かれるメニューに用いられるContextMenuコントロールの内容を、 開かれる前に編集したいことがあると思いますが、 そのときはContextMenuOpeningイベントを利用すると良いみたいです。 自分はListVIewを右クリックしたときに表示されるメニューのうち、アイテムが複数選択されているときのみ有効な項目を作りたかったので、以下のようにしました。

    public partial class Window1 : Window
    {
        ContextMenu contextMenu = new ContextMenu();

        MenuItem menuItem0 = new MenuItem();
        MenuItem menuItem1 = new MenuItem();

        public Window1 ()
        {
            InitializeComponent();
            CreateMenu();
        }

        private void CreateMenu()
        {
            menuItem0.Header = "選択項目をマージ";
            menuItem0.Click += menuItem_ClickMergeItems;
            menuItem1.Header = "選択項目を削除";
            menuItem1.Click += menuitem_ClickDelete;

            contextMenu.Items.Add(menuItem1);
            contextMenu.Items.Add(menuItem2);

            //コンテキストメニュー表示時に発生するイベントを追加
            listVIew.ContextMenuOpening += contextMenu_Click;

            //ListViewコントロール
            listVIew.ContextMenu = contextMenu;
        }

        private void contextMenu_Click(object sender, RoutedEventArgs e)
        {
            //listviewのアイテムが複数選択されていない場合、一部項目を無効にする
            menuItem0.IsEnabled =  listVIew.SelectedItems.Count > 1;
        }
    }

大事なのはContextMenuの親元のコントロールにイベントを追加することです。 最初

contextMenu.ContextMenuOpening += contextMenu_Click;

としていたのですが、こうするとContextMenuが開かれているときに右クリックした場合しか呼ばれなくなります。

stackoverflow.com