タタッと!ドラゴンさん🐉

タタッ!と書いてく記事まとめ

UdonSharpで状態は変わるのにアニメーションが動かないときに確認したこと

VRChatワールドで、ボタンを押すと状態は変わるのに、見た目のアニメーションが進まないことがありました。調べてみると、アニメーションの更新が途中で止まっていました。

今回は、遅延イベントのタイミングが原因の候補でした。

1フレーム後でも同じフレーム番号になることがある

UdonSharpでは、次のようにイベントを遅延実行できます。

SendCustomEventDelayedFrames(nameof(_Step), 1);

ただし、Updateより前の入力イベントなどから予約すると、Time.frameCount上の遅延が想定より1フレーム短くなる場合があります。同じフレーム番号のまま _Step が呼ばれることもあります。

参考:VRChat公式ドキュメント — SendCustomEventDelayedFrames

同じフレームの重複防止で更新が止まる

次の図は、更新が止まる可能性のある流れと、今回試した修正を並べたものです。修正前に同一フレームの判定が実際に発生したかは、ログで確認できていません。

修正前:止まる場合修正後:初回を遅らせる
① 開始時に _Step をすぐ実行① 初回の _Step を2フレーム後に予約
② 次の更新を1フレーム後に予約② フレームが進んでから初回の更新を実行
③ 同じフレーム番号で届くと、重複防止で終了③ 次の更新を1フレーム後に予約
④ 次の予約がなくなり、更新が停止④ アニメーションの更新が進む

例えば、次のようなコードです。アニメーション中だけ _Step を繰り返す処理の抜粋です。

private bool _running;
private int _lastFrame = -1;

private void StartLoop()
{
    if (_running) return;
    _running = true;
    _lastFrame = -1;
    _Step(); // 最初はすぐ実行
}

public void _Step()
{
    if (!_running || _lastFrame == Time.frameCount) return;

    _lastFrame = Time.frameCount;

    // ここでアニメーションを更新する

    SendCustomEventDelayedFrames(nameof(_Step), 1);
}

予約した _Step が同じフレーム番号で呼ばれると、重複防止の return で抜けます。この例では次の予約も行われないため、_running が true のまま更新が止まることがあります。

最初の更新を2フレーム後に予約する

今回は、開始時に _Step() を直接呼ばず、最初の更新を2フレーム後に予約するようにしました。

private void StartLoop()
{
    if (_running) return;
    _running = true;
    _lastFrame = -1;

    // 入力直後でも、最初の更新を後のフレームに送る
    SendCustomEventDelayedFrames(nameof(_Step), 2);
}

変更後、VRChatの実クライアントでボタンを繰り返し操作しても、アニメーションが進むようになりました。

ただし、修正前にフレーム番号までは記録していなかったため、実際に同じフレームの判定で止まったかは確定できていません。また、2フレーム遅らせる方法は、古い予約との重複や非表示からの復帰など、すべてのケースを解決するものではありません。

同じ症状が出たら、状態だけでなく、進行値、Time.frameCount、前回処理したフレーム、次のイベントを予約したかも確認すると、原因を絞りやすくなります。

なぜバルクインサート・バルクデリートで統一しないのか

なぜ「複数(バルク)対応」だけにしないのか? すべてを配列([{...}])で受け取るようにすれば、確かにエンドポイントは一つで済みます。 しかし、以下の点がネックになる。

エラーハンドリングが複雑になる: 3件送って「1件成功、2件失敗」したとき、レスポンスをどう返すべきでしょうか?

一部成功を許すのか(HTTP 207 Multi-Status などが必要)。

一つでもダメなら全部ロールバックするのか。 単体APIなら「成功(201)か失敗(400/500)」の2択で済むので、フロントエンド側の処理が圧倒的に楽になえう。

バリデーション(入力チェック)のコスト: 100件のデータを一気に送られた場合、サーバーのメモリやDBの負荷が急増します。単体用APIなら、一回のリクエストに対する負荷を予測・制限しやすくなる。

開発者体験(DX): たった1件更新したいだけなのに、わざわざ [ と ] で囲って送らなければいけないのは、地味にフロントエンド開発者のストレス(とバグの元)になります。

lazygit おすすめの設定

# 1. 差分表示の強化 (Delta)
git:
  paging:
    colorArg: always
    pager: delta --dark --paging=never
  commit:
    signOff: false
    autoWrapCommitMessage: true
    autoWrapWidth: 72
  merging:
    args: '--no-edit'  # マージコミットメッセージ編集スキップ

# 2. UI の見た目と使い勝手
gui:
  language: 'ja'            # 日本語化
  border: 'rounded'         # 角丸でモダンな枠線
  sidePanelWidth: 0.33      # パネル幅を黄金比に
  showIcons: true           # アイコン表示 (フォントが必要)

# 3. パフォーマンス向上
refresher:
  refreshInterval: 10  # 自動更新間隔(秒)
  fetchInterval: 60    # fetch間隔(秒)

# 4. キーバインド調整
keybinding:
  universal:
    quit: 'q'
    return: '<esc>'
    nextTab: ']'
    prevTab: '['
  commits:
    copyCommitMessageToClipboard: '<c-y>'  # コミットメッセージをコピー

# 5. 作業を爆速にするカスタムコマンド
customCommands:
  # 'C' で Conventional Commits 形式のメニューを開く
  - key: 'C'
    command: 'git commit -m "{{index .PromptResponses 0}}: {{index .PromptResponses 1}}"'
    context: 'files'
    description: 'Conventional Commit を作成'
    prompts:
      - type: 'menu'
        title: 'コミット種別を選択'
        options:
          - name: 'feat'
            description: '新機能'
            value: 'feat'
          - name: 'fix'
            description: 'バグ修正'
            value: 'fix'
          - name: 'docs'
            description: 'ドキュメント'
            value: 'docs'
          - name: 'refactor'
            description: 'リファクタリング'
            value: 'refactor'
          - name: 'chore'
            description: '雑用'
            value: 'chore'
      - type: 'input'
        title: 'メッセージを入力'
        initialValue: ''
  
  # 'D' でマージ済みブランチを掃除
  - key: 'D'
    command: 'git branch --merged | grep -v "\*" | xargs -n 1 git branch -d'
    context: 'localBranches'
    description: 'マージ済みのブランチを一括削除'
    loadingText: '削除中...'
  
  # 'F' でブランチから直接ファイルを取得
  - key: 'F'
    command: 'git checkout {{.SelectedLocalBranch.Name}} -- {{.SelectedFile.Name}}'
    context: 'files'
    description: '他ブランチから該当ファイルを取得'
  
  # 'P' でプッシュ前に差分確認
  - key: 'P'
    command: 'git diff {{.SelectedLocalBranch.UpstreamRemote}}/{{.SelectedLocalBranch.UpstreamBranch}}..HEAD'
    context: 'localBranches'
    description: 'プッシュされる差分を表示'

DXF TEXT エンティティの読み方まとめ

はじめに

DXF ファイルは「グループコード(番号)」と「値」の組み合わせで構成されます。\ グループコードは「次の値が何を意味するか」を決める役割を持ちます。

この資料では、TEXT エンティティを例に\ DXF の読み方をわかりやすく整理したメモをまとめています。


基本:DXF の行はこう読む

10
130.39
  • 10 → グループコード(意味:X 座標)\
  • 130.39 → 値(X = 130.39)

DXF はすべてこの形式です。


代表的なグループコード(TEXT 用)

グループコード 意味


0 エンティティ種別 1 テキスト内容 5 ハンドル(ID) 7 テキストスタイル 8 レイヤ名 10 / 20 / 30 挿入位置(X / Y / Z) 11 / 21 / 31 第2基準点 40 文字高さ 50 回転角度 72 水平揃え(左・中央など) 73 生成フラグ


■サンプル TEXT エンティティ(DXF 生データ)

0
TEXT
5
A9
330
C
100
AcDbEntity
8
info
100
AcDbText
10
130.39496288806708
20
40.2434320017132
30
0
40
2.6458333333333335
1
4F : 11840mm
50
0
7
STANDARD
72
0
11
130.39496288806708
21
40.2434320017132
31
0
100
AcDbText
73
2

■サンプルの内容を読み解いた結果(整形表示)

ENTITY TYPE : TEXT
──────────────────────────────────
ハンドル           : A9
レイヤ             : info

表示文字列         : "4F : 11840mm"
文字高さ           : 2.6458333333333335
回転角             : 0°
整列方法           : 左揃え (72 = 0)
文字生成フラグ     : 2

挿入位置(基準点)
  X : 130.39496288806708
  Y : 40.2434320017132
  Z : 0

第2基準点
  X : 130.39496288806708
  Y : 40.2434320017132
  Z : 0

フォントスタイル   : STANDARD

DXF の理解を深めるポイント

  • DXF は すべてが「番号 → 意味 → 値」で構成される\
  • TEXT の本文は グループコード 1\
  • 座標系の基本は 10・20・30\
  • 文字の見た目は 40(高さ)、50(角度)、7(フォント)\
  • エンティティは 0 で始まり、次の 0 の直前までがひとつの塊

zshでIssueごとにBranch作る運用を助ける

Git Helper Functions

Issue単位でブランチを切り替える際の手間を軽減するためのZsh関数集。

gnew - 新しいIssue用ブランチへの切り替え

現在のブランチを削除し、main を最新化してから新しいブランチを作成する。

使い方

gnew <new-branch-name>

例

# feature/issue-123 ブランチで作業完了後、次のIssueへ
gnew feature/issue-456

# 実行される処理:
# 1. git checkout main
# 2. git branch -D feature/issue-123  (現在のブランチを削除)
# 3. git pull
# 4. git checkout -b feature/issue-456

注意事項

  • 未コミットの変更がある場合: git checkout main が失敗するため、先にコミットまたはstashしてください
  • main/master上で実行した場合: ブランチ削除をスキップし、pull → 新ブランチ作成のみ実行
  • PRがマージされていない場合: リモートブランチは残るため、GitHub上で別途削除が必要

セットアップ

~/.zshrc に以下が追加済み:

gnew() {
  local current_branch=$(git branch --show-current)
  local new_branch=$1
  
  if [[ -z "$new_branch" ]]; then
    echo "Usage: gnew <new-branch-name>"
    return 1
  fi
  
  if [[ "$current_branch" == "main" || "$current_branch" == "master" ]]; then
    echo "Already on main/master. Pulling and creating new branch..."
    git pull && git checkout -b "$new_branch"
    return
  fi
  
  echo "Switching from '$current_branch' to new branch '$new_branch'..."
  git checkout main && \
  git branch -D "$current_branch" && \
  git pull && \
  git checkout -b "$new_branch"
}

反映方法

新しいターミナルを開くか、以下を実行:

source ~/.zshrc

【TypeScript】残余プロパティを使った分割代入

残余プロパティを使った分割代入

オブジェクトから特定のプロパティを除外して、残りのプロパティを新しいオブジェクトとして取得するパターン。 書いていて思いついたので、メモしておく!

パターン例

例えば特大オブジェクトがあるとする

// オリジナルのユーザーデータから特定フィールドを除外してコピーを作成
const {
  id: _id,
  password: _password,
  createdAt: _createdAt,
  updatedAt: _updatedAt,
  ...userDataWithoutMetadata
} = originalUserData

このパターンの優れた点

ポイント 説明
Immutable 元のoriginalUserDataを変更せず、新しいオブジェクトを生成
型安全 TypeScriptがuserDataWithoutMetadataの型を自動推論(除外プロパティを除いたOmit<T, 'id' | 'password' | ...>相当)
宣言的 「何を除外するか」が明確に見える
_プレフィックス 「意図的に未使用」を示す慣例(ESLintのno-unused-vars対策にもなる)

代替案の問題点

// NG例1: 手動コピー(DRY違反、メンテナンス困難)
const userDataWithoutMetadata = {
  name: originalUserData.name,
  email: originalUserData.email,
  // ... 大量のプロパティを列挙
}

// NG例2: delete使用(型が崩れる、意図が不明瞭)
const userDataWithoutMetadata = { ...originalUserData }
delete userDataWithoutMetadata.id
delete userDataWithoutMetadata.password

汎用テンプレート

// 特定プロパティを除外してオブジェクトをコピー
const { 
  propToRemove1: _propToRemove1,  // _ = 「意図的に捨てる」
  propToRemove2: _propToRemove2,
  ...rest                          // 残りのプロパティを取得
} = sourceObject

// TypeScriptの型推論
// rest → Omit<typeof sourceObject, 'propToRemove1' | 'propToRemove2'>

ユースケース

  • 新規作成用データ: 既存データからidを除外してPOSTリクエスト用に変換
  • セキュリティ: passwordやtokenを除外してログ出力やレスポンスに使用
  • 状態リセット: 特定のフィールドをクリアしたコピーを作成
  • APIレスポンス整形: 不要なメタデータを除外してフロントに返却

【拡張記事】structuredCloneで関数がクローンできない理由

structuredCloneで関数がクローンできない理由は、もともとStructured Clone Algorithmは他の実行コンテキストにデータを移送したり、データを永続化したりする目的で定義されていたからだと思われます。例えば、他のWindowにpostMessageでオブジェクトを送信するとき、あるいはオブジェクトをIndexedDBに保存する場合にStructured Clone Algorithmが使用されます。

とくに永続化のことを考えると、structuredCloneでクローンできるためにはデータがシリアライズできる必要があります。実際の実装はともかく、structuredCloneの挙動は仕様書上では「シリアライズしてからデシリアライズする」というアルゴリズムで定義されています。

zenn.dev

  1. structuredCloneの元々の目的 javascript// 他のウィンドウにデータ送信 window.postMessage({ name: "太郎", age: 25 }, "*")

// データベースに保存 indexedDB.put({ name: "太郎", age: 25 }) → データを別の場所に移すための仕組み 2. シリアライズの問題 シリアライズ = データを文字列や bytes に変換すること javascript// これはOK(文字列に変換可能) const data = { name: "太郎", age: 25 } JSON.stringify(data) // '{"name":"太郎","age":25}'

// これはNG(関数は文字列にできない) const func = () => console.log("hello") JSON.stringify(func) // undefined 3. 関数が特殊な理由 javascriptlet x = 100 const myFunc = () => x + 1 // 外の変数xを参照

// この関数をコピーしても... // 新しい場所にxは存在しない! 環境依存 = 関数は周りの変数やスコープに依存している → 関数だけコピーしても動かない 簡単に言うと 「関数は周りの環境とセットじゃないと意味がないから、単体ではコピーできない」