2026春休み開発日記(1)

1月19日(月)

やるぞおおおお!

と、ゲーム用のドット絵を作ろうとしたけど、難しい。

ただのピクセルアート的なドット絵ならLoRAやドット絵風のモデル(Checkpoint)を使って生成すれば、

それっぽいものも出力できるけど、画風やドット絵の解像度的な部分まで指示するのが難しい。

香菱 – 原神

そこで、StableDiffusion に囚われず、柔軟な発想ということで、Nano Banana Pro を活用してみることにした。

まずは、StableDiffusion でちびキャラを描く。

ネフェル – 原神

ここから、Nano Banana Pro に渡してドット絵かをお願いする。プロンプトはこんな感じにした。

pixel art, 8-bit SNES RPG style, Final Fantasy 4 style
ネフェル(8-bit風) – 原神

すごいな🍌…

これ、キャラチップまで作れるんじゃないか??

添付ファイルのような形式で作り直して。
参考画像 – https://civitai.com/images/89203992

ちゃんと作ってくれた。凄すぎる🍌

ネフェル(キャラチップ) – 原神

最後になんちゃってドット絵を本物のドット絵に変換する。

spritefusion-pixel-snapperで変換

ネフェルっぽさを残しつつ、大乱闘タイピング大戦争的なドット絵を作ることが出来た。

あとは、このサイズ感で他のキャラも量産できれば良いのだが…

32×32とか48×48のサイズで作らせるとか、そのへんがコントロールできたらこの方法でいけそうだ。

1月20日(火)

開発を再開するときはいつも、全体像の把握から始まる。半年すると忘れちゃうからね。

箇条書きで書いていたものを Obsidian の Canvas を使ってカラフルにまとめてみた。パット見キレイなんだけど、グループ見出しが大きすぎたり、オブジェクト感の余白の確保のせいでメインのテキストが小さくて見づらく、案外全体が俯瞰して見えないことがわかった。

クラス図 – 大乱闘タイピング大戦争

Obsidian の勉強にはなったが、一回捨てて、作り直そう💪

1月21日(水)

本来は大学の授業がある日だが、先週で講義は終了。晴れて春休みである。

開発する時間も取れると思いきや、今日はICT支援員の作業があって午後の時間は潰れてしまった。

とりあえず、フォルダ・ファイルデータから自動でCanvasデータを生成するスクリプトを作った。

DTDフォルダ・ファイル構成図

スクリプトはChatGPTと相談しながら作成。ビジュアル的な指示はなかなかうまく行かず、結局自分でスクリプトの内容をいじった。

import json
import re
import sys

# ---- Layout Constants ----
TEXT_W = 360            # テキストノード幅
TEXT_H = 40             # テキストノード高さ

GROUP_TEXT_OFFSET_TOP = 20
GROUP_PADDING_TOP = 40
GROUP_PADDING_RIGHT = 20
GROUP_PADDING_BOTTOM = 20
GROUP_PADDING_LEFT = 20

ITEM_GAP_Y = 10         # テキスト同士の縦GAP(※テキスト間はこれのみ)
GROUP_GAP_Y = 40        # ブロック間(グループ絡み)の縦GAP

F_SUFFIXES = set("*@=|>")  # tree -F の末尾記号(/ は別扱い)
LINE_RE = re.compile(r"^(?P<prefix>(?:│   |    )*)(?:├── |└── )(?P<name>.+)$")


class IdGen:
    def __init__(self):
        self.i = 1

    def next(self):
        v = f"n{self.i}"
        self.i += 1
        return v


def parse_lines(lines):
    items = []
    for ln in lines[1:]:
        ln = ln.replace("\xa0", " ").rstrip("\n")
        m = LINE_RE.match(ln)
        if not m:
            continue

        depth = len(m.group("prefix")) // 4
        name = m.group("name").rstrip()

        if name.endswith("/"):
            items.append((depth, name[:-1], True))
            continue

        if name and name[-1] in F_SUFFIXES:
            name = name[:-1]

        items.append((depth, name, False))

    return items


def build_tree(items):
    root = {"name": ".", "children": []}
    stack = [(0, root)]

    for depth, name, is_dir in items:
        while stack and stack[-1][0] > depth:
            stack.pop()

        parent = stack[-1][1]
        node = {"name": name, "children": []} if is_dir else {"name": name}
        parent["children"].append(node)

        if is_dir:
            stack.append((depth + 1, node))

    return root


def _kind(child):
    return "group" if "children" in child else "text"


def layout(node, gx, gy, idgen, nodes):
    # グループ内コンテンツ領域の左上
    content_x = gx + GROUP_PADDING_LEFT
    content_y = gy + GROUP_PADDING_TOP

    children = node.get("children", [])

    # 右端計算(最低でもテキスト1つ分の幅を確保できる初期値)
    max_right = content_x + TEXT_W

    cy = content_y

    for i, child in enumerate(children):
        k = _kind(child)

        if k == "group":
            # 子グループは「親のpadding内」にそのまま置く(INDENTなし)
            next_y, child_w = layout(child, content_x, cy, idgen, nodes)
            max_right = max(max_right, content_x + child_w)
            cy = next_y
        else:

            # --- 連続するテキストの先頭かどうかの判定 ---
            is_first_text = (i == 0) or (_kind(children[i-1]) == "group")
            
            if is_first_text:
                # グループのPADDINGを打ち消して、独自のオフセットを適用
                cy -= GROUP_PADDING_TOP
                cy += GROUP_TEXT_OFFSET_TOP

            nid = idgen.next()
            nodes.append({
                "id": nid,
                "type": "text",
                "x": content_x,
                "y": cy,
                "width": TEXT_W,
                "height": TEXT_H,
                "text": f"**{child['name']}**"
            })
            max_right = max(max_right, content_x + TEXT_W)
            cy += TEXT_H

        # --- 次の要素とのGAP ---
        # テキスト→テキスト:ITEM_GAP_Y のみ
        # それ以外(グループ絡み):GROUP_GAP_Y
        if i < len(children) - 1:
            next_k = _kind(children[i + 1])
            if k == "text" and next_k == "text":
                cy += ITEM_GAP_Y
            else:
                cy += GROUP_GAP_Y

    # コンテンツ末尾(子が無ければ最低高さ用の仮テキストを想定)
    # if children:
    #     content_bottom = cy
    # else:
    #     content_bottom = content_y + TEXT_H
    content_bottom = cy

    # 高さ:コンテンツ末尾 + 下padding(最低高さも保証)
    h = (content_bottom - gy) + GROUP_PADDING_BOTTOM
    # min_h = GROUP_PADDING_TOP + TEXT_H + GROUP_PADDING_BOTTOM
    # h = max(h, min_h)

    # 幅:右端 + 右padding(最低幅も保証)
    w = (max_right - gx) + GROUP_PADDING_RIGHT
    min_w = GROUP_PADDING_LEFT + TEXT_W + GROUP_PADDING_RIGHT
    w = max(w, min_w)

    gid = idgen.next()
    nodes.append({
        "id": gid,
        "type": "group",
        "x": gx,
        "y": gy,
        "width": w,
        "height": h,
        "label": node["name"]
    })

    return gy + h, w


def main():
    if len(sys.argv) != 3:
        print("python tree_to_canvas.py tree.txt output.canvas")
        return

    with open(sys.argv[1], encoding="utf-8") as f:
        lines = f.readlines()

    items = parse_lines(lines)
    tree = build_tree(items)

    idgen = IdGen()
    nodes = []

    # ルートグループは x=0,y=0 から開始(ROOT_GROUP_PADDINGは未導入)
    layout(tree, 0, 0, idgen, nodes)

    with open(sys.argv[2], "w", encoding="utf-8") as f:
        json.dump({"nodes": nodes, "edges": []}, f, ensure_ascii=False, indent=2)


if __name__ == "__main__":
    main()

1月22日(木)

1月23日(金)

1月24日(土)

おっと、2日も忘れてしまった。危ない危ない。

せっかくスクリプトが出来たので、DTDのフォルダーまとめを作ろう。

思い出しながら眺めていくと、ボタン関連の仕様変更が中途半端になっていることを発見!とりあえずリハビリがてら、ボタン関連のスクリプトとファイルの整理から始めることにした。

息抜き。NanoBananaProに作ってもらった。リフターロング、グランカングー、リフターロング、ハイエースバンワイド。2月中旬のハイエースの抽選発表で全ては決まる💪

1月25日(日)

ボタン関連の整理を始めるに当たって、新しいボタン管理システムがどんなものだったのか忘れてしまったので、おさらいから入ることにした。

UniversalButton.cs (【体】Unityオブジェクトに貼るスクリプト)
┃ 
┃ どちらかのルートで「脳(Action)」を受け取る
┃
┣━━ 【静的ルート:事前に準備】 (エディタでの作業)
┃    ┃ 
┃    ┣━ xxxAction.cs (金型:プログラムを書く)
┃    ┃  ┗━ xxx.asset (データ:右クリックで作成。日本語名OK!)
┃    ┃
┃    ┣━ SceneTransitionAction.cs (【脳の設計図】シーン遷移の機能)
┃    ┃ ┃
┃    ┃ ┃ [実体化] 右クリックから「データ」として作成
┃    ┃ ┣━ ToTitle.asset (設定:sceneName = "Title")
┃    ┃ ┗━ ToBattle.asset (設定:sceneName = "Battle")
┃    ┃
┃    ┣━ GachaAction.cs (【脳の設計図】ガチャの機能)
┃    ┃ ┃
┃    ┃ ┣━ SingleGacha.asset (設定:count = 1)
┃    ┃ ┗━ MultiGacha.asset (設定:count = 10)
┃    ┃
┃    ┗━ 活用シーン:タイトル遷移、ガチャ画面を開くなど「決まった動作」
┃
┣━━ 【動的ルート:その場で作る】 (実行時のプログラム処理)
┃    ┃ 
┃    ┣━ ButtonActionFactory.cs (工場:ScriptableObjectをメモリ上に生成)
┃    ┃  ┗━ リフレクション (魔法:private変数に値を流し込む)
┃    ┃
┃    ┗━ 活用シーン:選んだアイテムを使う、入力内容でログインするなど「状況で変わる動作」
┃
┗━━ 実行の瞬間 (クリック時)
     ┃ 
     ┣━ ButtonContext (現場レポート:ボタンの情報を箱に詰める)
     ┗━ Action.Execute(context) (実行:受け取った脳を動かす)

ButtonAction.cs

1月26日(月)

UniversalButton.cs

  • ボタンが押されたときの挙動の指定方法が2つ用意されている
    • 従来のOnXXXXのようなイベント指定
    • 挙動を記述したAssetファイル指定

根本的に分かりづらい。勉強のために背伸びして作ってみたが、設計的に良くなかった。

自分の身の丈にあった仕組みにするようにする。

各Sceneに存在するxxxxManager.csにOnClick_xxxxxxのようなメソッドを配置するようにルール化する。また、シーンの移動など広域なものはGameManager.csに用意してみるのも良いかもしれない。

やってみたが、GamaManagerは各シーンに実体がないので、この案は無理っぽい。ということで、あまり良くないけど、Scene固有のボタンと同じ様に、OnClick_xxxの形で広域なものも重複するが😢メソッドを配置するようにした。

とりあえずの環境で開発を始めたが、UnityとCursorの連携が取れないでいた。UnityにCursorの拡張機能をインストールしてEditorでCursorを指定しているのに再起動すると元に戻ってしまうし、ファイルの更新状況をチェックしてくれず、Cursorでソースを変更してもUnityが反応してコンパイルしてくれない。いろいろドツボにはまりながら検証したところ、使用していたバージョン 2022.3.62f3 がLinuxと相性が悪いらしい。ということで、他のバージョンを色々試したところ、2023.2.22f1 で動いたので、そちらにバージョンアップさせて開発することにした。

1月27日(火)

DeckBuilderSceneの構造設計が独特だった(xxxxManagerがCanvasにアタッチされていた)ので、他のシーンと同じ形に修正。

構造設計は次のように統一する。

  • 各シーンにxxxxManagerというそのシーン全体を管理するクラスを用意する。
  • ヒエラルキーのトップに空のオブジェクトを作成してあタッチする。
  • シーン内のボタンの挙動はxxxManagerにOnClick_xxxxxのようなメソッドを用意する。

1月28日(水)

1月29日(木)

1月30日(金)

今日は朝から夕方まで、市内の学校を回ってiPadの設定をしてきた。忙しかったとはいえ、おサボりをしてしまった!やっぱ朝の時間に開発できないとダメだね〜

開発日記を見直してみると、先週も、木金あたりで、おサボりしていたので、これは修正せねば!

木曜日は朝から夜まで講座なので、やはり、朝イチに時間を取る必要がある。朝起きれるように水曜日は早く寝る。12時までには布団に入るべし🛌

1月31日(土)

OmarchyでCursorの拡張機能などを一回リセットしていたので、Paste Image が入っていなかった。Obsidianのまとめで拡張機能の記事も書かねば…。
トラブった!!!!インストールしても動かず…Geminiに相談してなんとか解決

Paste Imageはxclipを必要としているみたいだけどOmarchyはx11ではなくWaylandなので、クリップボード用のwl-pasteをxclipとして動くようにラッパースクリプトを作る必要があった。

sudo tee /usr/local/bin/xclip <<EOF
#!/bin/sh
wl-paste -t image/png
EOF

sudo chmod +x /usr/local/bin/xclip

2月1日(日)

いやはや、もう2月ですね。大学の成績も登録したので、あとはカリキュラムの見直しをしたら、完全に春休み。忘れないように早めにやろう。

春休みはDTDの開発以外に教材の開発もすることにした。

p5.js用にフォントをロードできるようにPG部にアップすることにした。

JF-Dot-Ayu18.ttf
JF-Dot-K12.ttf
JF-Dot-MPlus10.ttf
JF-Dot-MPlus12.ttf
JF-Dot-Shinonome12.ttf
JF-Dot-Shinonome14.ttf
misaki_gothic.ttf
SNchibi2_5.ttf

このサーバーに置いてあるフォントファイルを読み込むためのサンプルを下記に残す。

let fontAssets = [];
// [ファイル名, 基準サイズ] の二次元配列
let fontData = [
  ['JF-Dot-Ayu18.ttf', 18],
  ['JF-Dot-K12.ttf', 12],
  ['JF-Dot-MPlus10.ttf', 10],
  ['JF-Dot-MPlus12.ttf', 12],
  ['JF-Dot-Shinonome12.ttf', 12],
  ['JF-Dot-Shinonome14.ttf', 14],
  ['misaki_gothic.ttf', 8],
  ['SNchibi2_5.ttf', 5]
];
let fontIndex = 0;

function preload() {
  const baseURL = 'https://pg.pasocafe.jp/WebFonts/';
  for (let i = 0; i < fontData.length; i++) {
    // フォントファイルを読み込んで配列に保存
    fontAssets.push(loadFont(baseURL + fontData[i][0]));
  }
}

function setup() {
  createCanvas(600, 400);
  noSmooth(); // ドットをクッキリさせる設定
}

function draw() {
  background(220);

  let currentFont = fontAssets[fontIndex];
  let baseSize = fontData[fontIndex][1];

  // フォントと基準サイズを適用
  textFont(currentFont);
  textSize(baseSize); 
  
  text('こんにちは、世界! 12345', 50, 100);
  
  // 2倍サイズで表示したい場合など
  textSize(baseSize * 2);
  text('こんにちは、世界! (x2)', 50, 200);
}

2月2日(月)

ドットフォントを配列化するためのプログラムを作った。

AIに嘘をつかれてnoSmooth()に悩まされ…
(結局公式リファレンスを参照してフォントには効果がないことがわかった😂)

フォントの種類に寄ってはベースラインや余白が異なるので、それにも翻弄され…
(textBounds()という便利な命令があたので解決できたのだけど)

大変だった!とはいえ、悩んだ末に解決できると気持ちいいね!

let fontData = [
  ["JF-Dot-Ayu18.ttf", 18], // 0
  ["JF-Dot-K12.ttf", 12], // 1
  ["JF-Dot-MPlus10.ttf", 10], // 2
  ["JF-Dot-MPlus12.ttf", 12], // 3
  ["JF-Dot-Shinonome12.ttf", 12], // 4
  ["JF-Dot-Shinonome14.ttf", 14], // 5
  ["misaki_gothic.ttf", 8], // 6
  ["SNchibi2_5.ttf", 5], // 7
];

let fontIndex = 1;
let currentFont;
let inputChar = "漢";

function preload() {
  const baseURL = "https://pg.pasocafe.jp/WebFonts/";
  currentFont = loadFont(baseURL + fontData[fontIndex][0]);
}

function setup() {
  createCanvas(300, 300);
  background(243, 235, 216);

  // フォントと基準サイズを適用
  let baseSize = fontData[fontIndex][1];
  textFont(currentFont);

  // スケーリング適用
  const drawScale = 20;
  const drawSize = baseSize * drawScale;

  // テキストサイズを設定
  textSize(drawSize);

  // グリッド線の描画
  stroke(107, 93, 57);
  for (let x = 0; x <= drawSize; x += drawScale) {
    line(x, 0, x, drawSize);
  }
  for (let y = 0; y <= drawSize; y += drawScale) {
    line(0, 0 + y, drawSize, y);
  }

  // テキスト描画ベースライン(Y座標)
  let lineY = 0;

  // テキスト描画シミュレーション
  // いくつかのフォントを確認したところ書き出し位置が曖昧になりがちなので補正値を求める
  lineY = drawSize;
  let bbox = currentFont.textBounds(inputChar, 0, lineY);

  // rect(bbox.x, bbox.y, bbox.w, bbox.h);
  // print("想定:x=" + 0 + " y=" + 0 + " w=" + drawSize + " h=" + drawSize);
  // print("想定:x=" + bbox.x + " y=" + bbox.y + " w=" + bbox.w + " h=" + bbox.h);

  // ベースラインを補正
  lineY -= bbox.y;

  // 文字列の描画
  noStroke(); // 枠線=なし
  fill(0); // 塗りつぶし=黒
  text(inputChar, 0, lineY);

  // データ格納用配列作成
  const matrix = [];
  for (let i = 0; i < baseSize; i++) {
    matrix[i] = new Array(baseSize).fill(0);
  }

  // ドット調査
  let bit = 0;
  for (let y = 0; y < baseSize; y++) {
    for (let x = 0; x < baseSize; x++) {
      let posX = x * drawScale + drawScale / 2;
      let posY = y * drawScale + drawScale / 2;
      bit = get(posX, posY + 1)[0];
      fill(255);
      circle(posX, posY, 2);
      if (bit < 128) {
        matrix[y][x] = 1;
      }
      else {
        matrix[y][x] = 0;
      }
    }
  }

  // 配列をコンソールに出力
  let output = "";
  let outputline = "";
  output += "[\n";
  for (let y = 0; y < baseSize; y++) {
    outputline = "  [";
    for (let x = 0; x < baseSize; x++) {
      outputline += matrix[y][x];
      if (x < baseSize - 1) {
        outputline += ",";
      }
    }
    if (y < baseSize - 1) {
      output += outputline + "],\n";
    }
    else {
      output += outputline + "]\n";
    }
  }
  output += "]";
  print(output);
}

2月3日(火)

2月4日(水)

2月5日(木)

む!

2月6日(金)

2月7日(土)

むむ!

流石にコレはマズイ!ということで、早起きしてDTDのプロジェクトを開いた。全体像の把握をしながら、動きを確認していたが、ステージクリアー後のリザルト画面やゲームオーバー時の画面が未完成だったので、そこから作ることにする。

  • ゲームオーバー、ゲームクリアー時にリザルト画面を表示する
    →「リトライ」、「タイトルへ戻る」を表示する⭕️
    or
    →マウスクリックでタイトルへ戻る❌️

コメントする