2026年9月25日金曜日

少女と学園城クリア

どうやらバットエンドの動画だけ取得できていないようですが、それ以外はあらかた取得した模様。
…いや、紅魔郷じゃないのかよ?と言われそうですが、あれのEXを攻略しようと思ったら老眼の私には半年以上かかりますので。
このゲームは以前言っていたスプリングセールで買ったゲームではあったのですよ…。
本来ゴールデンウィークにやる予定だったのですが、いろいろとゴタゴタがありまして…かといって盆は盆でいろいろと…
このまま引きずるのも嫌だなと思ってイージーモードでシルバーウィークに一気にやったのですが4日かかりました…いや、マップ広すぎだって…。

概要
…まあ、一番目を引くのは「世界観が透き通っている」と評されるキャラデザ。
ジャンルはメトロイドバニアで色んな「ウケる」ゲームや要素を投入して作ったソフト…です。メトロイドバニア系はエンダーリリーズ以来ですね…そう言えばマグノリアも買ったのですが手が付けられていません。


ざっくりとした感想
色々な要素を混ぜ込んでいて、暗に何を参考にしたかを暗示する展示も作中にあったりします(メトロイド/悪魔城ドラキュラ/ポータル…)。
所謂インディゲームな訳でそれは良いのですが…

足すのは良いけどちゃんと割ろう

というのが全体を通した感想でしたかねえ。いろんな意味で「好きな物を盛り込みまくった」けどまとまりにかけていました。

良い点
…じっくりと感想を書くと悪い点を並べてしまうので、まずは良い点を。

キャラデザ
人を選ぶキャラデザだと思いますが…ひとまずウケる画ではあると思います。

音楽
基本的に邪魔にならないBGMです。耳に残るほどの良曲は無いものの煩わしいと感じる事もありません。探索ゲーなので主張が激しいと辛いですからねえ…まあ、元祖の悪魔城ドラキュラはBGMで世界観作ってましたが。

キャラの動き
主人公を筆頭に敵キャラもちゃんとモデリングされよく動きます。

お話
メトロイドバニア系はゴシックホラーかSFホラーになるか、それらをバッサリ切り捨てたコミカル系が多いのですが…
実の所硬派なSFをコミカルで覆い隠している…といった感じで、後期のロックマンみたいなアンバランスさに挑戦しています。
無難にしなかった製作姿勢は褒めるべきでしょう。


ねっとりとした感想※がっつりネタバレします

お話
このゲームのお話の大部分は探索中に拾う読み物を読む事で察する事ができるようになっていまして…2018年にクリアしたアースドーンなんかも読み物で世界観を知るゲームだったと思いますが、ゲーム内ではあまり語られません…なんせ、主人公の正体が謎の中心なのであまり語れないのですよね…

割と中国産のゲームにありがちですが、ポストアポカリプス前後あたりの世界で、舞台となるカーメル学園は外界と隔絶された世界。で、この学園自体は地球人が住める環境があるという惑星カーメルを目指す宇宙船として作られていた…という設定。
近年ではナインソールが巨大宇宙船による移住の話だったような。

まあ、それだけならありがちなのですが、移住先の星の観測中に時間の流れのおかしい「あの惑星」を発見。とんでもなく早く時間が動いたり止まっていたりしたそうですが…
文明の変化に飢えていた「あの惑星」の生命体が「扉(本当に巨大な扉の形をした物体がある謎の空間)」を介してて接触してきて(ベタなSFのように戦争を仕掛けて来たのでは無く、興味本位だったらしい)星間移動の研究が急転。
まず、この惑星の時間にとらわれない「時性流体」がミ〇フスキー粒子並になんでもできるシロモノで、時間を止める/進める/ループさせる事が可能。研究する時間をループさせて、常に最善の結果だけを出し続ければ最高の未来が作れる…と、思ったのだが…
通常の人類では時間をループさせてもそれを認識できず同じ日を続けるだけで結果が変わらないので、時間を行き来できる時性流生命体を使役して次のループへ結果を引き継がせようと画策。
しかし、この時性流生命体は紫色の不定形有機生物で知性はあるもののコミュニケーションすらままならないシロモノだった…
時間を超越できる割に器具に封入できたりはする不思議な生物で人型に近づけるほどコミュニケーションが取りやすくなる事までは判明したのだが…

読み物によると、ある色を見せてからダメージを与え、別の色を見せてから餌を与える…といった動物の学習実験のような所からスタートしたらしいので、人類と会話出来て制御すらできるようになるまでは相当遠大な計画だったようです。
ちなみに、この時の実験中の生命体は最終ステージの敵として出て来るので、マイルドな表現をしたスペースホラーだったりします。
最終的には明示はされないものの、難病だった初代校長の娘と時性流生命体を融合(この時娘の人格は無くなってしまったらしい)して主人公が出来たらしいです。

で、良い結果の未来へ導く人格を持たせる必要があったため、学園全体をループする時間で包んで舞台装置とし、人の役に立つと得票できる生徒会長選挙に主人公を半ば強制的に参加させ、成長を促した…というのがこのゲームのざっくりとしたストーリー。

ループした時間の中に居る一般生徒達はループを実感できないのですが、一部勘づいている生徒やロボット等も居まして…中にはループを実感できるようになってしまい、40年分以上ものループを繰り返した娘も居たりします。


惜しい点:演出
さて、お話を念頭に考えればお気楽な学園物を被った壮大なSFなのですが…
それなら学園物は極力コメディに走って、所々不穏な影が…という演出をすれば良い…ハズなんですけど、どちらの演出もちょっと弱いのですよねえ…
モブキャラに至るまでモデリングしたのなら「逃げた食材を集めて来てくれ」ってサブクエの時等は逃げる食材と追っかけるモブのカットでも挿入すれば生活感が出たハズですが、全体的にモブは動きません。
どうやらボーンを組んでいない固定オブジェクトのようでモブキャラ達が決まったポーズのままずっと動かないのですよ。
ファミコンの村人だって歩き回るくらいはしてますからさすがにココはどうにかして欲しかったですね。

夢の残骸?カジキ&フィッシュヘッド
この手のゲームのお約束であるライバルキャラ&ライバル軍団…のハズなのですが。
主人公と同じ技を使い、主人公をライバル視するカジキの被り物をしているカジキさん。
実のところは人助けをして回っており、助けられた生徒達がファンクラブ的に立ち上げたのが魚類の被り物をした人型敵キャラ集団フィッシュヘッド。
ただ、フィッシュヘッドのボスと8人の中核メンバー以外は狂信者化して各地で暴れているという設定。
…最初にカジキを観た時は別の時間軸の主人公か、ロックマンのブルース(ロックマンより一つ型番が古いプロトタイプのような存在)的なキャラかと思っていましたが…
ネタバレをすれば、主人公のコピーとして作られたがテストの結果廃棄になった…というキャラ。自分の存在意義の為人助けをして回ったが最後は主人公に成り代わろうとして決闘する…という筋。

…なのですが不自然な点があって、ボス戦チュートリアルとして最序盤にフィッシュヘッドの闘技場でカジキと対戦になります。フィッシュヘッドはカジキの狂信者ですが、カジキから見たフィッシュヘッドは赤の他人なので妙な舞台設定です。これはやはり、カジキを筆頭にしたライバル軍団という設定だったお話を途中で変えたのかなあと。
※又、この闘技場だけはモブキャラがちゃんと動いていて、カジキ戦も背景のモブたちが動いて応援していたりします。

カジキは最初の闘技場以降登場せず、最終盤の隠しボスとして登場して先述の決闘を行うのですが…もっと頻繁に出た方が物語が良くなったのでは?と思えてなりませんでした。

惜しい点:マップの仕掛け/演出
この手のゲームでメトロイド系の探索だとあまりやりたくは無いですが、悪魔城系なら期待したいのがマップの大仕掛け。
なのですが、動きに制限の多い8ビット時代ならいざ知らず、2026年のゲームにしては直線的なマップ仕掛けが多い。所謂、真横や上下に動く即死罠ですね。
40年前のファミコンでも悪魔城伝説の振り子渡り(そしてスーパーファミコンのシャンデリア渡り)等の曲線的なマップトラップはあるのですが、直線的な罠が多く、曲線的と言っても回転するレーザートラップくらいなのですよねえ…

このゲームボス、キャラの中には巨大ロボットとかも居るのですが…
メトロイドとかだとボス戦前のマップでボスが背景を飛び回っていたり、背景に残骸が転がっていたりという「匂わせ」が在るのもですが、そういった点が薄いんですよね。
もうちょっとココは盛り上げる所だろっ、とツッコむ部分は多かったです。キャラデザやグラフィックを頑張るなら見せる方の努力もしないと報われないなあと思いました。

割とダメな点:操作系
いろいろ詰め込み過ぎて整理しなかった結果大変な事になったのが操作系…
このゲーム、ジャンプやダッシュはもちろん、所謂フックロープアクション、パリィ、等もあるのですが全てを別のボタンに割り振っています。
唯一スキルパネルボタンだけ攻撃と同時押しで特殊攻撃が出るようになっていますが…
古いゲームのように左の十字キーで移動、右の親指だけでアクションという割り振りではありません。特に両手の人差し指で行う操作に慣れていないとどのボタンがどのアクションだったかが混乱しがちです。
結局、私は難易度をイージーにして、パリィと特殊攻撃を一切せず通常攻撃と強攻撃(フローバースト)のみでゴリ押ししてクリアしました。
ちなみにこの方法でもイージーなら体力0で敵に倒された事はクリアまでに1回だけでした。
慣れたらスタイリッシュにプレーできるのでしょうけど、方向キー+ボタン系の操作の方がプレーに集中できる気がしてなりません。

又、デフォルトで移動に3Dスティックが割り振られているのですが…細かい操作が要求されるゲーム性と噛み合っていません。

マップデザイン的にやたらと難易度の高いジャンプの高度管理を求められるシーンが多いのですが、前述の操作の難解さでかなり大変でした。
エンダーリリーズとかでもこの手のジャンプアクションは苦労したのですが…正直操作手順を手に馴染ませる作業が楽しさより苦行感が強めでした(エンリリはこのテの操作も楽しめたという記憶があるのですが…)。
個人の感想…と言いたい所ですが、苦情が来たのか特に細かい操作が求められるアスレチックマップは発売日の翌日にショートカットできるように仕様変更されています。
※ただし、マップ踏破率100%にするためにはアスレチックマップの途中にある隠し部屋等を見つけないといけない

この操作要る?
前述のテキスト読むメニューなのですが…
普通に考えると、十字キーで各項目にフォーカス、決定ボタンで選択した項目を十字キーで動かせる(ページ送りやテキストのスクロール等)、サブ項目があるなら更に選択して決定ボタン/キャンセルボタンで一つ前の項目に戻る…が、一般的な操作です。なんなら実家のテレビのリモコンですらこういった操作なのですが…

うーん?
一応、この操作では前述の操作のように各テキストを読む為にステップを踏まないといけないと言う操作をすっ飛ばしてアクセスできるという利点はありますが…
正直、こんなにサイドボタンやトリガー割り振る必要あったかなと。非常に操作しづらいです。そもそも順にステップを踏んで情報にアクセスする方法は人間が情報を系統立てて考える事と合致している為スタンダードになっている訳で、革新的というより素人感が出てしまっているという印象が強かったです。


全体を通すと
正に惜しいゲームです。一般的にインディーズと言えば一点豪華主義で「売り」を推してそれ以外を妥協するものですが、全体的にボリュームも十分あり企業レベルの物を仕上げて来ています。
いますが、それだけに「企業レベル」で見られてしまうと操作系の煩雑さはいただけないし、画の出来に対して演出面の弱さが「作りこめていない」と感じられます。
ラストバトルなんて、これまで倒したボスが味方してくれるという、お約束とは言え熱い展開があったりするのですが…文字送りが早くて読めない(オイオイ)なんて悲しいスベり方をしています。
クラウドファンディングで立ち上げたソフトらしいのですが、次回作からちゃんと作りこんだソフトを提供できるようになるのか…今後に期待な内容だと思いました。


蛇足
プレー中5時間近くハマって唯一攻略記事を見る事になったのがクレアさん…
人探しクエストを極限まで簡単にしたら返って難解になったという稀有な事例ではないでしょうか(ネット検索したらハマった人は結構いるらしい)。
発見時期が遅れた為、いきなり病気で倒れている所から始まった彼女のクエストなのですが…序盤で発見した場合は通常時の会話もある模様。
で、彼女は主人公にそっくりな上で同じく外部からの留学生あつかい。…つまり世界観の匂わせをするキャラだったようなのですよね。
最終的には主人公の同族では無く、学園の生徒ですらない(学園の生徒はある程度統制管理された強化人間だったりする)地球人というポジションらしいですが。
序盤で逢っていたら印象が変わったかもしれないので、コレも惜しいポイントでしたね。

考察:時性流生命体って結局なんだったんだろう?
もちろん明示はされていないのですが、考察できる点はいろいろあって…

  • アメーバみたいな見た目だが実は文字を持っていて、文明は停滞してしまっている
  • 人型に近付けるほど意思疎通が可能になる
  • 人類と融合できる(昆虫とも融合できるのだが)
  • 航空学部の生徒の会話の中に宇宙で生活をする事になり、地球に触れる事も通信する事も無くなったらそれは最早宇宙人では?という会話がある

…これ、人類の成れの果てでは?
時間を行き来できる生命体の為、遥か未来だか過去だかは不明ですが…生命体として行きつくところまで行ったのがこの時性流生命体なのでは?
ちなみに、学園ではいくら燃焼させても再生力が上回る為、燃料として時性流生命体を使っていたりします…主人公の同族に対してあんまりな仕打ちな気が…
又、扉を介して「あの惑星」に時性流体等を採掘しに行けば資源問題は解決できる等と書いてあるテキストもあり、考察通りであれば究極の人類の住む星である「あの惑星」を資源採取目的の属国扱いしている描写が割とあります。
そもそも、この「扉」の仕組みをちゃんと解明すれば巨大宇宙船を建造して移動などしなくても惑星カーネルをはじめどんな星にでも行ける訳で、けっこう間が抜けている気がしないでも無いです。

まあ、ネットで一番間が抜けているとツッコまれている点は

惑星間移住船なのに、女性しか居ない学園飛ばしたら一代で全滅して終わりだろ

と言う点なのですが、それはまあ、ゲームの方向性なので…ツッコんだら負けなのでしょう。

2026年9月17日木曜日

円と三角のコリジョン

以前師匠の難題7の際にちょっと触れたカリング判定式…検索したらCGソフト系の解説で単に3D処理を軽減する判定…と解説されている記事が見つかりまして。そら、CGツールの利用から考えれば理解する所はそこだけで良いのだけれども…と思って簡単なソースを書いてみました。

そもカリングとは
はい、巻くを意味するカールの動名詞です。それの判定式ですから…
3頂点が右に巻いているか、左に巻いているか、あるいはどちらでも無い(全て同一点もしくは直線状に並んでいる)かの判定に用いられます。
どちらでもなければ0、右/左なら正もしくは負の値を返すのですが…
どちらが右回りでどちらが左回りかは環境によるのでなんとも言えないです、DirectX と OprnGL でも違いましたし、ゲーム機とCGツールで齟齬が出た事もありました…すり合わせで地獄を見た記憶が…

カリング判定の計算式
(X1-Px)(Y2-Py)-(X2-Px)(Y1-Py)

えらく簡単です。やっている事は3次元の面法線を外積から求める計算から、必要な部分だけを抽出した計算式…らしいのですが、正直外積の理解がイマイチなので綺麗に説明できません。
ちなみに面法線とは Blender では水色に見えるコレ(面に垂直に立つ面の向いている方向を示すベクトル)の事で、コレが手前に向いているか奥に向いているかを抽出しています。

表示処理の軽減:裏面省略
サイコロのような立方体を考えた際、これを3角ポリゴンで表現するとして…

これらの表面に例えば右巻きのマークを描きます。
で、このサイコロを回してみたらどうなるか?を考えてみましょう。
裏面…というか手前側から観えない面は左巻きになりますよね。
ならば、逆巻になったポリゴンは見えないのだから描画処理を省略すれば良い…というのが裏面省略でした。
…が、最近は光の反射計算等を行っている場合もあるので単純に省略すれば良いと言うものでも無くなってきています。

又、応用として、右巻き/左巻きの面の境界になる辺を線で描画すると輪郭線ができるため、アニメ調のアウトライン処理を行う際にも用いられています。


表示処理の軽減:視界によるオブジェクト省略
この図を上からの俯瞰図ととらえ、三角の下の頂点を視点、三角を視野角の範囲とすると青く変色した×マークが視野角の範囲に入った物で、それ以外は見えていない物になります。
ならば見えていないオブジェクトを省略すれば処理を大きく省略できます。もちろん光の反射等もあるので(以下略

どう判定するのかと言えば…
視野の範囲の三頂点がどちら周りかを調べ…
判定する点と各辺の頂点がどちら周りになるのかを調べます。
これらが全て同じ方向に回転している場合、判定する頂点は三角の中にあると判ります。


視界の範囲…を三角で示しましたが「視界って距離依存だから三角ではなくないか?」という当然の疑問が出ます。はい、正確には円の判定を組み合わせて距離の判定も行います…


円の範囲判定

(Px-Ox)(Px-Ox)+(Py-Oy)(Py-Oy)

これを円の半径の二乗と比較します。式自体はピタゴラスの定理って奴ですね。
直角三角形の底辺の二乗と高さの二乗の値が斜辺の二乗の値に等しいという奴です。
この斜辺の二乗と半径の二乗を比較して半径の二乗の方が大きければ円の範囲内に入っている事になります。

より正確には斜辺の二乗の平方根が斜辺の長さになり(あたり前)、この値と半径の長さを比較すれば良いのだけど、平方根の計算は非常に重い処理である事と、二乗の場合必ず正の値になる為比較が用意である事、各値の絶対値の大小関係と二乗の大小関係が同じである事から二乗の値で比較を行います。


でもって二つの処理を重ねた物がこんな感じ

ソースここから

import pyxel
import random

SCREEN_SIZE_W = 320
SCREEN_SIZE_H = 240

OBJ_MAX = 100
RADIUS = 50

def JudgCircle(px,py,circle_x,circle_y,r):
    
    x = px - circle_x
    y = py - circle_y
    
    if r*r >= x*x+y*y:
        return True
    return False

def JudgCulling(px,py,ax,ay,bx,by):
    return (ax-px)*(by-py)-(bx-px)*(ay-py)

def JudgTriangle(px,py,ax,ay,bx,by,cx,cy):
    f = JudgCulling(ax,ay,bx,by,cx,cy)
    f1 = JudgCulling(px,py,ax,ay,bx,by)
    f2 = JudgCulling(px,py,bx,by,cx,cy)
    f3 = JudgCulling(px,py,cx,cy,ax,ay)
    
    if f == 0:
        return False

    if f < 0 and f1 < 0 and f2 < 0 and f3 < 0:
        return True

    if f > 0 and f1 > 0 and f2 > 0 and f3 > 0:
        return True

    return False

class Obj:
    def __init__(self):
        x = 0
        y = 0
        color = 2
    
class App:

    def __init__(self):
        # 画面サイズ 320x240 で初期化
        pyxel.init(SCREEN_SIZE_W, SCREEN_SIZE_H, title="judg")
        
        self.object = []
        for i in range(OBJ_MAX):
            self.object.append(Obj())
            self.object[i].x = random.randint(0,SCREEN_SIZE_W)
            self.object[i].y = random.randint(0,SCREEN_SIZE_H)
            self.object[i].color = 2
        
        pyxel.run(self.update, self.draw)

    def update(self):
        pass

    def draw(self):
        # 画面を黒(色番号0)でクリア
        pyxel.cls(0)
        
        mx = pyxel.mouse_x
        my = pyxel.mouse_y
        
        t1x = mx + int(RADIUS/2)
        t1y = my - RADIUS
        t2x = mx - int(RADIUS/2)
        t2y = my - RADIUS

        pyxel.circb(mx,my,RADIUS,15)
        
        pyxel.line(mx,my,t1x,t1y,15)
        pyxel.line(mx,my,t2x,t2y,15)
        pyxel.line(t1x,t1y,t2x,t2y,15)
        
        for i in range(OBJ_MAX):
            self.object[i].color = 2
            
            if JudgCircle(self.object[i].x,self.object[i].y,mx,my,RADIUS):
                if JudgTriangle(self.object[i].x,self.object[i].y,mx,my,t1x,t1y,t2x,t2y):
                    self.object[i].color = 3
            
            pyxel.line(self.object[i].x-5,self.object[i].y-5,
                       self.object[i].x+5,self.object[i].y+5,
                       self.object[i].color)
            pyxel.line(self.object[i].x+5,self.object[i].y-5,
                       self.object[i].x-5,self.object[i].y+5,
                       self.object[i].color)
App()

ソースここまで

…三角だけの時とそこまで変わらなくない?という気がしないでもないですが、オブジェクト表示そのものを省略できるならかなりの軽量化になりますし、
円の判定計算は観ての通り非常に軽いのでやっておいて損は無いと思います。


はい、そんなわけで円と三角のコリジョン判定式でした
ぶっちゃけた話、2Dアクションゲームの当たり判定のほとんどはこの二つの式で片付きます。昔はよく取り扱ったなあと思い、書いてみました。

2026年9月15日火曜日

これはなつかしい

東方紅魔郷NC
Steam見たら懐かしいものが見えて思わず購入…
だいぶ前に、東方はUSB接続のSEGAコントローラーが壊れたため、当時エクストラを攻略中だった地霊殿を最後に引退した…と、書いていたのですが、今の私にはセガコン用コンバータと純正のセガコンがあるのですよ。
これならチョン避けできるはず…

初見でなんとかEasy咲夜さん倒せましたが…
多分、15年ぶり位に紅魔郷をプレー
まず言えるのが画面がすごい見やすい。そして、意外と自機もボスもデカかったんだなあと思いました。
あと、3面あたりからぼちぼちピチュったのですが…。
咲夜さんの所で、やっと低速ボタンに気づく。そうか、ショットボタン押しっぱなしで低速になるのは後の作品だったかと。
あと、低速にならないと自分の当たり判定出ないから霊夢と言えどかすりが分かりづらい。

案外覚えているものもありまして
イージー名物のチルノの正面安地は健在。当時はバカの代名詞でした。

やっぱり避けられないメイド秘技
イージーでも2度も軌道が変わる。老眼のじじいに気合避けは無理ですがな。

…うーん頑張ってフランちゃん討伐までやろうかなあ…
地霊まではEX攻略してたんですけどねえ…いかんせん体が当時の様に動きません、特に目が…

2026年9月12日土曜日

師匠の難題8

〆たばかりだったのですが、前回アップしたソースを修正して一つ気づいた部分がありまして…
どうせやるならタイルコピーによる疑似アルファ処理を追加しようかな?と。
タイル処理による疑似アルファとはなんぞや?という話ですが、コレは90年代に見られた処理で…以前行った大カプコン展でもちらっと紹介されてました。

50%タイル

25%タイル

はい、2×2ドットの範囲で転送するドットとしないドットを決める事で疑似的に半透明っぽく見せる処理です。
又、表示するドットをずらす事で重なりを表現する事も可能です…
こうしてみると、全て転送した時に比べて画像のノイジーさがあまり気にならなくなりますね…

コードはかなり冗長になりますので…定数定義の部分と RollBlt クラスのみ抜粋

ソースここから

import pyxel

SCREEN_SIZE_W = 160
SCREEN_SIZE_H = 120
CENTER_X = 80
CENTER_Y = 60
EMPTY = -32767

ALPHA_NON       = 0
ALPHA_HALF_1    = 1
ALPHA_HALF_2    = 2
ALPHA_QUARTER_1 = 3
ALPHA_QUARTER_2 = 4
ALPHA_QUARTER_3 = 5
ALPHA_QUARTER_4 = 6

中略:第7回参照

class RollBlt:
    
    def __init__(self):
        self.buff = []
        for i in range(SCREEN_SIZE_H):
            self.buff.append(LineBuffer())

        self.top = 0
        self.bottom = 0

        self.vbre = VBresenham()
        self.bre  = Bresenham()

    def setup(self):
        pass
    
    # 原点座標中央位置補正コピー    
    def blt_c(self,dest_x, dest_y, bank, src_x, src_y, src_w, src_h, angle, scale_w=128, scale_h=128,alpha=ALPHA_NON):
        h = src_h * scale_h >> 8
        w = src_w * scale_w >> 8
        
        x = dest_x - my_sin(angle,h) - my_sin(angle+32,w)
        y = dest_y - my_cos(angle,h) - my_cos(angle+32,w)
        self.blt(x, y, bank, src_x, src_y, src_w, src_h, angle, scale_w, scale_h, alpha)
        
    def blt(self,dest_x, dest_y, bank, src_x, src_y, src_w, src_h, angle, scale_w=128, scale_h=128, alpha=ALPHA_NON):

        # 角度のまるめ
        angle &= 127

        # 原点から見た縦方向の端の座標
        h = src_h * scale_h >> 7
        tv_x = my_sin(angle,h-1)+dest_x
        tv_y = my_cos(angle,h-1)+dest_y

        # 原点から見た横方向の端の座標
        w = src_w * scale_w >> 7
        th_x = my_sin(angle+32,w-1)+dest_x
        th_y = my_cos(angle+32,w-1)+dest_y
        
        # 4頂点と比較し上下の座標を求める
        self.top = dest_y
        if tv_y < self.top:
            self.top = tv_y
        if th_y < self.top:
            self.top = th_y
        if (tv_y + th_y - dest_y) < self.top:
            self.top = tv_y + th_y - dest_y
        if self.top < 0:
            self.top = 0

        self.bottom = dest_y
        if tv_y > self.bottom:
            self.bottom = tv_y
        if th_y > self.bottom:
            self.bottom = th_y
        if (tv_y + th_y - dest_y) > self.bottom:
            self.bottom = tv_y + th_y - dest_y
        if self.bottom > (SCREEN_SIZE_H-1):
            self.bottom = SCREEN_SIZE_H-1

        # テーブル初期化
        for y in range (self.top, self.bottom+1):
            self.buff[y].l = EMPTY
            self.buff[y].r = EMPTY

        # 画像縦方向処理
        reg = self.vbre.setup(dest_x,dest_y,tv_x,tv_y)
        self.bre.setup(src_h-1,reg-1)

        match angle:
            # 垂直にコピーする時は縦方向の処理を行わない
            case a if a == 32 or a == 96:
                pass
                
            case a if a < 32:
                
                for i in range (reg):
                    x,y = self.vbre.step()
                    add_src_y = self.bre.step()

                    # MainSide
                    if y >= 0 and y < SCREEN_SIZE_H:
                        
                        if self.buff[y].l == EMPTY:
                            self.buff[y].l = x
                        #pyxel.pset(x,y,6)

                        self.buff[y].src_sx = src_x
                        self.buff[y].src_sy = src_y + add_src_y
                    
                    x = x - dest_x + th_x
                    y = y - dest_y + th_y

                    # SubSide
                    if y >= 0 and y < SCREEN_SIZE_H:

                        self.buff[y].r = x
                        #pyxel.pset(x,y,6)
                        
                        self.buff[y].src_tx = src_x + src_w-1
                        self.buff[y].src_ty = src_y + add_src_y

            case a if a < 64:

                for i in range (reg):
                    x,y = self.vbre.step()
                    add_src_y = self.bre.step()
                    
                    # MainSide
                    if y >= 0 and y < SCREEN_SIZE_H:

                        self.buff[y].r = x
                        #pyxel.pset(x,y,6)

                        self.buff[y].src_tx = src_x
                        self.buff[y].src_ty = src_y + add_src_y
                        
                    x = x - dest_x + th_x
                    y = y - dest_y + th_y
                    
                    # SubSide
                    if y >= 0 and y < SCREEN_SIZE_H:

                        if self.buff[y].l == EMPTY:
                            self.buff[y].l = x
                        #pyxel.pset(x,y,6)

                        self.buff[y].src_sx = src_x + src_w-1
                        self.buff[y].src_sy = src_y + add_src_y
            
            case a if a < 96:

                for i in range (reg):
                    x,y = self.vbre.step()
                    add_src_y = self.bre.step()
                    
                    # MainSide
                    if y >= 0 and y < SCREEN_SIZE_H:

                        if self.buff[y].r == EMPTY:
                            self.buff[y].r = x
                        #pyxel.pset(x,y,6)

                        self.buff[y].src_tx = src_x
                        self.buff[y].src_ty = src_y + add_src_y
                    
                    x = x - dest_x + th_x
                    y = y - dest_y + th_y
                    
                    # SubSide
                    if y >= 0 and y < SCREEN_SIZE_H:

                        self.buff[y].l = x
                        #pyxel.pset(x,y,6)

                        self.buff[y].src_sx = src_x + src_w-1
                        self.buff[y].src_sy = src_y + add_src_y

            case _:

                for i in range (reg):
                    x,y = self.vbre.step()
                    add_src_y = self.bre.step()
                
                    # MainSide
                    if y >= 0 and y < SCREEN_SIZE_H:

                        self.buff[y].l = x
                        #pyxel.pset(x,y,6)

                        self.buff[y].src_sx = src_x
                        self.buff[y].src_sy = src_y + add_src_y

                    x = x - dest_x + th_x
                    y = y - dest_y + th_y
                    
                    # SubSide
                    if y >= 0 and y < SCREEN_SIZE_H:

                        if self.buff[y].r == EMPTY:
                            self.buff[y].r = x
                        #pyxel.pset(x,y,6)

                        self.buff[y].src_tx = src_x + src_w-1
                        self.buff[y].src_ty = src_y + add_src_y

        # 画像横方向処理
        reg = self.vbre.setup(dest_x,dest_y,th_x,th_y)
        self.bre.setup(src_w-1,reg-1)

        match angle:
            # 水平にコピーする時は横方向の処理を行わない
            case a if angle == 0 or angle == 64:
                pass
                
            case a if a < 32:
                
                for i in range (reg):
                    x,y = self.vbre.step()
                    add_src_x = self.bre.step()
                    
                    # MainSide
                    if y >= 0 and y < SCREEN_SIZE_H:

                        if self.buff[y].l == EMPTY:
                            self.buff[y].l = x
                        #pyxel.pset(x,y,6)

                        self.buff[y].src_sx = src_x + add_src_x
                        self.buff[y].src_sy = src_y
                    
                    x = x - dest_x + tv_x
                    y = y - dest_y + tv_y

                    # SubSide
                    if y >= 0 and y < SCREEN_SIZE_H:

                        self.buff[y].r = x
                        #pyxel.pset(x,y,6)

                        self.buff[y].src_tx = src_x + add_src_x
                        self.buff[y].src_ty = src_y + src_h-1
                
            case a if a < 64:

                for i in range (reg):
                    x,y = self.vbre.step()
                    add_src_x = self.bre.step()
                
                    # MainSide
                    if y >= 0 and y < SCREEN_SIZE_H:

                        self.buff[y].l = x
                        #pyxel.pset(x,y,6)

                        self.buff[y].src_sx = src_x + add_src_x
                        self.buff[y].src_sy = src_y
                    
                    x = x - dest_x + tv_x
                    y = y - dest_y + tv_y

                    # SubSide
                    if y >= 0 and y < SCREEN_SIZE_H:

                        if self.buff[y].r == EMPTY:
                            self.buff[y].r = x
                        #pyxel.pset(x,y,6)

                        self.buff[y].src_tx = src_x + add_src_x
                        self.buff[y].src_ty = src_y + src_h-1

            case a if a < 96:

                for i in range (reg):
                    x,y = self.vbre.step()
                    add_src_x = self.bre.step()

                    # MainSide
                    if y >= 0 and y < SCREEN_SIZE_H:

                        if self.buff[y].r == EMPTY:
                            self.buff[y].r = x
                        #pyxel.pset(x,y,6)

                        self.buff[y].src_tx = src_x + add_src_x
                        self.buff[y].src_ty = src_y
                    
                    x = x - dest_x + tv_x
                    y = y - dest_y + tv_y

                    # SubSide
                    if y >= 0 and y < SCREEN_SIZE_H:

                        self.buff[y].l = x
                        #pyxel.pset(x,y,6)

                        self.buff[y].src_sx = src_x + add_src_x
                        self.buff[y].src_sy = src_y + src_h-1

            case _:
                
                for i in range (reg):
                    x,y = self.vbre.step()
                    add_src_x = self.bre.step()
                    
                    # MainSide
                    if y >= 0 and y < SCREEN_SIZE_H:

                        self.buff[y].r = x
                        #pyxel.pset(x,y,6)

                        self.buff[y].src_tx = src_x + add_src_x
                        self.buff[y].src_ty = src_y
                    
                    x = x - dest_x + tv_x
                    y = y - dest_y + tv_y

                    # SubSide
                    if y >= 0 and y < SCREEN_SIZE_H:

                        if self.buff[y].l == EMPTY:
                            self.buff[y].l = x
                        #pyxel.pset(x,y,6)

                        self.buff[y].src_sx = src_x + add_src_x
                        self.buff[y].src_sy = src_y + src_h-1

        # バッファを基に転送を行う

        # タイル処理なし
        if alpha == ALPHA_NON:
            for y in range (self.top, self.bottom+1):
                reg = self.vbre.setup(self.buff[y].src_sx,self.buff[y].src_sy,self.buff[y].src_tx,self.buff[y].src_ty)
                self.bre.setup(reg-1,self.buff[y].r - self.buff[y].l)
                
                sx,sy = self.vbre.step()
                old_src = 0
                new_src = 0

                for x in range (self.buff[y].l,self.buff[y].r+1):
                    new_src = self.bre.step()
                    for step_src in range (old_src, new_src):
                        sx,sy = self.vbre.step()
                    old_src = new_src
                    
                    # 画面左右端処理
                    if x < 0:
                        continue
                    if x >= SCREEN_SIZE_W:
                        break
                    
                    pyxel.blt(x, y, 0, sx, sy, 1, 1)

        # タイル処理50% 1
        if alpha == ALPHA_HALF_1:
            for y in range (self.top, self.bottom+1):
                reg = self.vbre.setup(self.buff[y].src_sx,self.buff[y].src_sy,self.buff[y].src_tx,self.buff[y].src_ty)
                self.bre.setup(reg-1,self.buff[y].r - self.buff[y].l)
                
                sx,sy = self.vbre.step()
                old_src = 0
                new_src = 0

                for x in range (self.buff[y].l,self.buff[y].r+1):
                    new_src = self.bre.step()
                    for step_src in range (old_src, new_src):
                        sx,sy = self.vbre.step()
                    old_src = new_src
                    
                    # 画面左右端処理
                    if x < 0:
                        continue
                    if x >= SCREEN_SIZE_W:
                        break
                        
                    if x&1 == y&1:
                        pyxel.blt(x, y, 0, sx, sy, 1, 1)

        # タイル処理50% 2
        if alpha == ALPHA_HALF_2:
            for y in range (self.top, self.bottom+1):
                reg = self.vbre.setup(self.buff[y].src_sx,self.buff[y].src_sy,self.buff[y].src_tx,self.buff[y].src_ty)
                self.bre.setup(reg-1,self.buff[y].r - self.buff[y].l)
                
                sx,sy = self.vbre.step()
                old_src = 0
                new_src = 0

                for x in range (self.buff[y].l,self.buff[y].r+1):
                    new_src = self.bre.step()
                    for step_src in range (old_src, new_src):
                        sx,sy = self.vbre.step()
                    old_src = new_src
                    
                    # 画面左右端処理
                    if x < 0:
                        continue
                    if x >= SCREEN_SIZE_W:
                        break
                        
                    if x&1 != y&1:
                        pyxel.blt(x, y, 0, sx, sy, 1, 1)

        # タイル処理25% 1
        if alpha == ALPHA_QUARTER_1:
            for y in range (self.top, self.bottom+1):
                reg = self.vbre.setup(self.buff[y].src_sx,self.buff[y].src_sy,self.buff[y].src_tx,self.buff[y].src_ty)
                self.bre.setup(reg-1,self.buff[y].r - self.buff[y].l)
                
                sx,sy = self.vbre.step()
                old_src = 0
                new_src = 0

                for x in range (self.buff[y].l,self.buff[y].r+1):
                    new_src = self.bre.step()
                    for step_src in range (old_src, new_src):
                        sx,sy = self.vbre.step()
                    old_src = new_src
                    
                    # 画面左右端処理
                    if x < 0:
                        continue
                    if x >= SCREEN_SIZE_W:
                        break
                        
                    if (x&1) + (y&1)==0:
                        pyxel.blt(x, y, 0, sx, sy, 1, 1)

        # タイル処理25% 2
        if alpha == ALPHA_QUARTER_2:
            for y in range (self.top, self.bottom+1):
                reg = self.vbre.setup(self.buff[y].src_sx,self.buff[y].src_sy,self.buff[y].src_tx,self.buff[y].src_ty)
                self.bre.setup(reg-1,self.buff[y].r - self.buff[y].l)
                
                sx,sy = self.vbre.step()
                old_src = 0
                new_src = 0

                for x in range (self.buff[y].l,self.buff[y].r+1):
                    new_src = self.bre.step()
                    for step_src in range (old_src, new_src):
                        sx,sy = self.vbre.step()
                    old_src = new_src
                    
                    # 画面左右端処理
                    if x < 0:
                        continue
                    if x >= SCREEN_SIZE_W:
                        break
                        
                    if ((x+1)&1) + (y&1)==0:
                        pyxel.blt(x, y, 0, sx, sy, 1, 1)

        # タイル処理25% 3
        if alpha == ALPHA_QUARTER_3:
            for y in range (self.top, self.bottom+1):
                reg = self.vbre.setup(self.buff[y].src_sx,self.buff[y].src_sy,self.buff[y].src_tx,self.buff[y].src_ty)
                self.bre.setup(reg-1,self.buff[y].r - self.buff[y].l)
                
                sx,sy = self.vbre.step()
                old_src = 0
                new_src = 0

                for x in range (self.buff[y].l,self.buff[y].r+1):
                    new_src = self.bre.step()
                    for step_src in range (old_src, new_src):
                        sx,sy = self.vbre.step()
                    old_src = new_src
                    
                    # 画面左右端処理
                    if x < 0:
                        continue
                    if x >= SCREEN_SIZE_W:
                        break
                        
                    if (x&1) + ((y+1)&1)==0:
                        pyxel.blt(x, y, 0, sx, sy, 1, 1)

        # タイル処理25% 4
        if alpha == ALPHA_QUARTER_4:
            for y in range (self.top, self.bottom+1):
                reg = self.vbre.setup(self.buff[y].src_sx,self.buff[y].src_sy,self.buff[y].src_tx,self.buff[y].src_ty)
                self.bre.setup(reg-1,self.buff[y].r - self.buff[y].l)
                
                sx,sy = self.vbre.step()
                old_src = 0
                new_src = 0

                for x in range (self.buff[y].l,self.buff[y].r+1):
                    new_src = self.bre.step()
                    for step_src in range (old_src, new_src):
                        sx,sy = self.vbre.step()
                    old_src = new_src
                    
                    # 画面左右端処理
                    if x < 0:
                        continue
                    if x >= SCREEN_SIZE_W:
                        break
                        
                    if (x&1) + (y&1)==2:
                        pyxel.blt(x, y, 0, sx, sy, 1, 1)

class App:

    def __init__(self):
        # 画面サイズ 160x120 で初期化
        pyxel.init(SCREEN_SIZE_W, SCREEN_SIZE_H, title="RollBlt")
        self.add_x = 0
        pyxel.load("yajirushi.pyxres")

        self.rb = RollBlt()
        
        self.angle  = 0
        self.angle2 = 0

        pyxel.run(self.update, self.draw)

    def update(self):
        self.angle += 1
        self.angle &= 127
        
        self.angle2 -= 1
        self.angle2 &= 127

    def draw(self):
        # 画面を黒(色番号0)でクリア
        pyxel.cls(0)
        
        #self.rb.blt_c(CENTER_X, CENTER_Y, 0, 0, 0, 32, 16, self.angle,scale_w=64,scale_h=64)
        #self.rb.blt_c(CENTER_X, CENTER_Y, 0, 0, 0, 32, 16, self.angle,scale_w=256,scale_h=64)
        ##
        """
        self.rb.blt_c(CENTER_X, CENTER_Y, 0, 0, 0, 32, 16, self.angle,scale_w=512,scale_h=512, alpha=ALPHA_HALF_1)
        self.rb.blt_c(CENTER_X, CENTER_Y, 0, 0, 0, 32, 16, self.angle2,scale_w=512,scale_h=512, alpha=ALPHA_HALF_2)
        """
        self.rb.blt_c(CENTER_X-30, CENTER_Y-20, 0, 0, 0, 32, 16, self.angle,scale_w=512,scale_h=512, alpha=ALPHA_QUARTER_1)
        self.rb.blt_c(CENTER_X-30, CENTER_Y+20, 0, 0, 0, 32, 16, self.angle2,scale_w=512,scale_h=512, alpha=ALPHA_QUARTER_4)
        self.rb.blt_c(CENTER_X+30, CENTER_Y-20, 0, 0, 0, 32, 16, self.angle,scale_w=512,scale_h=512, alpha=ALPHA_QUARTER_2)
        self.rb.blt_c(CENTER_X+30, CENTER_Y+20, 0, 0, 0, 32, 16, self.angle2,scale_w=512,scale_h=512, alpha=ALPHA_QUARTER_3)

App()


ソースここまで

各 ALPHA が if 文で括られていますが違いは Pyxel.Blt を呼ぶかの判定式のみ
ただ、再三書いている通り、ループの中に分岐を書くのでは無く、分岐の中にループを書かないと処理が重くなるのでこのような冗長な記述になります。
…というか、この4重アルファの時点で Celeron 2コアマシンでは処理落ちが始まるのですよ…以前、処理の遅さを感じられるからプログラミングの訓練にはロースペックマシンが良いと書いたのですが、いきなり実感する事になりました。
さすがにこれ以上の発展は…やり始めるとパレットモードで3Dモデルを表示するだのおかしな話になりますし、そうなると Python より C++ で書かせろという話になります。
いや、ゲーム自体を作れば…は流石に何年かかるか判りませんし…又なにかあったら追記するかもしれませんが、ひとまずこれでホントの締めになると思います。

2026年9月11日金曜日

師匠の難題7

はい、今回予告通り〆となります。

ソースここから

import pyxel

SCREEN_SIZE_W = 160
SCREEN_SIZE_H = 120
CENTER_X = 80
CENTER_Y = 60
EMPTY = -32767

my_table = (
省略:第5回参照

def my_sin(angle,r):
省略:第5回参照
    
def my_cos(angle,r):
省略:第5回参照

class VBresenham:
省略:第5回参照

class Bresenham:
省略:第5回参照

class LineBuffer:
省略:第5回参照

class RollBlt:
    
    def __init__(self):
        self.buff = []
        for i in range(SCREEN_SIZE_H):
            self.buff.append(LineBuffer())

        self.top = 0
        self.bottom = 0

        self.vbre = VBresenham()
        self.bre  = Bresenham()

    def setup(self):
        pass
    
    # 原点座標中央位置補正コピー    
    def blt_c(self,dest_x, dest_y, bank, src_x, src_y, src_w, src_h, angle, scale_w=128, scale_h=128):
        h = src_h * scale_h >> 8
        w = src_w * scale_w >> 8
        
        x = dest_x - my_sin(angle,h) - my_sin(angle+32,w)
        y = dest_y - my_cos(angle,h) - my_cos(angle+32,w)
        self.blt(x, y, bank, src_x, src_y, src_w, src_h, angle, scale_w, scale_h)
        
    def blt(self,dest_x, dest_y, bank, src_x, src_y, src_w, src_h, angle, scale_w=128, scale_h=128):

        # 角度のまるめ
        angle &= 127

        # 原点から見た縦方向の端の座標
        h = src_h * scale_h >> 7
        tv_x = my_sin(angle,h-1)+dest_x
        tv_y = my_cos(angle,h-1)+dest_y

        # 原点から見た横方向の端の座標
        w = src_w * scale_w >> 7
        th_x = my_sin(angle+32,w-1)+dest_x
        th_y = my_cos(angle+32,w-1)+dest_y
        
        # 4頂点と比較し上下の座標を求める
        self.top = dest_y
        if tv_y < self.top:
            self.top = tv_y
        if th_y < self.top:
            self.top = th_y
        if (tv_y + th_y - dest_y) < self.top:
            self.top = tv_y + th_y - dest_y
        if self.top < 0:
            self.top = 0

        self.bottom = dest_y
        if tv_y > self.bottom:
            self.bottom = tv_y
        if th_y > self.bottom:
            self.bottom = th_y
        if (tv_y + th_y - dest_y) > self.bottom:
            self.bottom = tv_y + th_y - dest_y
        if self.bottom > (SCREEN_SIZE_H-1):
            self.bottom = SCREEN_SIZE_H-1

        # テーブル初期化
        for y in range (self.top, self.bottom+1):
            self.buff[y].l = EMPTY
            self.buff[y].r = EMPTY

        # 画像縦方向処理
        reg = self.vbre.setup(dest_x,dest_y,tv_x,tv_y)
        self.bre.setup(src_h-1,reg-1)

        match angle:
            # 垂直にコピーする時は縦方向の処理を行わない
            case a if a == 32 or a == 96:
                pass
                
            case a if a < 32:
                
                for i in range (reg):
                    x,y = self.vbre.step()
                    add_src_y = self.bre.step()

                    # MainSide
                    if y >= 0 and y < SCREEN_SIZE_H:
                        
                        if self.buff[y].l == EMPTY:
                            self.buff[y].l = x
                        pyxel.pset(x,y,6)

                        self.buff[y].src_sx = src_x
                        self.buff[y].src_sy = src_y + add_src_y
                    
                    x = x - dest_x + th_x
                    y = y - dest_y + th_y

                    # SubSide
                    if y >= 0 and y < SCREEN_SIZE_H:

                        self.buff[y].r = x
                        pyxel.pset(x,y,6)
                        
                        self.buff[y].src_tx = src_x + src_w-1
                        self.buff[y].src_ty = src_y + add_src_y

            case a if a < 64:

                for i in range (reg):
                    x,y = self.vbre.step()
                    add_src_y = self.bre.step()
                    
                    # MainSide
                    if y >= 0 and y < SCREEN_SIZE_H:

                        self.buff[y].r = x
                        pyxel.pset(x,y,6)

                        self.buff[y].src_tx = src_x
                        self.buff[y].src_ty = src_y + add_src_y
                        
                    x = x - dest_x + th_x
                    y = y - dest_y + th_y
                    
                    # SubSide
                    if y >= 0 and y < SCREEN_SIZE_H:

                        if self.buff[y].l == EMPTY:
                            self.buff[y].l = x
                        pyxel.pset(x,y,6)

                        self.buff[y].src_sx = src_x + src_w-1
                        self.buff[y].src_sy = src_y + add_src_y
            
            case a if a < 96:

                for i in range (reg):
                    x,y = self.vbre.step()
                    add_src_y = self.bre.step()
                    
                    # MainSide
                    if y >= 0 and y < SCREEN_SIZE_H:

                        if self.buff[y].r == EMPTY:
                            self.buff[y].r = x
                        pyxel.pset(x,y,6)

                        self.buff[y].src_tx = src_x
                        self.buff[y].src_ty = src_y + add_src_y
                    
                    x = x - dest_x + th_x
                    y = y - dest_y + th_y
                    
                    # SubSide
                    if y >= 0 and y < SCREEN_SIZE_H:

                        self.buff[y].l = x
                        pyxel.pset(x,y,6)

                        self.buff[y].src_sx = src_x + src_w-1
                        self.buff[y].src_sy = src_y + add_src_y

            case _:

                for i in range (reg):
                    x,y = self.vbre.step()
                    add_src_y = self.bre.step()
                
                    # MainSide
                    if y >= 0 and y < SCREEN_SIZE_H:

                        self.buff[y].l = x
                        pyxel.pset(x,y,6)

                        self.buff[y].src_sx = src_x
                        self.buff[y].src_sy = src_y + add_src_y

                    x = x - dest_x + th_x
                    y = y - dest_y + th_y
                    
                    # SubSide
                    if y >= 0 and y < SCREEN_SIZE_H:

                        if self.buff[y].r == EMPTY:
                            self.buff[y].r = x
                        pyxel.pset(x,y,6)

                        self.buff[y].src_tx = src_x + src_w-1
                        self.buff[y].src_ty = src_y + add_src_y

        # 画像横方向処理
        reg = self.vbre.setup(dest_x,dest_y,th_x,th_y)
        self.bre.setup(src_w-1,reg-1)

        match angle:
            # 水平にコピーする時は横方向の処理を行わない
            case a if angle == 0 or angle == 64:
                pass
                
            case a if a < 32:
                
                for i in range (reg):
                    x,y = self.vbre.step()
                    add_src_x = self.bre.step()
                    
                    # MainSide
                    if y >= 0 and y < SCREEN_SIZE_H:

                        if self.buff[y].l == EMPTY:
                            self.buff[y].l = x
                        pyxel.pset(x,y,6)

                        self.buff[y].src_sx = src_x + add_src_x
                        self.buff[y].src_sy = src_y
                    
                    x = x - dest_x + tv_x
                    y = y - dest_y + tv_y

                    # SubSide
                    if y >= 0 and y < SCREEN_SIZE_H:

                        self.buff[y].r = x
                        pyxel.pset(x,y,6)

                        self.buff[y].src_tx = src_x + add_src_x
                        self.buff[y].src_ty = src_y + src_h-1
                
            case a if a < 64:

                for i in range (reg):
                    x,y = self.vbre.step()
                    add_src_x = self.bre.step()
                
                    # MainSide
                    if y >= 0 and y < SCREEN_SIZE_H:

                        self.buff[y].l = x
                        pyxel.pset(x,y,6)

                        self.buff[y].src_sx = src_x + add_src_x
                        self.buff[y].src_sy = src_y
                    
                    x = x - dest_x + tv_x
                    y = y - dest_y + tv_y

                    # SubSide
                    if y >= 0 and y < SCREEN_SIZE_H:

                        if self.buff[y].r == EMPTY:
                            self.buff[y].r = x
                        pyxel.pset(x,y,6)

                        self.buff[y].src_tx = src_x + add_src_x
                        self.buff[y].src_ty = src_y + src_h-1

            case a if a < 96:

                for i in range (reg):
                    x,y = self.vbre.step()
                    add_src_x = self.bre.step()

                    # MainSide
                    if y >= 0 and y < SCREEN_SIZE_H:

                        if self.buff[y].r == EMPTY:
                            self.buff[y].r = x
                        pyxel.pset(x,y,6)

                        self.buff[y].src_tx = src_x + add_src_x
                        self.buff[y].src_ty = src_y
                    
                    x = x - dest_x + tv_x
                    y = y - dest_y + tv_y

                    # SubSide
                    if y >= 0 and y < SCREEN_SIZE_H:

                        self.buff[y].l = x
                        pyxel.pset(x,y,6)

                        self.buff[y].src_sx = src_x + add_src_x
                        self.buff[y].src_sy = src_y + src_h-1

            case _:
                
                for i in range (reg):
                    x,y = self.vbre.step()
                    add_src_x = self.bre.step()
                    
                    # MainSide
                    if y >= 0 and y < SCREEN_SIZE_H:

                        self.buff[y].r = x
                        pyxel.pset(x,y,6)

                        self.buff[y].src_tx = src_x + add_src_x
                        self.buff[y].src_ty = src_y
                    
                    x = x - dest_x + tv_x
                    y = y - dest_y + tv_y

                    # SubSide
                    if y >= 0 and y < SCREEN_SIZE_H:

                        if self.buff[y].l == EMPTY:
                            self.buff[y].l = x
                        pyxel.pset(x,y,6)

                        self.buff[y].src_sx = src_x + add_src_x
                        self.buff[y].src_sy = src_y + src_h-1

        # バッファを基に転送を行う
        for y in range (self.top, self.bottom+1):
            reg = self.vbre.setup(self.buff[y].src_sx,self.buff[y].src_sy,self.buff[y].src_tx,self.buff[y].src_ty)
            self.bre.setup(reg-1,self.buff[y].r - self.buff[y].l)
            
            sx,sy = self.vbre.step()
            old_src = 0
            new_src = 0

            for x in range (self.buff[y].l,self.buff[y].r+1):
                new_src = self.bre.step()
                for step_src in range (old_src, new_src):
                    sx,sy = self.vbre.step()
                old_src = new_src
                
                # 画面左右端処理
                if x < 0:
                    continue
                if x >= SCREEN_SIZE_W:
                    break
                
                pyxel.blt(x, y, 0, sx, sy, 1, 1)


class App:

    def __init__(self):
        # 画面サイズ 160x120 で初期化
        pyxel.init(SCREEN_SIZE_W, SCREEN_SIZE_H, title="RollBlt")
        self.add_x = 0
        pyxel.load("yajirushi.pyxres")

        self.rb = RollBlt()
        
        self.angle = 0

        pyxel.run(self.update, self.draw)

    def update(self):
        self.angle += 1
        self.angle &= 127

    def draw(self):
        # 画面を黒(色番号0)でクリア
        pyxel.cls(0)
        
        #self.rb.blt_c(CENTER_X, CENTER_Y, 0, 0, 0, 32, 16, self.angle,scale_w=64,scale_h=64)
        #self.rb.blt_c(CENTER_X, CENTER_Y, 0, 0, 0, 32, 16, self.angle,scale_w=256,scale_h=64)
        self.rb.blt_c(CENTER_X, CENTER_Y, 0, 0, 0, 32, 16, self.angle,scale_w=512,scale_h=512)

App()

ソースここまで

※画面左右端処理を追記
128を基準値とした拡縮/新設した blt_c 関数による中心指定コピー、上下画面外への処理も可能に。ちなみに前回の状態で画面上下にはみ出しても大丈夫ではありました…これは Python の仕様上ラインバッファが配列では無くリストになっていた為、インデックスがマイナスになってもエラーを吐かない為です。とは言え、それを頼ってはいけないので処理は追加しましたが。
拡大すると45度付近で顕著にズレが生じる事が解ります。なにやら古いDVDのようですね…法則性があるので解決策もありそうですが…

今回長方形の回転を作っていたので、Blt 関数内の各 case 文で原点を中心に辺の計算をした後で、原点を平行移動させて対になる辺の値も作っていました。
角度と長さが同じと判っている為、こうすれば無駄がない為です(各 MainSide SubSide 参照)。
しかし、師匠の最終課題とは…恐らく4頂点を自由移動…つまりポリゴン化だったのではないかと推測します。

セガサターンのポリゴンは「スプライトを変形させた疑似ポリゴン」

…という解説を読んだことはないでしょうか?

疑似も何もポリゴンは多角形の事なので4角形ならポリゴンなのですが。
つまり、これと同様のものを作れ…というのが最終課題だったのではないか?と、思われます。ただ、これは結構な問題で3角形は数学上折れる事は無いのですが4角形は折れたり自分自身と重なったりします。3頂点の中に他の頂点が含まれたか?の判定については、カリング判定式というものがあり、これも実数も割り算も使わない古いやり方があるのですが…
※9/17追記
セガサターンのスプライト変形は今回の処理と違い元画像を主体とした画像の転送であり、どちらかと言うと第4回の時の処理に近い物のようです。

最後に白状してしまうと…
四半世紀以上昔の話、一度ゲーム業界を辞めて、一般ソフトハウスに入り、その後もう一度ゲーム業界にカムバックしたのですが。
その際にウィンドウズで DirectX を使わない独自ゲームライブラリを作って、サンプルデモを持ち込み転職した事がありました。
描画関係はほぼ i386 アセンブラ(一部 MMX Pentium )を使用しており、なんならアセンブラ内部からC++のメンバー関数を呼ぶといった器用な事もやっていたのですが…
画像転送、アニメーション有りスプライト処理(拡縮はできたが回転はできず)の他に…256色パレットモードビットマップを使った3D処理を作っており、六角大王で作ったデータを変換して3Dアニメーションデモを作る…といった処理も組んでいました。
ええ、テクスチャ無しなら4角だろうがポリゴンだろうが円だろうが描画する処理自体は組んだ事があり、それらをZソートで繋いで表示する事くらいはやっていたのです。
当時ビットマップ回転処理だけできなかったのが心残りでした。

まあ、そんなものを作る性分ですから DirectX どころかアンリアルエンジンやらのライブラリの「借り方」が上手い奴が上に行く当時の風潮や、とにかく新しい言語を覚え続けないといけないという世界に嫌気がさしてしまったんですけどね。
当時の風潮になじめなかったアセンブラバカを始めとするエンジニアの安息の地はどこにあったのでしょうね?久々に真面目にプログラムを組んでそんな事を思いました。

というわけで、実数と割り算を使用しないレトロ縛りの回転画像転送処理を一旦〆たいと思います。本当は年末までかかるかと思ったのですが、一気にやらないと頭から抜け落ちそうだったので押し通しました。
最後に、ゲーム業界でプログラマー1年目だった自分にこれだけの奥の深い課題を出してくれた師匠に最大限の感謝を。