ノートの端の書き残し

UnityやらC#やら。設計が得意かもしれない。

初めての設計をやり抜くための本

www.amazon.co.jp

読書感想文です。

この本で言うところの設計は、1モジュールの詳細設計ではなく、アプリケーション、ソフトウェア全体のアーキテクチャという設計ですね。

ちゃんとシステム開発の歴史上定義されてる言葉の中に、著者の周りで使われてるだけでは?という言葉がたまに混ざっている気がするのでその点はちょっとどうかなと思いますが、私が経験則でなんとなくこうすれば都合が良いと思っているものが言語化されていたので、ためになりました。

ただ、設計ってどうしても確実な正解が無いし、この本は結構そこを強調しているので、めちゃめちゃ正しい姿勢ではあるのですが、設計何もわからんという人は参考にはできないのではないかなと思いました。

プロジェクト全体を見れるようになり始めた、くらいの人向けの本かなと思います。

良いコード、悪いコードってなんなのか

設計はトレードオフ

私自身よく、このコードはこういう理由でダメだからこう書いた方がいい、とかコードレビューで言いますが、その良し悪しの基準というのはそもそも1つではないです。

多分もっといっぱい分けようと思えば分けられるんでしょうが、このように色々と追求したい要素というのはあり、しかもこれはどう考えても両立できない、ということがほとんどです。

例えば、実行時パフォーマンスだけを追い求めるなら全て密結合にして拡張性は無くなるし、エラーハンドリングも無い方がいいやとなりかねません。
超高度でハイパフォーマンスな最新のテクニックを使ったイケイケなプロジェクトは、それらを使える人材が少なく教育や人員確保に苦労するでしょう。

こんなことは明文化するまでもなく誰でも普段から考えているはずなのですが、良いコードとか悪いコードとかの話になると視野が狭くなることが多いように思います。

悪いコード

絶対的に悪いコードというのは、「さっき挙げたようなどの要素についてもクオリティを落とすことなく、どれかの要素のクオリティを上げる手段」が残っているコード、と言えます。コストを支払わずにクオリティを上げられる方法があるならそれは絶対に採用した方がいいですよね。

例えばRust

Rustはここ数年で言うとかなり注目の言語と言えると思います。私も好きです。Rustの良いところは、コンパイラが頑張ることでできるだけコストを支払うことなく、安全性や実行時パフォーマンスを上げようとしているところで、これが需要とマッチしていたのだと思います。

しかし、Rustは習得難度は高いし、ジェネリックがめちゃくちゃ入れ子になるのも正直読みづらいです。
それでもRustが支持されるのは、支払っているコストが、メリットと比べて重要ではない、もしくは努力によって償却できるものだからです。トレードオフの判断が上手いんですね。

良いコードは、トレードオフの判断力と、それを実現する知識から生まれる

ここまでの話を要約すると

良いコードは重要でない部分でコストを支払い、重要な部分で高いクオリティを発揮する

と言えます。「重要か重要でないか」は場合によるので、当然絶対的に良いコードなんて無いです。コードに限らず、技術選定もそうですね。プログラムに限った話ですらないと思います。

ですから、「良いコード」を書くには、まずはトレードオフの妥当な判断ができること、そして次に妥当な判断を実現できる技術力を身につけることが必要でしょう。同じプロジェクト内でも、レイヤーによって重視するべき要素は変わってきます。これが難しいけど面白いポイントですね。

というわけで、これからは良くないコードを見たときに「クソコード!」と叫ぶ前に、トレードオフの判断が間違っているのか、知識や技術力が不足しているのか、落ち着いて考えましょう。考えてから叫ぶと良いと思います。

【Unity】Deep Profiler Supportをオンにしてビルドするだけでも多少重くなる

DevelopmentBuild

  • Development Build: DevelopmentBuildのフラグ。これがオンなら下2つも有効にできる。
  • Autoconnect Profiler: 起動時にUnity のProfilerに自動で接続するフラグ。
  • Deep Profiling Support: ビルド後でもDeep Profileを使えるようにするフラグ。

比較

環境

- Unity2022.3.2

空っぽなシーンに以下のような、毎フレームなんでもないことをするオブジェクトだけ追加してビルド。 とりあえず楽なのでWindows向けのIL2CPPビルドです。

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

public class Test : MonoBehaviour
{
    private List<int> _list = new();
    private void Start()
    {
        _list = Enumerable.Range(0, 1000000).ToList();
    }

    private void Update()
    {
        var sum = 0;
        foreach (var e in _list)
        {
            sum += e;
        }
    }
}

ビルドしたらプロファイラでTest.Updateの処理時間(ms)を見てみます。 値はブレるので雑ですが、だいたいこの辺りの数値のフレームが多かったですよというくらいです。
まぁ正確な数値が欲しいわけではないので。

  1. ビルド時にDeep Profiling Supportをオンにするかどうか
  2. プロファイラで見るときにDeep Profileをオンにするかどうか

で場合分けしています。

Deep Profile OFF Deep Profile ON
Deep Profiling Support OFF 1.65ms ×
Deep Profiling Support ON 5.11ms 72.90ms

というわけで、Deep Profiling Supportフラグを有効にすると、プロファイラでDeep ProfileをONにしなくても実行時間は増えます。
とりあえずDevelopment BuildするならDeep Profiling Supportもオンでええやろ! というのはあまり良い考えでは無さそうですね。目的次第ですが。

結論

  1. Deep Profileはオーバーヘッドが極めて大きく出るので、実行時間は信用できない。
    でもヒープアロケーションの調査にはかなり向いている。

  2. Deep Profiling Supportをオンにしてビルドすると、Deep Profileを使わなくても実行時間は結構大きく出る。
    なので、ある程度重そうなところの見当が付いたら、Deep Profiling Supportをしっかりオフにして、Profiler.BeginSampleで計測するのが良い。

コンセプトから理解するRust

Amazonリンク

https://www.amazon.co.jp/dp/4297125625

感想

Rustを書いていて、どうしてもコンパイルエラーの解決がスムーズにいかないとか身に付いていない感じがしたので読んでみました。
中身としては、そもそもRustの理解のためにはメモリ領域とかその参照関係とか基礎を理解するのが一番重要だよねというコンセプトで、その辺の基礎の説明が多かったです。
ただ、ヒープとかスタックとかその辺の割と簡単めな基礎知識の部分で終わってしまっていて、排他制御とか本当に難しいところは説明を省いており、まぁ良くも悪くも入門書だなという印象です。

これで「完全に理解した」とはならなかったですが、そもそも基礎知識にふわふわしたところがあったらRustはできないというところはハッキリしたのでその点は良かったかなと思います。

【Unity】エディタ上でだけ編集可能なパラメータ

エディタ上でだけ編集可能にする

「エディタ上で編集不可能にする」ではなく、その逆です。
↓こういうこと

ReadonlyInScript属性が付いたフィールドはインスペクタでは普通に編集できますが、スクリプトで代入しようとするとエラーで怒られます。

ネタバラし

RoslynAnalyzerで書き込みを禁止しているだけです。 以下のような感じで、すごく愚直に。

public override void Initialize(AnalysisContext context)
        {
            // お約束。
            context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
            context.EnableConcurrentExecution();

            // binary assignment expression全部
            var kinds = new SyntaxKind[]
            {
                SyntaxKind.SimpleAssignmentExpression,
                SyntaxKind.AddAssignmentExpression,
                SyntaxKind.SubtractAssignmentExpression,
                SyntaxKind.MultiplyAssignmentExpression,
                SyntaxKind.DivideAssignmentExpression,
                SyntaxKind.ModuloAssignmentExpression,
                SyntaxKind.AndAssignmentExpression,
                SyntaxKind.ExclusiveOrAssignmentExpression,
                SyntaxKind.OrAssignmentExpression,
                SyntaxKind.LeftShiftAssignmentExpression,
                SyntaxKind.RightShiftAssignmentExpression,
                SyntaxKind.CoalesceAssignmentExpression,
                SyntaxKind.PreIncrementExpression,
                SyntaxKind.PreDecrementExpression,
                SyntaxKind.PostIncrementExpression,
                SyntaxKind.PostDecrementExpression
            };
            
            context.RegisterSyntaxNodeAction(AnalyzeSyntax, kinds);
        }

        private static void AnalyzeSyntax(SyntaxNodeAnalysisContext context)
        {
            var syntax = context.Node switch
            {
                AssignmentExpressionSyntax assignExpression => assignExpression.Left,
                PostfixUnaryExpressionSyntax postUnary => postUnary.Operand,
                PrefixUnaryExpressionSyntax preUnary => preUnary.Operand,
                _ => default
            };

            if (syntax == default) return;

            var symbol = context.SemanticModel
                .GetSymbolInfo(syntax, context.CancellationToken)
                .Symbol;

            var result = symbol switch
            {
                IFieldSymbol fieldSymbol
                    => fieldSymbol.GetAttributes()
                        .Any(x => x.AttributeClass.Name.Contains("ReadonlyInScriptAttribute")),
                IPropertySymbol propertySymbol 
                    => IsAutoProperty(propertySymbol) && HasReadonlyAttribute(propertySymbol),
                _ => false,
            };

            if (result)
            {
                // Diagnosticを作ってReportDiagnosticに詰める。
                var diagnostic = Diagnostic.Create(Rule, context.Node.GetLocation());
                context.ReportDiagnostic(diagnostic);
            }

            bool IsAutoProperty(IPropertySymbol propertySymbol)
            {
                var node = propertySymbol.DeclaringSyntaxReferences.First().GetSyntax();
                if (node is not PropertyDeclarationSyntax propertyDeclarationSyntax) return false;
                return propertyDeclarationSyntax.AccessorList.Accessors.All(a => a.Body == null);
            }

            bool HasReadonlyAttribute(IPropertySymbol propertySymbol)
            {
                var node = propertySymbol.DeclaringSyntaxReferences.First().GetSyntax();
                if (node is not PropertyDeclarationSyntax propertyDeclarationSyntax) return false;
                return propertyDeclarationSyntax.DescendantNodes()
                    .OfType<AttributeSyntax>()
                    .Any(a => a.Name.ToString().Contains("ReadonlyInScript"));
            }
        }

飽くまでもコードを分析しているだけなので、エディタからの編集は何も問題無いというわけです。

これの何が嬉しいの?って言われたらわからないですが、 アナライザのコードとdllは以下のリポジトリに公開しています。 github.com

以下のページなどを参考に、dllをポイっとUnityにインポートして、ReadonlyInScriptAtributeって名前の属性を作ればOKです。

docs.unity3d.com

Rustでバックエンド開発ができるフレームワークTauri

Tauri

tauri.app

Rustでネイティブアプリ開発ができる、というところでTauriを知りましたが、実際のところRustでの開発しかできないわけではないです。 フロントエンドもバックエンドも使える言語やフレームワークは結構自由で、いくつか用意されているテンプレートから始められるサポートがあるんですが、 個人的にはフロントがReact + TypeScript、バックがRustが鉄板かなと思っています。

  • ビルドのサポート(Github Actionsでマルチプラットフォーム向けにビルドできるサポートがある)
  • WebView2とかWebkitを利用してるからアプリそのものに描画フレームワークが含まれてなくてアプリサイズが小さい
  • Rust環境を使えば特にそう感じますが、依存パッケージの管理とか環境構築が楽

あたりが実際にオススメできるポイントなんじゃないかと思います。

まだ開発中ではありますが、モバイル環境向けにもビルドできるようになるようで、PCモバイル共にターゲットにできるならかなり強いですね。

ChatGPTのブームに乗っかって勉強したこと(内容無し)

最近やっていたことのメモ

ChatGPTのブームに乗っかって、自分のあまり詳しくない分野の開発の勉強をしていました。
(簡単にリアクションが返ってくると楽しくてやる気が出るというのは、ゲーム開発初学者がとりあえずゲームエンジンを触ってモチベーションを上げるみたいな感じでとても良い気がしています)

プログラム自体Unity触ってみるところから始めて教育を受けてない人間なので、色々と基礎が無い。
やってみたこととしては以下のようなこと。 とはいえどれもチュートリアル程度です……

  • Node.jsでの簡単な開発
    • TypeScriptでChatGPTにPOSTしてレスポンスをSlackに投稿。2つとも結局はHTTPリクエスト投げてるだけなのでかなり簡単な作業だった。
    • vueとNext.jsを試して簡単なGUIで動かしてみたが、Next.js環境が一番しっくり来た。なんとなく調べながらノリで書けそうな気がする。
  • Slackアプリの開発
    • SlackからChatGPTにPOSTしたかった。
    • Slackから依頼を受けてChatGPTにリクエストするサーバを立てる必要があったのでGCFを使ってみたのだが、APIキーなどを外に置いておきたくないので断念。GCFのチュートリアルだけ済ませて、ローカルサーバに依頼を投げる形で妥協した。
  • Rustライブラリ作成
    • Rust自体は少しかじってたけど、最近の世間のRust熱にあてられて改めて勉強。pyo3ライブラリを使ってpythonから呼び出すのは簡単に作れた。maturinとか色々方法は調べたら出てきたけど、以下のリンクのが一番素直で良いと思った。実用なら、「Pythonから呼ぶ」前提のプロジェクトは気持ち悪いので、Rustプロジェクトの方ではPythonバインド専用のファイルを用意して、詳細なロジックとは切り離した方がよさそうに思う。

zenn.dev

なんもわからん状態からとりあえず色々うっすら先が見える状態になってきて、何か"""力"""を得た感覚がありますが、いわゆる「完全に理解した」状態なのでちゃんと継続して勉強しないとすぐ忘れそうです。