[{"data":1,"prerenderedAt":2474},["ShallowReactive",2],{"blog-tag-ゲーム開発":3},[4,1060,1765,1873,2018,2128,2313,2449],{"id":5,"title":6,"body":7,"date":1045,"description":1046,"draft":1047,"extension":1048,"image":1049,"meta":1050,"navigation":1051,"path":1052,"pr":1047,"seo":1053,"stem":1054,"tags":1055,"updated":1058,"__hash__":1059},"blog\u002Fblog\u002F2026-08-01-sarumono-durable-objects.md","個人サイトのブラウザゲームに、サーバーを1台も借りずにオンライン対戦を付けた",{"type":8,"value":9,"toc":1029},"minimark",[10,20,23,28,31,34,37,41,44,47,50,53,56,59,62,104,107,110,113,116,119,332,339,346,350,353,356,371,378,382,389,453,456,542,549,552,555,683,686,689,692,695,698,813,816,819,822,826,829,832,936,939,942,946,949,952,955,958,961,965,968,971,975,978,981,984,988,991,994,997,1012,1017,1025],[11,12,13,14,19],"p",{},"自作の3D対戦ミニゲーム「",[15,16,18],"a",{"href":17},"\u002Fsarumono\u002F","サルモノハオトス","」に、オンライン対戦を付けました。合言葉を4文字決めれば、友だちとその場で対戦できます。",[11,21,22],{},"作る前にいちばん引っかかっていたのは、技術というより身の丈の話でした。個人サイトのミニゲームひとつのために、24時間動き続けるサーバーを持つのか、と。",[24,25,27],"h2",{"id":26},"npc戦は動いた友だちと対戦にした瞬間話が変わった","NPC戦は動いた。「友だちと対戦」にした瞬間、話が変わった",[11,29,30],{},"もともとこのゲームは、ブラウザの中だけで完結していました。プレイヤーとNPCが浮き島の上でマスを崩し合う。盤面はブラウザが持っていて、それで何も困りません。",[11,32,33],{},"ところが2人の人間が別々の端末から遊ぶとなると、途端に困ります。二人が同時に同じマスへ踏み込んだとき、どちらが先に着いたことにするのか。片方の画面では自分が勝っていて、もう片方では相手が勝っている、という状態を防げません。",[11,35,36],{},"つまり「正しい盤面はこれです」と言い切る役、権威が要る。そしてそれは、どちらのブラウザでもない場所に置く必要があります。",[24,38,40],{"id":39},"常時起動のサーバーもp2pも選ばなかった","常時起動のサーバーもP2Pも選ばなかった",[11,42,43],{},"素直な選択肢はVPSを1台借りて、そこでNodeとWebSocketサーバーを動かすことです。やり方は完全に確立していますし、情報も多い。",[11,45,46],{},"ただ、うちのサイトの規模を考えると、誰も対戦していない時間のほうが圧倒的に長い。その大半を、ただプロセスが起きているだけの状態で過ごすことになります。加えて、動き続けるものは面倒を見続ける必要がある。ゲームを作りたくて始めたのに、サーバーの世話が主な仕事になるのは避けたいところでした。",[11,48,49],{},"ブラウザ同士を直結するWebRTCも考えました。こちらは中央にサーバーが要らないのが魅力です。ただ、相手を見つけるための仲介役はどのみち必要ですし、何より「権威をどこに置くか」という最初の問題が形を変えて戻ってくる。決定論的に同期させる方法も、片方をホスト扱いする方法もありますが、今回はそこに頭を使いたくありませんでした。単一の権威を、いちばん単純に置きたかった。",[11,51,52],{},"そこで Cloudflare の Durable Objects を使いました。すごく雑に言うと、部屋ごとに小さなサーバーが勝手に立ち上がって、誰もいなくなったら畳まれる仕組みです。対戦が始まっていない時間に、動き続けているものがない。今回いちばん欲しかったのは、まさにこの性質でした。",[24,54,55],{"id":55},"部屋がひとつのオブジェクトになる",[11,57,58],{},"構成はWorkerがひとつと、Durable Objectが2種類です。",[11,60,61],{},"Workerの本体は驚くほど薄くて、やっているのはルーティングだけです。リクエストの中身は、ロビーか対戦ルームのどちらかに丸ごと渡します。",[63,64,69],"pre",{"className":65,"code":66,"language":67,"meta":68,"style":68},"language-toml shiki shiki-themes github-light github-dark","[durable_objects]\nbindings = [\n  { name = \"LOBBY\", class_name = \"FightLobby\" },\n  { name = \"ROOM\",  class_name = \"FightRoom\" }\n]\n","toml","",[70,71,72,80,86,92,98],"code",{"__ignoreMap":68},[73,74,77],"span",{"class":75,"line":76},"line",1,[73,78,79],{},"[durable_objects]\n",[73,81,83],{"class":75,"line":82},2,[73,84,85],{},"bindings = [\n",[73,87,89],{"class":75,"line":88},3,[73,90,91],{},"  { name = \"LOBBY\", class_name = \"FightLobby\" },\n",[73,93,95],{"class":75,"line":94},4,[73,96,97],{},"  { name = \"ROOM\",  class_name = \"FightRoom\" }\n",[73,99,101],{"class":75,"line":100},5,[73,102,103],{},"]\n",[11,105,106],{},"Lobby DOは、ルームIDを発行する唯一の場所であり、部屋一覧を書き換える唯一の場所でもあります。Room DOは1試合分の権威で、参加者のWebSocketを持ち、盤面を進めます。",[11,108,109],{},"ここで効いてくるのが、Durable Objectは同じIDに対して世界にひとつしか存在しない、という性質です。部屋一覧を書き換えるのが常にひとつのオブジェクトなら、ふたりが同時に部屋を作りにきても、書き込みが混線しません。ロックも、トランザクションも、自分では書いていません。並行処理の面倒な部分を、置き場所の設計で回避した格好です。",[11,111,112],{},"ルームIDは4文字にしました。声に出して伝えられる長さで、紛らわしい文字（0とO、1とIなど）は候補から抜いてあります。ただしこれは合言葉であって、資格情報ではありません。あとから再接続してくる人が本人かどうかは、別の値で確かめています。",[24,114,115],{"id":115},"クライアントは何も計算していない",[11,117,118],{},"サーバーが権威だと決めたので、ブラウザ側は徹底して表示専用にしました。オンライン対戦時のコントローラは、更新関数が空っぽです。",[63,120,124],{"className":121,"code":122,"language":123,"meta":68,"style":68},"language-ts shiki shiki-themes github-light github-dark","export function createRemoteFightController(\n  viewerId: string,\n  initial: ControllerSnapshot\n): RemoteFightController {\n  let snapshot = initial\n  \u002F\u002F ...\n  return {\n    \u002F\u002F 派生情報はローカル対戦と同じ純関数から取り出す\n    getWarningCells: () => snapshotView(snapshot, viewerId).warningCells,\n    getInvincibleIds: () => snapshotView(snapshot, viewerId).invincibleIds,\n    \u002F\u002F シミュレーションはサーバー側。ここでは何もしない\n    update: (_dtMs: number, _input: StepInput) => {},\n    pushState: (next, events) => { snapshot = next }\n  }\n}\n","ts",[70,125,126,143,159,169,182,196,203,211,217,235,250,256,292,320,326],{"__ignoreMap":68},[73,127,128,132,135,139],{"class":75,"line":76},[73,129,131],{"class":130},"szBVR","export",[73,133,134],{"class":130}," function",[73,136,138],{"class":137},"sScJk"," createRemoteFightController",[73,140,142],{"class":141},"sVt8B","(\n",[73,144,145,149,152,156],{"class":75,"line":82},[73,146,148],{"class":147},"s4XuR","  viewerId",[73,150,151],{"class":130},":",[73,153,155],{"class":154},"sj4cs"," string",[73,157,158],{"class":141},",\n",[73,160,161,164,166],{"class":75,"line":88},[73,162,163],{"class":147},"  initial",[73,165,151],{"class":130},[73,167,168],{"class":137}," ControllerSnapshot\n",[73,170,171,174,176,179],{"class":75,"line":94},[73,172,173],{"class":141},")",[73,175,151],{"class":130},[73,177,178],{"class":137}," RemoteFightController",[73,180,181],{"class":141}," {\n",[73,183,184,187,190,193],{"class":75,"line":100},[73,185,186],{"class":130},"  let",[73,188,189],{"class":141}," snapshot ",[73,191,192],{"class":130},"=",[73,194,195],{"class":141}," initial\n",[73,197,199],{"class":75,"line":198},6,[73,200,202],{"class":201},"sJ8bj","  \u002F\u002F ...\n",[73,204,206,209],{"class":75,"line":205},7,[73,207,208],{"class":130},"  return",[73,210,181],{"class":141},[73,212,214],{"class":75,"line":213},8,[73,215,216],{"class":201},"    \u002F\u002F 派生情報はローカル対戦と同じ純関数から取り出す\n",[73,218,220,223,226,229,232],{"class":75,"line":219},9,[73,221,222],{"class":137},"    getWarningCells",[73,224,225],{"class":141},": () ",[73,227,228],{"class":130},"=>",[73,230,231],{"class":137}," snapshotView",[73,233,234],{"class":141},"(snapshot, viewerId).warningCells,\n",[73,236,238,241,243,245,247],{"class":75,"line":237},10,[73,239,240],{"class":137},"    getInvincibleIds",[73,242,225],{"class":141},[73,244,228],{"class":130},[73,246,231],{"class":137},[73,248,249],{"class":141},"(snapshot, viewerId).invincibleIds,\n",[73,251,253],{"class":75,"line":252},11,[73,254,255],{"class":201},"    \u002F\u002F シミュレーションはサーバー側。ここでは何もしない\n",[73,257,259,262,265,268,270,273,276,279,281,284,287,289],{"class":75,"line":258},12,[73,260,261],{"class":137},"    update",[73,263,264],{"class":141},": (",[73,266,267],{"class":147},"_dtMs",[73,269,151],{"class":130},[73,271,272],{"class":154}," number",[73,274,275],{"class":141},", ",[73,277,278],{"class":147},"_input",[73,280,151],{"class":130},[73,282,283],{"class":137}," StepInput",[73,285,286],{"class":141},") ",[73,288,228],{"class":130},[73,290,291],{"class":141}," {},\n",[73,293,295,298,300,303,305,308,310,312,315,317],{"class":75,"line":294},13,[73,296,297],{"class":137},"    pushState",[73,299,264],{"class":141},[73,301,302],{"class":147},"next",[73,304,275],{"class":141},[73,306,307],{"class":147},"events",[73,309,286],{"class":141},[73,311,228],{"class":130},[73,313,314],{"class":141}," { snapshot ",[73,316,192],{"class":130},[73,318,319],{"class":141}," next }\n",[73,321,323],{"class":75,"line":322},14,[73,324,325],{"class":141},"  }\n",[73,327,329],{"class":75,"line":328},15,[73,330,331],{"class":141},"}\n",[11,333,334,335,338],{},"地味ですが、この ",[70,336,337],{},"update()"," が空であることが設計の芯だと思っています。ブラウザは受け取った盤面を映すだけ。予測もしないし、辻褄合わせもしない。",[11,340,341,342,345],{},"そのうえで、盤面から表示用の情報を取り出す ",[70,343,344],{},"snapshotView"," は、ローカル対戦とオンラインで同じものを使っています。「どのマスが警告色か」「誰が無敵中か」といった判定を二重に実装していないので、片方だけ直し忘れて表示がずれる、という事故が起きません。",[24,347,349],{"id":348},"durable-objectの中に置いたのはループと後始末だけ","Durable Objectの中に置いたのは、ループと後始末だけ",[11,351,352],{},"Durable Objectはテストが書きづらい部類のコードです。ストレージもアラームもWebSocketも、実行環境そのものに依存しています。",[11,354,355],{},"なので、判断を伴う部分は全部外に出しました。席の予約やフェーズの遷移はDOに依存しない純関数の集まりで、盤面を1ティック進める処理はブラウザ版と共用しているエンジンの関数です。DOに残ったのは、タイマーを回すこと、状態を保存すること、参加者に配ることだけ。",[63,357,359],{"className":121,"code":358,"language":123,"meta":68,"style":68},"\u002F\u002F DO非依存の純粋なルーム状態機械。\n\u002F\u002F Room DO(Durable Object)はここの遷移関数を呼ぶだけにして、ロジックはvitestで検証する。\n",[70,360,361,366],{"__ignoreMap":68},[73,362,363],{"class":75,"line":76},[73,364,365],{"class":201},"\u002F\u002F DO非依存の純粋なルーム状態機械。\n",[73,367,368],{"class":75,"line":82},[73,369,370],{"class":201},"\u002F\u002F Room DO(Durable Object)はここの遷移関数を呼ぶだけにして、ロジックはvitestで検証する。\n",[11,372,373,374,377],{},"これは ",[70,375,376],{},"room-logic.ts"," の冒頭に自分で書いたコメントですが、方針としてはこの一行に尽きます。おかげでテストは普通に速くて、ゲーム関連だけで584件、サイト全体では1354件が2.5秒ほどで終わります。ここを守っておかないと、通信まわりの実装は手が出しづらくなっていたと思います。",[24,379,381],{"id":380},"_50msで進めて実際には100msごとに配っていた","50msで進めて、実際には100msごとに配っていた",[11,383,384,385,388],{},"対戦中のループは50ミリ秒間隔です。ただ ",[70,386,387],{},"setInterval"," は指定どおりの間隔では回ってくれないので、経過時間は実測を使っています。",[63,390,392],{"className":121,"code":391,"language":123,"meta":68,"style":68},"\u002F\u002F タイマードリフトに頑健なよう、実経過時間を累積してdtにする\nconst now = Date.now()\nconst dtMs = now - this.lastTickAt\nthis.lastTickAt = now\n",[70,393,394,399,419,440],{"__ignoreMap":68},[73,395,396],{"class":75,"line":76},[73,397,398],{"class":201},"\u002F\u002F タイマードリフトに頑健なよう、実経過時間を累積してdtにする\n",[73,400,401,404,407,410,413,416],{"class":75,"line":82},[73,402,403],{"class":130},"const",[73,405,406],{"class":154}," now",[73,408,409],{"class":130}," =",[73,411,412],{"class":141}," Date.",[73,414,415],{"class":137},"now",[73,417,418],{"class":141},"()\n",[73,420,421,423,426,428,431,434,437],{"class":75,"line":88},[73,422,403],{"class":130},[73,424,425],{"class":154}," dtMs",[73,427,409],{"class":130},[73,429,430],{"class":141}," now ",[73,432,433],{"class":130},"-",[73,435,436],{"class":154}," this",[73,438,439],{"class":141},".lastTickAt\n",[73,441,442,445,448,450],{"class":75,"line":94},[73,443,444],{"class":154},"this",[73,446,447],{"class":141},".lastTickAt ",[73,449,192],{"class":130},[73,451,452],{"class":141}," now\n",[11,454,455],{},"そして配信は毎ティックではなく、間引いています。この記事を書くために自分のコードを読み返して、そこで気づいたことがありました。",[63,457,459],{"className":121,"code":458,"language":123,"meta":68,"style":68},"this.broadcastAccumMs += dtMs\nif (this.broadcastAccumMs >= BROADCAST_MS) {   \u002F\u002F BROADCAST_MS = 66 \u002F\u002F 15Hz\n  this.broadcastAccumMs = 0\n  this.broadcast({ t: 'state', snapshot: result.snapshot, events: this.pendingEvents })\n}\n",[70,460,461,474,501,513,538],{"__ignoreMap":68},[73,462,463,465,468,471],{"class":75,"line":76},[73,464,444],{"class":154},[73,466,467],{"class":141},".broadcastAccumMs ",[73,469,470],{"class":130},"+=",[73,472,473],{"class":141}," dtMs\n",[73,475,476,479,482,484,486,489,492,495,498],{"class":75,"line":82},[73,477,478],{"class":130},"if",[73,480,481],{"class":141}," (",[73,483,444],{"class":154},[73,485,467],{"class":141},[73,487,488],{"class":130},">=",[73,490,491],{"class":154}," BROADCAST_MS",[73,493,494],{"class":141},") {   ",[73,496,497],{"class":201},"\u002F\u002F BROADCAST_MS = 66",[73,499,500],{"class":201}," \u002F\u002F 15Hz\n",[73,502,503,506,508,510],{"class":75,"line":88},[73,504,505],{"class":154},"  this",[73,507,467],{"class":141},[73,509,192],{"class":130},[73,511,512],{"class":154}," 0\n",[73,514,515,517,520,523,526,530,533,535],{"class":75,"line":94},[73,516,505],{"class":154},[73,518,519],{"class":141},".",[73,521,522],{"class":137},"broadcast",[73,524,525],{"class":141},"({ t: ",[73,527,529],{"class":528},"sZZnC","'state'",[73,531,532],{"class":141},", snapshot: result.snapshot, events: ",[73,534,444],{"class":154},[73,536,537],{"class":141},".pendingEvents })\n",[73,539,540],{"class":75,"line":100},[73,541,331],{"class":141},[11,543,544,545,548],{},"定数には ",[70,546,547],{},"66 \u002F\u002F 15Hz"," と書いてあります。書いた本人はそのつもりでした。でも実際は違います。50ミリ秒のティックで累積していくと、1回目は50で閾値に届かず、2回目の100で初めて配信されます。そして累積を66引くのではなく0に戻しているので、次もまた50、100と積み直す。結局ずっと100ミリ秒ごと、およそ10Hzです。",[11,550,551],{},"面白いのは、公開してから今まで誰にも指摘されず、自分で遊んでいても違和感がなかったことです。理由はゲームの作りにあります。このゲームは格子の上で動きます。キャラの位置は8×8マスの整数で、途中の座標というものがない。連続した位置を追いかけるアクションゲームなら10Hzは論外ですが、マス単位なら1秒に10回で足ります。",[11,553,554],{},"そして見た目の滑らかさは、通信とは別のところで作っています。描画側では、論理的なマス位置を目標にして、毎フレーム指数減衰で近づけているだけです。",[63,556,558],{"className":121,"code":557,"language":123,"meta":68,"style":68},"\u002F\u002F フレームレート非依存の指数減衰補間。halfLifeMsごとに残距離が半分になる。\nexport function dampenValue(current: number, target: number, dtMs: number, halfLifeMs: number): number {\n  const factor = 1 - Math.pow(0.5, dtMs \u002F halfLifeMs)\n  return current + (target - current) * factor\n}\n",[70,559,560,565,619,655,679],{"__ignoreMap":68},[73,561,562],{"class":75,"line":76},[73,563,564],{"class":201},"\u002F\u002F フレームレート非依存の指数減衰補間。halfLifeMsごとに残距離が半分になる。\n",[73,566,567,569,571,574,577,580,582,584,586,589,591,593,595,598,600,602,604,607,609,611,613,615,617],{"class":75,"line":82},[73,568,131],{"class":130},[73,570,134],{"class":130},[73,572,573],{"class":137}," dampenValue",[73,575,576],{"class":141},"(",[73,578,579],{"class":147},"current",[73,581,151],{"class":130},[73,583,272],{"class":154},[73,585,275],{"class":141},[73,587,588],{"class":147},"target",[73,590,151],{"class":130},[73,592,272],{"class":154},[73,594,275],{"class":141},[73,596,597],{"class":147},"dtMs",[73,599,151],{"class":130},[73,601,272],{"class":154},[73,603,275],{"class":141},[73,605,606],{"class":147},"halfLifeMs",[73,608,151],{"class":130},[73,610,272],{"class":154},[73,612,173],{"class":141},[73,614,151],{"class":130},[73,616,272],{"class":154},[73,618,181],{"class":141},[73,620,621,624,627,629,632,635,638,641,643,646,649,652],{"class":75,"line":88},[73,622,623],{"class":130},"  const",[73,625,626],{"class":154}," factor",[73,628,409],{"class":130},[73,630,631],{"class":154}," 1",[73,633,634],{"class":130}," -",[73,636,637],{"class":141}," Math.",[73,639,640],{"class":137},"pow",[73,642,576],{"class":141},[73,644,645],{"class":154},"0.5",[73,647,648],{"class":141},", dtMs ",[73,650,651],{"class":130},"\u002F",[73,653,654],{"class":141}," halfLifeMs)\n",[73,656,657,659,662,665,668,670,673,676],{"class":75,"line":94},[73,658,208],{"class":130},[73,660,661],{"class":141}," current ",[73,663,664],{"class":130},"+",[73,666,667],{"class":141}," (target ",[73,669,433],{"class":130},[73,671,672],{"class":141}," current) ",[73,674,675],{"class":130},"*",[73,677,678],{"class":141}," factor\n",[73,680,681],{"class":75,"line":100},[73,682,331],{"class":141},[11,684,685],{},"キャラの追従は70ミリ秒、タイルの沈み込みは110ミリ秒を半減期にしてあります。通信が10Hzでも、画面は60fpsで滑らかに動く。通信レートと見た目の気持ちよさは、そもそも別の問題だったわけですね。",[11,687,688],{},"とはいえ意図と実装がずれているのは事実なので、ここは直します。ただ本番で動いているサーバーの挙動を変える話なので、記事とは切り離して、実際に対戦しながら確かめるつもりです。",[24,690,691],{"id":691},"抜けた人はサルのまま残しておく",[11,693,694],{},"対戦中に誰かの回線が切れたとき、どうするか。試合を止めるのがいちばん簡単ですが、残されたほうは何もしていないのに終わらされます。",[11,696,697],{},"なので、切断した席は30秒待ってからNPCに引き継がせることにしました。実装はこうです。",[63,699,701],{"className":121,"code":700,"language":123,"meta":68,"style":68},"\u002F\u002F エンジンの契約に従い、bot引き継ぎ=IDをhuman系Recordからnpc系Recordへ移す操作\nexport function takeOverWithBot(snapshot: ControllerSnapshot, id: string): ControllerSnapshot {\n  const humanCooldownMs = { ...snapshot.humanCooldownMs }\n  delete humanCooldownMs[id]\n  return {\n    ...snapshot,\n    humanCooldownMs,\n    npcCooldownMs: { ...snapshot.npcCooldownMs, [id]: NPC_TICK_MS_BY_DIFFICULTY[snapshot.difficulty] }\n  }\n}\n",[70,702,703,708,744,762,770,776,784,789,805,809],{"__ignoreMap":68},[73,704,705],{"class":75,"line":76},[73,706,707],{"class":201},"\u002F\u002F エンジンの契約に従い、bot引き継ぎ=IDをhuman系Recordからnpc系Recordへ移す操作\n",[73,709,710,712,714,717,719,722,724,727,729,732,734,736,738,740,742],{"class":75,"line":82},[73,711,131],{"class":130},[73,713,134],{"class":130},[73,715,716],{"class":137}," takeOverWithBot",[73,718,576],{"class":141},[73,720,721],{"class":147},"snapshot",[73,723,151],{"class":130},[73,725,726],{"class":137}," ControllerSnapshot",[73,728,275],{"class":141},[73,730,731],{"class":147},"id",[73,733,151],{"class":130},[73,735,155],{"class":154},[73,737,173],{"class":141},[73,739,151],{"class":130},[73,741,726],{"class":137},[73,743,181],{"class":141},[73,745,746,748,751,753,756,759],{"class":75,"line":88},[73,747,623],{"class":130},[73,749,750],{"class":154}," humanCooldownMs",[73,752,409],{"class":130},[73,754,755],{"class":141}," { ",[73,757,758],{"class":130},"...",[73,760,761],{"class":141},"snapshot.humanCooldownMs }\n",[73,763,764,767],{"class":75,"line":94},[73,765,766],{"class":130},"  delete",[73,768,769],{"class":141}," humanCooldownMs[id]\n",[73,771,772,774],{"class":75,"line":100},[73,773,208],{"class":130},[73,775,181],{"class":141},[73,777,778,781],{"class":75,"line":198},[73,779,780],{"class":130},"    ...",[73,782,783],{"class":141},"snapshot,\n",[73,785,786],{"class":75,"line":205},[73,787,788],{"class":141},"    humanCooldownMs,\n",[73,790,791,794,796,799,802],{"class":75,"line":213},[73,792,793],{"class":141},"    npcCooldownMs: { ",[73,795,758],{"class":130},[73,797,798],{"class":141},"snapshot.npcCooldownMs, [id]: ",[73,800,801],{"class":154},"NPC_TICK_MS_BY_DIFFICULTY",[73,803,804],{"class":141},"[snapshot.difficulty] }\n",[73,806,807],{"class":75,"line":219},[73,808,325],{"class":141},[73,810,811],{"class":75,"line":237},[73,812,331],{"class":141},[11,814,815],{},"やっているのは、キャラのIDを人間側のRecordからNPC側のRecordへ引っ越しさせることだけです（実際には移動と攻撃で2つのクールダウンを扱っています）。エンジンは「人間とは humanCooldownMs にキーがある者のことだ」という契約で書いてあるので、キーの置き場所を変えるだけで、そのキャラは次のティックから勝手に動きはじめます。",[11,817,818],{},"再接続したときは、まったく同じ操作を逆向きにやります。人間かNPCかという、いかにも大がかりに聞こえる区別が、Recordのどちらにキーがあるかという一点に還元されている。個人的に、この実装がいちばん気持ちよかった部分です。",[11,820,821],{},"ちなみに両方とも切断した場合は待ちません。その場で試合を畳みます。誰も見ていない試合を回し続ける理由がないからです。",[24,823,825],{"id":824},"同時に同じマスへ来た2人をどちらから処理するか","同時に同じマスへ来た2人を、どちらから処理するか",[11,827,828],{},"サーバーは1ティックの中で、参加者の入力を順番に適用します。ということは、先に処理された人が有利になります。同じマスを取り合ったとき、常に先手の側が勝つ。",[11,830,831],{},"対策は拍子抜けするほど単純で、順番を毎回ずらしています。",[63,833,835],{"className":121,"code":834,"language":123,"meta":68,"style":68},"\u002F\u002F 各人間の行動。逐次適用による先行者有利(衝突)を平均化するため、適用順をティックごとに回す\nconst humanIds = Object.keys(humanCooldownMs)\nconst rotation = humanIds.length > 0 ? snapshot.tick % humanIds.length : 0\nconst orderedHumanIds = [...humanIds.slice(rotation), ...humanIds.slice(0, rotation)]\n",[70,836,837,842,860,899],{"__ignoreMap":68},[73,838,839],{"class":75,"line":76},[73,840,841],{"class":201},"\u002F\u002F 各人間の行動。逐次適用による先行者有利(衝突)を平均化するため、適用順をティックごとに回す\n",[73,843,844,846,849,851,854,857],{"class":75,"line":82},[73,845,403],{"class":130},[73,847,848],{"class":154}," humanIds",[73,850,409],{"class":130},[73,852,853],{"class":141}," Object.",[73,855,856],{"class":137},"keys",[73,858,859],{"class":141},"(humanCooldownMs)\n",[73,861,862,864,867,869,872,875,878,881,884,887,890,892,894,897],{"class":75,"line":88},[73,863,403],{"class":130},[73,865,866],{"class":154}," rotation",[73,868,409],{"class":130},[73,870,871],{"class":141}," humanIds.",[73,873,874],{"class":154},"length",[73,876,877],{"class":130}," >",[73,879,880],{"class":154}," 0",[73,882,883],{"class":130}," ?",[73,885,886],{"class":141}," snapshot.tick ",[73,888,889],{"class":130},"%",[73,891,871],{"class":141},[73,893,874],{"class":154},[73,895,896],{"class":130}," :",[73,898,512],{"class":154},[73,900,901,903,906,908,911,913,916,919,922,924,926,928,930,933],{"class":75,"line":94},[73,902,403],{"class":130},[73,904,905],{"class":154}," orderedHumanIds",[73,907,409],{"class":130},[73,909,910],{"class":141}," [",[73,912,758],{"class":130},[73,914,915],{"class":141},"humanIds.",[73,917,918],{"class":137},"slice",[73,920,921],{"class":141},"(rotation), ",[73,923,758],{"class":130},[73,925,915],{"class":141},[73,927,918],{"class":137},[73,929,576],{"class":141},[73,931,932],{"class":154},"0",[73,934,935],{"class":141},", rotation)]\n",[11,937,938],{},"同時押しをきれいに裁く方法ではありません。有利不利を両者に均等にばらまいているだけです。ただ、1秒に20回入れ替わるので、体感としては偏りが消えます。",[11,940,941],{},"入力の送り方も少し工夫していて、方向は変わったときだけ送っています。押しっぱなしの間に同じ方向を送り続けても意味がないからです。",[24,943,945],{"id":944},"消したはずの部屋が一覧に生き返る","消したはずの部屋が、一覧に生き返る",[11,947,948],{},"いちばん厄介だったのはここでした。",[11,950,951],{},"ロビーの部屋一覧は、各部屋が30秒ごとに送ってくる心拍で維持しています。90秒届かなくなった部屋は、落ちたものとみなして一覧から消す。ありふれたリース方式です。",[11,953,954],{},"問題は、部屋を削除した直後に、削除前に送られた心拍が遅れて届いたときです。ロビーから見れば「知らない部屋から心拍が来た」ので、律儀に一覧へ載せ直してしまう。消したはずの部屋が復活し、入ろうとすると当然どこにも繋がらない。",[11,956,957],{},"対策として、削除した部屋のIDを2分間だけ墓標として覚えておくことにしました。この間に届いた心拍は、どれだけ正しい形をしていても無視します。",[11,959,960],{},"ただし、この墓標はメモリの上にしかありません。ロビーのオブジェクトが入れ替われば消えます。実害としては、ちょうどその瞬間に迷子の心拍が届けば同じことが起きるわけで、完全ではない。今のところ2分あれば足りていますが、正直に書いておきます。",[24,962,964],{"id":963},"durableという名前ほどは勝手に続かない","「Durable」という名前ほどは、勝手に続かない",[11,966,967],{},"名前から誤解していたことがあります。Durable Objectは状態を保存してくれますが、動きまで復元してくれるわけではありません。",[11,969,970],{},"盤面はストレージに書いてあるので消えません。でもオブジェクトが起き直ったとき、対戦ループのタイマーが自動で回りはじめるわけではないし、心拍も止まったまま、WebSocketの接続もありません。持続するのはデータであって、実行ではない。ここは実装しながら理解が更新されたところでした。",[24,972,974],{"id":973},"デプロイの順番を間違えて自分で自分を切断した","デプロイの順番を間違えて、自分で自分を切断した",[11,976,977],{},"最後に、実際にやらかした話をひとつ。",[11,979,980],{},"ルーム内チャットを追加したとき、サーバーとページの両方を更新する必要がありました。ページを先に出してしまったんですね。すると、新しいページを開いた人がチャットのメッセージを送る、まだ更新されていないサーバーはそんなメッセージを知らない、不正なメッセージとみなして接続を切る、という流れになります。自分で自分を追い出しました。",[11,982,983],{},"順番はサーバーが先、ページが後です。新しいサーバーは古いメッセージも受け取れますが、逆は成立しない。個人開発でクライアントとサーバーを同じリポジトリで書いていると、両方まとめて出したくなるのですが、出す順番には向きがあります。",[24,985,987],{"id":986},"今できることこれから","今できること、これから",[11,989,990],{},"現在は2人対戦です。合言葉で部屋を作って共有すれば、そのまま対戦できます。決着したあとも5分ほど部屋は残るので、チャットで健闘を称え合えます。",[11,992,993],{},"次は3〜4人での対戦、観戦、そして再戦をそのままの部屋でできるようにしたいと考えています。参加人数を増やすと、さきほどの順番ローテーションのような話がまた出てくるはずで、そこは実際に人を増やしてみないと分からない部分です。",[11,995,996],{},"遊んでみたい方はこちらからどうぞ。ダウンロードは要りません。",[998,999,1000,1006],"ul",{},[1001,1002,1003],"li",{},[15,1004,1005],{"href":17},"サルモノハオトスを遊ぶ",[1001,1007,1008],{},[15,1009,1011],{"href":1010},"\u002Fgames\u002Fsarumono\u002F","ゲームの紹介ページ",[1013,1014,1016],"h3",{"id":1015},"このサイトについて少しだけ","このサイトについて（少しだけ）",[11,1018,1019,1020,1024],{},"SCGF.NETでは自作ゲームを公開しつつ、こうした技術まわりの記録も書いています。作りながら分かったこと、間違えたことをそのまま残していくつもりです。テック系の記事は ",[15,1021,1023],{"href":1022},"\u002Fblog\u002Ftag\u002F%E3%83%86%E3%83%83%E3%82%AF\u002F","#テック"," タグからまとめて読めます。",[1026,1027,1028],"style",{},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}",{"title":68,"searchDepth":82,"depth":82,"links":1030},[1031,1032,1033,1034,1035,1036,1037,1038,1039,1040,1041,1042],{"id":26,"depth":82,"text":27},{"id":39,"depth":82,"text":40},{"id":55,"depth":82,"text":55},{"id":115,"depth":82,"text":115},{"id":348,"depth":82,"text":349},{"id":380,"depth":82,"text":381},{"id":691,"depth":82,"text":691},{"id":824,"depth":82,"text":825},{"id":944,"depth":82,"text":945},{"id":963,"depth":82,"text":964},{"id":973,"depth":82,"text":974},{"id":986,"depth":82,"text":987,"children":1043},[1044],{"id":1015,"depth":88,"text":1016},"2026-08-01","自作の3D対戦ミニゲーム「サルモノハオトス」にオンライン対戦を実装した記録です。常時起動のゲームサーバーもP2Pも選ばず、Cloudflare Workers と Durable Objects で「部屋ごとに小さなサーバーが立つ」構成にしました。50msティックと配信レートの話、切断した人をNPCに引き継がせる仕組み、消したはずの部屋が一覧に生き返った件まで、実コードを引用しながら書きます。",false,"md","\u002Fimages\u002Fblog\u002Feyecatch-sarumono-durable-objects.jpg",{},true,"\u002Fblog\u002F2026-08-01-sarumono-durable-objects",{"title":6,"description":1046},"blog\u002F2026-08-01-sarumono-durable-objects",[1056,1057],"テック","ゲーム開発",null,"UqM1FRWOuU5B3_UbnDAuF4-gwTJtsxkJvvVyUByZwRU",{"id":1061,"title":1062,"body":1063,"date":1755,"description":1756,"draft":1047,"extension":1048,"image":1757,"meta":1758,"navigation":1051,"path":1759,"pr":1051,"seo":1760,"stem":1761,"tags":1762,"updated":1045,"__hash__":1764},"blog\u002Fblog\u002F2026-07-29-moca-coax-lan.md","テレビ線でMac Studioを有線化した。工事なしのMoCAで、AirPlayのカクつきもSSH切断も消えた",{"type":8,"value":1064,"toc":1716},[1065,1068,1071,1074,1078,1081,1084,1088,1091,1094,1098,1101,1104,1110,1113,1117,1120,1123,1127,1130,1133,1136,1139,1143,1146,1149,1153,1156,1159,1163,1166,1169,1176,1179,1186,1190,1193,1197,1200,1203,1206,1210,1213,1216,1219,1223,1226,1230,1240,1243,1246,1249,1253,1256,1262,1265,1272,1279,1283,1286,1289,1297,1305,1308,1311,1386,1389,1392,1395,1401,1404,1407,1413,1416,1422,1425,1431,1435,1438,1441,1444,1448,1451,1456,1462,1466,1472,1479,1482,1485,1532,1535,1538,1541,1544,1548,1551,1554,1559,1562,1565,1569,1572,1575,1579,1582,1585,1591,1594,1597,1601,1604,1608,1619,1623,1631,1635,1643,1647,1652,1655,1658,1661,1663,1666,1671],[11,1066,1067],{},"書斎のMac StudioをWi-Fiでつないでいたのですが、どうにも不安定でした。速度は出ているのに、肝心なときに引っかかる。",[11,1069,1070],{},"有線を引きたい。でもそのためだけに壁に穴を開けたくはない。",[11,1072,1073],{},"そこで、使っていないテレビのアンテナ線を使って有線化しました。工事なしで、挿すだけです。",[24,1075,1077],{"id":1076},"speedtestは速いのになぜか不安定だった","speedtestは速いのに、なぜか不安定だった",[11,1079,1080],{},"我が家は1階にルーターがあり、Mac Studioは2階の書斎にあります。距離は1フロア分。Wi-Fiの電波はちゃんと届いていて、測れば下り270Mbps前後は出ていました。",[11,1082,1083],{},"数字だけ見れば十分です。でも使っていると、明らかにおかしい場面がありました。",[1013,1085,1087],{"id":1086},"_2階のmacから1階のapple-tvへゲーム画面がカクつく","2階のMacから1階のApple TVへ、ゲーム画面がカクつく",[11,1089,1090],{},"一番困っていたのがこれです。2階のMacの画面をAirPlayで1階のApple TVにミラーリングして、テレビの大画面でゲームをする。これがとにかくカクつく。",[11,1092,1093],{},"AirPlayのミラーリングは映像を流し続けるので、一瞬でも詰まるとそれがそのまま画面のカクつきとして見えてしまいます。speedtestが測る「平均の速度」とは、求められているものが違うわけですね。",[1013,1095,1097],{"id":1096},"スマホからmacへのsshが定期的に切れる","スマホからMacへのSSHが定期的に切れる",[11,1099,1100],{},"もうひとつ。かなりマニアックな話ですが、外出先や家の中からスマホでMac StudioにSSHでつないで作業することがあります。これが定期的にセッションごと切れる。",[11,1102,1103],{},"SSHは数百ミリ秒の瞬断でも落ちます。裏を返せば、speedtestでは絶対に見えない不具合を教えてくれるということでもあります。「270Mbps出ているのにSSHは切れる」というのは、速度と安定性が別物だという何よりの証拠でした。",[11,1105,1106,1109],{},[15,1107,1108],{"href":17},"オンラインで対戦できるゲーム","を自分で作っている身としては、通信の遅延と安定性は他人事ではありません。それなのに自宅の環境がこれか、と。",[11,1111,1112],{},"問題は速度ではなく安定性。だったら有線にするしかありません。",[24,1114,1116],{"id":1115},"有線を引きたいでも工事はしたくない","有線を引きたい。でも工事はしたくない",[11,1118,1119],{},"2階まで有線を引く方法はいくつかあります。",[11,1121,1122],{},"まっとうなのはLAN配線工事です。でも壁に穴を開けたり配管を通したりする話になる。お金もかかるし、そもそも家に手を入れたくない。これはナシ。",[1013,1124,1126],{"id":1125},"plcコンセントlanを検討してやめた理由","PLC（コンセントLAN）を検討してやめた理由",[11,1128,1129],{},"次に調べたのがPLCです。電源コンセントに挿すだけで、家の電気配線をLANとして使う製品。工事不要という点では理想的でした。",[11,1131,1132],{},"ただ、調べていくと評判が芳しくない。光回線が1Gbps契約でも、実際には300Mbps程度で頭打ちになるという話がいくつも出てきます。今のWi-Fiで270Mbps出ているのだから、それでは乗り換える意味がありません。",[11,1134,1135],{},"安定性は良くなるかもしれない。でも速度は上がらないどころか、下手をすると誤差の範囲。これも見送りました。",[11,1137,1138],{},"そして「電気配線がダメなら、他に家中を通っている線はないか」と考えたところで、MoCAに行き当たりました。",[24,1140,1142],{"id":1141},"テレビの同軸ケーブルでlanを通すmocaという選択肢","テレビの同軸ケーブルでLANを通す「MoCA」という選択肢",[11,1144,1145],{},"MoCA（Multimedia over Coax Alliance）は、テレビのアンテナ線、つまり同軸ケーブルでLANを通す規格です。最新のMoCA 2.5は最大2.5Gbpsに対応しています。",[11,1147,1148],{},"言われてみれば、同軸ケーブルはもともと放送の信号を通すために作られた線です。電源線を間借りするPLCと比べたら、通信に使う下地としては明らかに有利。",[1013,1150,1152],{"id":1151},"北米では普及日本ではほぼ無名","北米では普及、日本ではほぼ無名",[11,1154,1155],{},"MoCAは北米ではかなり普及していて、ケーブルテレビ事業者が標準的に使っています。一方、日本ではほとんど知られていません。家電量販店で売っているのを見たことがある人は、まずいないと思います。",[11,1157,1158],{},"日本で情報が少ないのは単に普及していないからで、規格そのものに問題があるわけではありません。ただ後で書くように、日本の放送事情ならではの注意点はあります。",[1013,1160,1162],{"id":1161},"買ったものgocoax-ma2500d-2個パック","買ったもの：goCoax MA2500D 2個パック",[11,1164,1165],{},"MoCAアダプタは2台1組で使います。ルーター側に1台、機器側に1台。同軸ケーブルの両端に置いて、その間をLANとして通す仕組みです。",[11,1167,1168],{},"買ったのはgoCoax MA2500Dの2個パック。MoCA 2.5対応で、2.5GbEのポート付き。最大16ノードまで増やせるので、あとから3階や別の部屋を足すこともできます。",[1170,1171],"affiliate-card",{"image":1172,"shop":1173,"title":1174,"url":1175},"https:\u002F\u002Fm.media-amazon.com\u002Fimages\u002FI\u002F61FDmt7039L._AC_SX679_.jpg","Amazon","goCoax MoCA 2.5 アダプター 2個パック（MA2500D \u002F 2.5GbEイーサネットポート）","https:\u002F\u002Fwww.amazon.co.jp\u002Fdp\u002FB09RB1QYR9",[11,1177,1178],{},"中身はこれだけ。本体、ACアダプタ、LANケーブル、同軸ケーブルが1台分ずつ入っています。",[11,1180,1181],{},[1182,1183],"img",{"alt":1184,"src":1185},"goCoax MA2500Dの開封状態。本体・ACアダプタ・黄色のLANケーブル・袋入りの同軸ケーブル","\u002Fimages\u002Fblog\u002Funboxing.jpg",[24,1187,1189],{"id":1188},"我が家の条件ここが再現できるかの分かれ目","我が家の条件（ここが再現できるかの分かれ目）",[11,1191,1192],{},"MoCAが使えるかは、家の配線状況でほぼ決まります。うちの条件を先に書いておくので、ご自宅と照らし合わせてみてください。",[1013,1194,1196],{"id":1195},"_2020年築各部屋にテレビ端子分配器は8k対応","2020年築・各部屋にテレビ端子・分配器は8K対応",[11,1198,1199],{},"我が家は2020年に建てた戸建てです。各部屋にテレビ端子が来ていて、壁の中は当然ながら同軸でつながっています。1階のルーターがある場所にも、2階の書斎にも端子がありました。",[11,1201,1202],{},"そして分配器が8K対応（3224MHz対応）でした。これが後で効いてきます。MoCAが使う周波数を余裕で通せるからです。",[11,1204,1205],{},"2018年以降に建った家なら、4K8K対応の分配器が入っている可能性は高いと思います。型番を調べて対応周波数を見てみてください。",[1013,1207,1209],{"id":1208},"アンテナを設置していないテレビ線が完全に空いていた","アンテナを設置していない＝テレビ線が完全に空いていた",[11,1211,1212],{},"我が家の最大の特徴がこれです。屋根の上にテレビアンテナを設置していません。テレビはインターネットで見ているので必要ないからです。",[11,1214,1215],{},"つまり家中の同軸ケーブルは、一度も使われないまま眠っていたわけです。放送の信号が何も流れていない、まっさらな配線。",[11,1217,1218],{},"これがMoCAには最高の条件でした。この後で書く干渉の問題が、そもそも存在しないので。",[24,1220,1222],{"id":1221},"あなたの家で使えるかは周波数で決まる","あなたの家で使えるかは、周波数で決まる",[11,1224,1225],{},"ここが一番大事なところです。少し技術的な話になりますが、自宅で使えるかどうかがここで決まるので、もう少しお付き合いください。",[1013,1227,1229],{"id":1228},"mocaと日本の放送波はどこで重なるのか","MoCAと日本の放送波はどこで重なるのか",[11,1231,1232,1233,1239],{},"MoCA 2.5が使う周波数帯は1125〜1675MHzです（",[15,1234,1238],{"href":1235,"rel":1236},"https:\u002F\u002Fmocalliance.org\u002Fmoca-2-5\u002F",[1237],"nofollow","MoCA Alliance公式","）。",[11,1241,1242],{},"これを日本のテレビ放送と並べてみます。地上デジタル放送（470〜710MHz）はMoCAと重なりません。一方、BS放送の受信機入力（BS-IF、およそ1032MHz以上）と110度CS放送のIFは、どちらもMoCAの帯域と重なります。",[11,1244,1245],{},"地デジだけなら周波数が離れているので共存できます。問題はBS\u002FCS。アンテナからケーブルに流れてくるBS\u002FCSの信号と、MoCAが使いたい帯域が真正面からぶつかります。",[11,1247,1248],{},"ちなみに4K8K放送の左旋（2224〜3224MHz）はMoCAより上なので、直接は重なりません。8K対応の分配器が「3224MHzまで通す」のはこの左旋のためで、結果としてMoCAの1675MHzも余裕で通ります。",[1013,1250,1252],{"id":1251},"gocoaxが衛星放送では使えないと書いている理由","goCoaxが「衛星放送では使えない」と書いている理由",[11,1254,1255],{},"この干渉、メーカー自身が認めています。goCoax MA2500Dの商品名にはっきりこう書かれています。",[1257,1258,1259],"blockquote",{},[11,1260,1261],{},"Not for Satellite TV (DirecTV, DISH)",[11,1263,1264],{},"北米の衛星放送とは併用できない、という宣言です。理由は日本のBS\u002FCSとまったく同じで、衛星放送がアンテナ線に流す信号がMoCAの帯域とぶつかるから。",[11,1266,1267,1271],{},[1268,1269,1270],"strong",{},"衛星放送を見ている家でMoCAを使うのは、メーカーが想定していない使い方","ということになります。",[11,1273,1274,1275,1278],{},"ここで注意してほしいことがあります。",[1268,1276,1277],{},"Amazon.co.jpの日本語の商品名では、この否定が抜けています。"," 末尾が「ケーブルインターネットホーム用 - 衛星テレビ(ホワイト)」となっていて、これだけ読むと衛星テレビ対応のように見えてしまう。英語表記は \"Not for Satellite TV\" なので、意味は正反対です。日本語の商品名だけで判断しないでください。",[1013,1280,1282],{"id":1281},"poeフィルタが要る家フィルタでは解決しない家","PoEフィルタが要る家、フィルタでは解決しない家",[11,1284,1285],{},"MoCAの解説を読むと、たいてい「PoEフィルタ（ローパスフィルタ）が必須」と書かれています。MoCAの信号がアンテナ線を通って屋外のアンテナまで届き、そこから電波として飛んでしまうのを防ぐためです。日本には電波法があるので、意図しない電波を出すのは避けなければいけません。",[11,1287,1288],{},"では、フィルタを入れればBS\u002FCSと共存できるのか。スペックを見る限り、無理です。",[11,1290,1291,1292,1239],{},"代表的なPoEフィルタ（PPC\u002FBelden）の仕様はこうなっています（",[15,1293,1296],{"href":1294,"rel":1295},"https:\u002F\u002Fwww.ppc-online.com\u002Fhubfs\u002F2057289\u002FSNLP-1GCW,%20SNLP-1GCWWS,%20GLP-1G70CW,%20GLP-1G70CWWS%20PPC%20MoCA%20POE%2040-70dB%20Filters%20Specs_7102019%20(1).pdf",[1237],"仕様書PDF",[998,1298,1299,1302],{},[1001,1300,1301],{},"通過帯域: 5〜1002MHz",[1001,1303,1304],{},"阻止帯域: 1125〜1525MHz（35dB以上減衰。上位モデルは1675MHzまで40〜70dB）",[11,1306,1307],{},"地デジ（470〜710MHz）は通過帯域に入るのでそのまま通ります。でもBS-IFの1125MHz以上は、MoCAと一緒に止められてしまう計算です。フィルタはMoCAだけを狙い撃ちできないので、そこに重なっている放送波も巻き込みます。",[11,1309,1310],{},"まとめるとこうです。",[1312,1313,1314,1330],"table",{},[1315,1316,1317],"thead",{},[1318,1319,1320,1324,1327],"tr",{},[1321,1322,1323],"th",{},"家の状況",[1321,1325,1326],{},"MoCAは使えるか",[1321,1328,1329],{},"必要なもの",[1331,1332,1333,1349,1360,1375],"tbody",{},[1318,1334,1335,1341,1346],{},[1336,1337,1338],"td",{},[1268,1339,1340],{},"アンテナ未設置・テレビはネット配信",[1336,1342,1343],{},[1268,1344,1345],{},"最適",[1336,1347,1348],{},"不要（我が家のケース）",[1318,1350,1351,1354,1357],{},[1336,1352,1353],{},"地デジのみ視聴",[1336,1355,1356],{},"使える",[1336,1358,1359],{},"PoEフィルタ（地デジは通り、MoCA帯だけ止まる）",[1318,1361,1362,1367,1372],{},[1336,1363,1364],{},[1268,1365,1366],{},"BS\u002FCSを視聴している",[1336,1368,1369],{},[1268,1370,1371],{},"非推奨",[1336,1373,1374],{},"フィルタでは解決しない。メーカーも非対応と明記",[1318,1376,1377,1380,1383],{},[1336,1378,1379],{},"集合住宅・共聴設備あり",[1336,1381,1382],{},"要注意",[1336,1384,1385],{},"他世帯へ信号が漏れる可能性",[11,1387,1388],{},"ここは正直に書いておきます。我が家はアンテナがないので、PoEフィルタは使っていません。屋外に信号が漏れる経路そのものがないからです。なので上の表の「地デジのみ」「BS\u002FCS」の欄は、メーカーの公開仕様から読み取ったもので、私が実測したものではありません。当てはまる方は、ご自身でも仕様を確認したうえで判断してください。",[24,1390,1391],{"id":1391},"導入は挿すだけで終わった",[11,1393,1394],{},"構成はいたってシンプルです。壁の中の同軸が、そのままLANケーブルの代わりになります。",[11,1396,1397],{},[1182,1398],{"alt":1399,"src":1400},"我が家の構成図。2階はMac StudioからLANケーブルでMoCAアダプタへ、そこから同軸ケーブルで壁のテレビ端子へ。壁の中の同軸は8K対応の分配器を経由して1階の壁のテレビ端子へ届き、1階でもMoCAアダプタとLANケーブルを介してルーターにつながる。ルーターはドコモ光1Gbpsでインターネットへ","\u002Fimages\u002Fblog\u002Fmoca-diagram.svg",[11,1402,1403],{},"やることは、壁のテレビ端子に同軸を挿すだけです。",[11,1405,1406],{},"こっちが1階側。テレビ端子（青いリングの付いた黒い同軸）と、LAN端子、コンセントが同じ場所にまとまっています。この同軸がMoCAアダプタにつながります。",[11,1408,1409],{},[1182,1410],{"alt":1411,"src":1412},"1階側の壁。同軸のテレビ端子とLAN端子・コンセントが並んでいる","\u002Fimages\u002Fblog\u002Fcoax-outlet-1f.jpg",[11,1414,1415],{},"そして2階側。こっちも挿すのは同軸1本だけ。",[11,1417,1418],{},[1182,1419],{"alt":1420,"src":1421},"2階の壁のテレビ端子に同軸ケーブルを挿した状態。すぐ隣のコンセントからMoCAアダプタの電源を取っている","\u002Fimages\u002Fblog\u002Fcoax-wall-outlet.jpg",[11,1423,1424],{},"設定作業はありませんでした。両方に電源を入れて同軸でつながっていれば、勝手にリンクします。本体のLEDで状態が分かるので、MoCAランプが点いていれば成功です。",[11,1426,1427],{},[1182,1428],{"alt":1429,"src":1430},"1階側に置いたgoCoax MA2500D。MoCAランプが点灯すればリンク確立","\u002Fimages\u002Fblog\u002Fmoca-adapter-led.jpg",[1013,1432,1434],{"id":1433},"米国仕様のacプラグ","米国仕様のACプラグ",[11,1436,1437],{},"ひとつだけ、届いて驚いたことがあります。ACアダプタが米国仕様のプラグでした（さきほどの開封写真の左に写っている黒いやつです）。",[11,1439,1440],{},"日本のコンセントは形が同じなのでそのまま挿さります。実際、問題なく動いています。とはいえ最初はちょっと戸惑うので、書いておきます。",[24,1442,1443],{"id":1443},"結果",[1013,1445,1447],{"id":1446},"実測下り2679mbps-4834mbps","実測：下り267.9Mbps → 483.4Mbps",[11,1449,1450],{},"導入後に、同じMac Studioの接続方式だけを切り替えて測り比べました。同じ日・同じ時間帯・同じ測定ツール（Googleの速度テスト、サーバーはTokyo）です。導入前と導入後で日をまたいで比べるより、回線側の条件が揃うぶん素直な比較になっているはずです。",[1452,1453,1455],"h4",{"id":1454},"wi-fi接続時","Wi-Fi接続時",[11,1457,1458],{},[1182,1459],{"alt":1460,"src":1461},"Wi-Fi接続時の測定結果。下り267.9Mbps \u002F 上り351.9Mbps \u002F レイテンシ15ミリ秒","\u002Fimages\u002Fblog\u002Fspeedtest-wifi.png",[1452,1463,1465],{"id":1464},"moca有線接続時","MoCA有線接続時",[11,1467,1468],{},[1182,1469],{"alt":1470,"src":1471},"MoCA有線接続時の測定結果。下り483.4Mbps \u002F 上り273.6Mbps \u002F レイテンシ8ミリ秒","\u002Fimages\u002Fblog\u002Fspeedtest-moca.png",[11,1473,1474,1475,1478],{},"下りは267.9 → 483.4Mbpsで、およそ1.8倍。そして",[1268,1476,1477],{},"レイテンシが15ミリ秒 → 8ミリ秒とほぼ半分","になりました。今回の目的からすると、こっちの方が大事な変化です。",[1013,1480,1481],{"id":1481},"有線側を3回測るとどうなったか",[11,1483,1484],{},"1回では判断できないので、有線側はその後もう2回測りました。3回分をまとめるとこうです。",[1312,1486,1487,1502],{},[1315,1488,1489],{},[1318,1490,1491,1493,1496,1499],{},[1321,1492],{},[1321,1494,1495],{},"下り",[1321,1497,1498],{},"上り",[1321,1500,1501],{},"レイテンシ",[1331,1503,1504,1518],{},[1318,1505,1506,1509,1512,1515],{},[1336,1507,1508],{},"Wi-Fi（1回）",[1336,1510,1511],{},"267.9",[1336,1513,1514],{},"351.9",[1336,1516,1517],{},"15ミリ秒",[1318,1519,1520,1523,1526,1529],{},[1336,1521,1522],{},"MoCA有線（3回）",[1336,1524,1525],{},"415.4〜483.4",[1336,1527,1528],{},"273.6〜294.6",[1336,1530,1531],{},"7〜8ミリ秒",[11,1533,1534],{},"下りの伸びは1.5〜1.8倍で、測るたびに幅があります。さっき挙げた483.4Mbpsは3回のうち一番良かった値です。一方レイテンシは3回とも7〜8ミリ秒で安定していました。速度より安定性が目的だったので、こちらは期待どおり。",[11,1536,1537],{},"そして正直に書いておくと、上りは3回すべてでWi-Fiを下回りました。273.6 \u002F 283.6 \u002F 294.6Mbps に対して、Wi-Fiは351.9Mbps。1回だけならばらつきで片付けられますが、3回続くと傾向として実在するように見えます。",[11,1539,1540],{},"原因は分かっていません。Wi-Fi側は1回しか測っていないので比較としては非対称で、これ以上は手元のデータからは言えません。ともあれ、有線にすれば全部の数字が良くなるわけではないというのが実測の結果でした。",[11,1542,1543],{},"もうひとつ補足。我が家はドコモ光の1Gbps契約です。この400Mbps台が回線側の上限なのかMoCA側の上限なのかは、今回の測定だけでは切り分けられていません。速度テストは経路全体をまとめて測るので、どこが一番細いかはこの数字からは分かりません。",[1013,1545,1547],{"id":1546},"airplayミラーリングのカクつきが消えた","AirPlayミラーリングのカクつきが消えた",[11,1549,1550],{},"体感で一番はっきり変わったのがこれです。カクつきが消えて、完全にスムーズになりました。",[11,1552,1553],{},"理由は分かりやすくて、我が家のApple TVも有線なので、MoCA導入後は",[11,1555,1556],{},[1268,1557,1558],{},"Mac（有線）→ ルーター → Apple TV（有線）",[11,1560,1561],{},"という経路になり、無線区間がゼロになったからです。映像を流し続ける用途では、この差がそのまま出ますね。",[11,1563,1564],{},"ちなみにApple TV側がWi-Fiのままでも、無線区間が2つから1つに減るので改善は見込めるはずです。送信側だけ有線にするのでも意味はあると思います。",[1013,1566,1568],{"id":1567},"sshが切れなくなった","SSHが切れなくなった",[11,1570,1571],{},"そしてSSHの定期的な切断がなくなりました。これが直ったおかげで、スマホからMacを触る作業がまともに使えるものになりました。",[11,1573,1574],{},"速度が上がったことより、こっちの方が日常のうれしさは大きいです。",[24,1576,1578],{"id":1577},"おまけ眠っていたairmac-time-capsuleが復活した","おまけ：眠っていたAirMac Time Capsuleが復活した",[11,1580,1581],{},"導入してから気づいた、予想外のおまけがあります。",[11,1583,1584],{},"2階に有線が来たことで、昔使っていたAirMac Time Capsuleを2.4GHzのアクセスポイントとして再設置できるようになりました。有線が届いていなければ、ただの置き物です。",[11,1586,1587],{},[1182,1588],{"alt":1589,"src":1590},"押し入れから出してきたAirMac Time Capsule。2階に有線が届いたので2.4GHzのアクセスポイントとして復活した","\u002Fimages\u002Fblog\u002Fairmac-timecapsule.jpg",[11,1592,1593],{},"これで屋外に付けているCircle Viewの防犯カメラの接続が安定しました。2階から電波が届くようになったからです。",[11,1595,1596],{},"メッシュWi-Fiを買い足す手もありましたが、押し入れで眠っていた機材が現役に戻って、しかも困っていたことが解決した。有線が1本通ると、そこを足がかりにできることが増えるんだなと思いました。",[24,1598,1600],{"id":1599},"導入できる家できない家-チェックリスト","導入できる家・できない家 チェックリスト",[11,1602,1603],{},"最後に判断の材料をまとめておきます。",[1013,1605,1607],{"id":1606},"条件が揃っている","◎ 条件が揃っている",[998,1609,1610,1613,1616],{},[1001,1611,1612],{},"テレビをインターネット配信で見ていて、アンテナを設置していない",[1001,1614,1615],{},"各部屋にテレビ端子がある",[1001,1617,1618],{},"分配器が4K8K対応（3224MHz）",[1013,1620,1622],{"id":1621},"使えるが準備が要る","○ 使えるが準備が要る",[998,1624,1625,1628],{},[1001,1626,1627],{},"地デジのみ視聴している → PoEフィルタを検討",[1001,1629,1630],{},"分配器の対応周波数が不明 → 型番を確認",[1013,1632,1634],{"id":1633},"おすすめしない","△ おすすめしない",[998,1636,1637,1640],{},[1001,1638,1639],{},"BS\u002FCSを視聴している → 周波数が重なる。メーカーも非対応と明記",[1001,1641,1642],{},"集合住宅・共聴設備 → 他世帯への影響が確認できない限り避けたい",[1013,1644,1646],{"id":1645},"そもそも不可","× そもそも不可",[998,1648,1649],{},[1001,1650,1651],{},"部屋にテレビ端子がない",[1653,1654],"hr",{},[11,1656,1657],{},"「工事なしで2階に有線を引きたい」と思ってPLCで止まっている方は、けっこういるんじゃないかと思います。使っていないテレビのアンテナ線が家にあるなら、それは規格上2.5Gbpsまで対応する有線LANの経路になり得ます。",[11,1659,1660],{},"次はMacでPS5のリモートプレイを試してみる予定です。遅延にシビアな用途なので、有線化の効果が出るならここだろうと思っています。結果はまた書きます。",[1013,1662,1016],{"id":1015},[11,1664,1665],{},"SCGF.NETはもともと自作ゲームを公開しているサイトですが、これからはこういう技術・機材まわりの記事も書いていきます。",[11,1667,1668,1669,1024],{},"オンラインで対戦できるゲームを作っていると、遅延や通信の安定性は避けて通れません。開発環境そのものの話も含めて、試して分かったことを記録に残していくつもりです。テック系の記事は ",[15,1670,1023],{"href":1022},[1672,1673,1674],"references",{},[998,1675,1676,1682,1689,1695,1702,1709],{},[1001,1677,1678],{},[15,1679,1681],{"href":1235,"rel":1680},[1237],"MoCA Alliance：MoCA 2.5仕様",[1001,1683,1684],{},[15,1685,1688],{"href":1686,"rel":1687},"https:\u002F\u002Fwww.gocoax.com\u002F",[1237],"goCoax 公式サイト",[1001,1690,1691],{},[15,1692,1694],{"href":1294,"rel":1693},[1237],"PPC（Belden）MoCA PoEフィルタ 仕様書 PDF",[1001,1696,1697],{},[15,1698,1701],{"href":1699,"rel":1700},"https:\u002F\u002Fwww.soumu.go.jp\u002Fmain_content\u002F000530382.pdf",[1237],"総務省：放送用周波数の割当ての現状",[1001,1703,1704],{},[15,1705,1708],{"href":1706,"rel":1707},"https:\u002F\u002Fwww.ite.or.jp\u002Fstudy\u002Fmusen\u002Ftips\u002Ftip08.html",[1237],"映像情報メディア学会：テレビ放送用電波の周波数一覧",[1001,1710,1711],{},[15,1712,1715],{"href":1713,"rel":1714},"https:\u002F\u002Fqiita.com\u002Fkerorinfather\u002Fitems\u002F069230f828ef253cf76f",[1237],"MoCA 2.5 を使用しTVの同軸ケーブルを使ってLAN構築（Qiita）",{"title":68,"searchDepth":82,"depth":82,"links":1717},[1718,1722,1725,1729,1733,1738,1741,1747,1748],{"id":1076,"depth":82,"text":1077,"children":1719},[1720,1721],{"id":1086,"depth":88,"text":1087},{"id":1096,"depth":88,"text":1097},{"id":1115,"depth":82,"text":1116,"children":1723},[1724],{"id":1125,"depth":88,"text":1126},{"id":1141,"depth":82,"text":1142,"children":1726},[1727,1728],{"id":1151,"depth":88,"text":1152},{"id":1161,"depth":88,"text":1162},{"id":1188,"depth":82,"text":1189,"children":1730},[1731,1732],{"id":1195,"depth":88,"text":1196},{"id":1208,"depth":88,"text":1209},{"id":1221,"depth":82,"text":1222,"children":1734},[1735,1736,1737],{"id":1228,"depth":88,"text":1229},{"id":1251,"depth":88,"text":1252},{"id":1281,"depth":88,"text":1282},{"id":1391,"depth":82,"text":1391,"children":1739},[1740],{"id":1433,"depth":88,"text":1434},{"id":1443,"depth":82,"text":1443,"children":1742},[1743,1744,1745,1746],{"id":1446,"depth":88,"text":1447},{"id":1481,"depth":88,"text":1481},{"id":1546,"depth":88,"text":1547},{"id":1567,"depth":88,"text":1568},{"id":1577,"depth":82,"text":1578},{"id":1599,"depth":82,"text":1600,"children":1749},[1750,1751,1752,1753,1754],{"id":1606,"depth":88,"text":1607},{"id":1621,"depth":88,"text":1622},{"id":1633,"depth":88,"text":1634},{"id":1645,"depth":88,"text":1646},{"id":1015,"depth":88,"text":1016},"2026-07-29","2階のMac StudioがWi-Fiで不安定だったので、使っていないテレビのアンテナ線でMoCA 2.5の有線化をやってみました。工事なしで下りは実測267→最大483Mbps、レイテンシは15→8ミリ秒。AirPlayのカクつきとSSHの切断も消えました。goCoax MA2500Dの実機レポートと、日本の家で使える条件（アンテナの有無・分配器の対応周波数・BS\u002FCSとの干渉）を実測と公式仕様から整理します。","\u002Fimages\u002Fblog\u002Feyecatch-moca-coax-lan.jpg",{},"\u002Fblog\u002F2026-07-29-moca-coax-lan",{"title":1062,"description":1756},"blog\u002F2026-07-29-moca-coax-lan",[1056,1763],"ネットワーク","IfZhP6gGXXdonEp9bL_JcEpkfRjZlnCgiFoHwojSjM4",{"id":1766,"title":1767,"body":1768,"date":1862,"description":1863,"draft":1047,"extension":1048,"image":1864,"meta":1865,"navigation":1051,"path":1866,"pr":1047,"seo":1867,"stem":1868,"tags":1869,"updated":1058,"__hash__":1872},"blog\u002Fblog\u002F2026-07-21-sarumono.md","新作「サルモノハオトス」公開！みんなで足場を崩し合うわちゃわちゃバトル(オープンテスト版)",{"type":8,"value":1769,"toc":1857},[1770,1776,1779,1785,1789,1792,1798,1809,1812,1815,1822,1828,1831,1834,1841,1844],[11,1771,1772,1773,1775],{},"新作ミニゲーム「",[1268,1774,18],{},"」を公開しました 🐒",[11,1777,1778],{},"足元のマスを崩し合って、最後まで残ったら勝ち。シンプルながらわちゃわちゃした対戦が楽しい、3Dの生き残りバトルです。",[11,1780,1781],{},[1182,1782],{"alt":1783,"src":1784},"サルモノハオトスのタイトル画面","\u002Fimages\u002Fblog\u002Fsarumono-title.png",[24,1786,1788],{"id":1787},"どんなゲーム","どんなゲーム？",[11,1790,1791],{},"浮き島の上で、サルたちが「崩す」で相手の足元のマスを攻撃し合います。マスは予告(色が変化)→崩落→数秒後に復活、というルール。落とされてもしばらくすると別の場所に復活できるので、何度でも参戦できます。",[11,1793,1794],{},[1182,1795],{"alt":1796,"src":1797},"NPCを増やして乱闘中の様子","\u002Fimages\u002Fblog\u002Fsarumono-battle.png",[998,1799,1800,1803,1806],{},[1001,1801,1802],{},"相手の数は1〜3体、難易度も3段階から選べます",[1001,1804,1805],{},"ダッシュ・3列崩し・シールドなどのアイテムも登場",[1001,1807,1808],{},"落下5回で負け。最後の1匹になるまで生き残ろう",[11,1810,1811],{},"初めての人でも迷わないよう、簡単なあそびかたガイドも用意しています。",[24,1813,1814],{"id":1814},"オンライン対戦もできます",[11,1816,1817,1818,1821],{},"今回の目玉は",[1268,1819,1820],{},"オンライン対戦","。合言葉ルームを作れば、友だちとその場で対戦できます。",[11,1823,1824],{},[1182,1825],{"alt":1826,"src":1827},"合言葉ルームでの待機画面とチャット","\u002Fimages\u002Fblog\u002Fsarumono-online.png",[11,1829,1830],{},"対戦が終わった後もチャットができるので、「ナイスファイト！」「もう一回やろう！」と健闘を称え合えます。",[24,1832,1833],{"id":1833},"オープンテスト版として公開します",[11,1835,1836,1837,1840],{},"まだ調整中の部分もある",[1268,1838,1839],{},"オープンテスト版","としての公開です。遊んでみて気になったこと・こうしてほしいということがあれば、ゲーム内の「感想を送る」からぜひ教えてください。いただいた声は今後の調整に活かしていきます。",[11,1842,1843],{},"ダウンロード・インストールは不要、ブラウザを開けばすぐに遊べます(スマホもOK)。",[998,1845,1846,1851],{},[1001,1847,1848,1850],{},[15,1849,1005],{"href":17}," — その場ですぐスタート",[1001,1852,1853,1856],{},[15,1854,1855],{"href":1010},"紹介ページを見る"," — スクリーンショットなど",{"title":68,"searchDepth":82,"depth":82,"links":1858},[1859,1860,1861],{"id":1787,"depth":82,"text":1788},{"id":1814,"depth":82,"text":1814},{"id":1833,"depth":82,"text":1833},"2026-07-21","サルたちが足場を崩し合う新作3D対戦ミニゲーム「サルモノハオトス」を公開しました。オンライン対戦・チャットにも対応。ダウンロード不要、ブラウザですぐ遊べます(オープンテスト版)。","\u002Fimages\u002Fblog\u002Feyecatch-sarumono.jpg",{},"\u002Fblog\u002F2026-07-21-sarumono",{"title":1767,"description":1863},"blog\u002F2026-07-21-sarumono",[1870,1871],"お知らせ","ゲーム","nHXQjt9TJkND7U5nLSTqXSj6v6k-Gg9vrhOKX7ONPSk",{"id":1874,"title":1875,"body":1876,"date":2009,"description":2010,"draft":1047,"extension":1048,"image":2011,"meta":2012,"navigation":1051,"path":2013,"pr":1047,"seo":2014,"stem":2015,"tags":2016,"updated":1058,"__hash__":2017},"blog\u002Fblog\u002F2026-07-16-controller-select.md","スマホでもっと遊びやすく！ブラウザ版に「操作方法の切り替え」を追加しました",{"type":8,"value":1877,"toc":2001},[1878,1885,1888,1894,1901,1905,1909,1915,1918,1922,1928,1935,1939,1946,1953,1956,1968,1998],[11,1879,1880,1881,1884],{},"ブラウザ版のゲームに、",[1268,1882,1883],{},"タッチ操作の方式を選べる機能","を追加しました 🎮",[11,1886,1887],{},"スマホで遊んでいると「十字キーだと指がズレる…」「もっと直感的に動かしたい」——そんな声に応える、操作方法の切り替え機能です。",[11,1889,1890],{},[1182,1891],{"alt":1892,"src":1893},"操作方法を選ぶ画面（十字キー／スティック）","\u002Fimages\u002Fblog\u002Fcontroller-select.png",[11,1895,1896,1897,1900],{},"ゲームを開くと、最初に",[1268,1898,1899],{},"この選択画面","が出ます。好きな方を選んでスタート！",[24,1902,1904],{"id":1903},"_2つの操作方法","2つの操作方法",[1013,1906,1908],{"id":1907},"十字キー従来どおり","十字キー（従来どおり）",[11,1910,1911],{},[1182,1912],{"alt":1913,"src":1914},"十字キーでの操作","\u002Fimages\u002Fblog\u002Fcontroller-dpad.png",[11,1916,1917],{},"画面左下に固定された十字ボタン。押した方向にキャラが動く、おなじみのスタイルです。「ボタンの位置が変わらない方が安心」という人におすすめ。",[1013,1919,1921],{"id":1920},"スティック新しい操作","スティック（新しい操作）",[11,1923,1924],{},[1182,1925],{"alt":1926,"src":1927},"スティックでの操作","\u002Fimages\u002Fblog\u002Fcontroller-stick.png",[11,1929,1930,1931,1934],{},"画面の左下エリアを",[1268,1932,1933],{},"どこでもいいので触ると","、そこを中心にスティックが浮かび、倒した方向へキャラが動きます。指の置き場所を気にしなくていいので、逃げ回るアクションもスムーズ。移動しながら親指を滑らせる感覚で操作できます。",[24,1936,1938],{"id":1937},"いつでも切り替えok","いつでも切り替えOK",[11,1940,1941,1942,1945],{},"一度選んだ操作方法は",[1268,1943,1944],{},"記憶される","ので、次からはそのまますぐ遊べます。",[11,1947,1948,1949,1952],{},"「やっぱりもう片方を試したい」というときは、画面上部の ",[1268,1950,1951],{},"⚙ 操作"," ボタンを押せば、いつでも選び直せます。自分の遊びやすい方を見つけてみてください。",[24,1954,1955],{"id":1955},"対象のゲーム",[11,1957,1958,1959,1963,1964,1967],{},"この機能は、ブラウザで遊べる",[15,1960,1962],{"href":1961},"\u002Fseries\u002Fkuinige\u002F","食い逃げ野郎シリーズ","をはじめ、",[1268,1965,1966],{},"ブラウザ版すべてのゲーム","で使えます。ダウンロードもインストールも不要、「遊ぶ」ボタンを押すだけでその場でスタートできます（もちろんスマホでもOK）。",[998,1969,1970,1977,1984,1991],{},[1001,1971,1972,1976],{},[15,1973,1975],{"href":1974},"\u002Fgames\u002Fkuinige\u002F","食い逃げ野郎"," — 記念すべきシリーズ1作目",[1001,1978,1979,1983],{},[15,1980,1982],{"href":1981},"\u002Fgames\u002Fkuinige2\u002F","食い逃げ野郎2 ～王国の逆襲～"," — ステージ選択制",[1001,1985,1986,1990],{},[15,1987,1989],{"href":1988},"\u002Fgames\u002Fkuinige3\u002F","食い逃げ野郎3 ～FINAL ESCAPE～"," — 弾幕系の高難度",[1001,1992,1993,1997],{},[15,1994,1996],{"href":1995},"\u002Fgames\u002Fkuinige4\u002F","食い逃げ野郎4 ～The kuinige is not enough～"," — シリーズ最大ボリューム",[11,1999,2000],{},"自分にしっくりくる操作で、ぜひクリアを目指してみてください！",{"title":68,"searchDepth":82,"depth":82,"links":2002},[2003,2007,2008],{"id":1903,"depth":82,"text":1904,"children":2004},[2005,2006],{"id":1907,"depth":88,"text":1908},{"id":1920,"depth":88,"text":1921},{"id":1937,"depth":82,"text":1938},{"id":1955,"depth":82,"text":1955},"2026-07-16","食い逃げ野郎シリーズなどのブラウザ版に、タッチ操作の方式を選べる機能を追加。「十字キー」と「スティック」を起動前に選べて、いつでも切り替えOK。スマホで遊びやすくなりました。","\u002Fimages\u002Fblog\u002Feyecatch-controller-select.jpg",{},"\u002Fblog\u002F2026-07-16-controller-select",{"title":1875,"description":2010},"blog\u002F2026-07-16-controller-select",[1870,1871],"e5jRMl5lOROcGNZ5GRWgiQRzwNdkxHaSmvKNIJZ3B0M",{"id":2019,"title":2020,"body":2021,"date":2119,"description":2120,"draft":1047,"extension":1048,"image":2121,"meta":2122,"navigation":1051,"path":2123,"pr":1047,"seo":2124,"stem":2125,"tags":2126,"updated":1058,"__hash__":2127},"blog\u002Fblog\u002F2026-07-08-clear-rate.md","食い逃げ野郎、みんなどこまで行けてる？到達度が見られるようになりました",{"type":8,"value":2022,"toc":2115},[2023,2030,2033,2039,2045,2049,2056,2073,2076,2080,2087,2094,2112],[11,2024,2025,2026,2029],{},"食い逃げ野郎シリーズのゲームページに、ブラウザ版で遊んだ",[1268,2027,2028],{},"みんなの到達度","が見られる機能を追加しました 🎮",[11,2031,2032],{},"「自分はここで詰まったけど、他の人はどこまで行けてるんだろう？」——そんな疑問に答える、ちょっとしたお楽しみ機能です。",[11,2034,2035],{},[1182,2036],{"alt":2037,"src":2038},"ブラウザ版プレイヤーの到達度（表示イメージ）","\u002Fimages\u002Fblog\u002Fstage-funnel.png",[11,2040,2041],{},[2042,2043,2044],"em",{},"※ 画像は表示イメージです。実際の数字はこれからプレイしてくれた人の記録で埋まっていきます。",[24,2046,2048],{"id":2047},"どういう機能","どういう機能？",[11,2050,2051,2052,2055],{},"各ゲームのページを下にスクロールすると、「",[1268,2053,2054],{},"ブラウザ版プレイヤーの到達度","」が表示されます。",[998,2057,2058,2061,2067],{},[1001,2059,2060],{},"何人が挑戦したか",[1001,2062,2063,2064],{},"ステージごとに、",[1268,2065,2066],{},"何％の人がそこまで到達できたか",[1001,2068,2069,2070],{},"そして…",[1268,2071,2072],{},"何％の人がクリアできたか！",[11,2074,2075],{},"ステージが進むほどバーは短くなっていきます。つまり「みんながどこで脱落しているか」が一目で分かるわけです。ラスボスやクリアまでたどり着ける人は、はたして何％——？",[24,2077,2079],{"id":2078},"みんなでクリアを目指そう","みんなでクリアを目指そう！",[11,2081,2082,2083,2086],{},"このシリーズ、実はけっこう歯ごたえがあります。",[1268,2084,2085],{},"クリア率が低いステージほど、突破したときの気持ちよさは格別","。",[11,2088,2089,2090,2093],{},"「あなたはどこまで行ける？」——ダウンロードもインストールも不要、ブラウザの「遊ぶ」ボタンを",[1268,2091,2092],{},"押すだけ","でその場で挑戦できます(スマホでもOK)。みんなでクリア率を押し上げていきましょう！",[998,2095,2096,2100,2104,2108],{},[1001,2097,2098,1976],{},[15,2099,1975],{"href":1974},[1001,2101,2102,1983],{},[15,2103,1982],{"href":1981},[1001,2105,2106,1990],{},[15,2107,1989],{"href":1988},[1001,2109,2110,1997],{},[15,2111,1996],{"href":1995},[11,2113,2114],{},"クリアできたら、ぜひ到達度バーを一番下まで伸ばしてやってください。あなたの1プレイが、この数字に刻まれます。",{"title":68,"searchDepth":82,"depth":82,"links":2116},[2117,2118],{"id":2047,"depth":82,"text":2048},{"id":2078,"depth":82,"text":2079},"2026-07-08","食い逃げ野郎シリーズのブラウザ版に「みんなの到達度」を追加。何％の人がどのステージまで進めたか、クリア率が見られます。みんなでクリアを目指そう！","\u002Fimages\u002Fblog\u002Feyecatch-clear-rate.jpg",{},"\u002Fblog\u002F2026-07-08-clear-rate",{"title":2020,"description":2120},"blog\u002F2026-07-08-clear-rate",[1870,1871],"sBLkzVlDEZbm4vvd8mjuI_8-4bvsFolmi4Xukp9iS1A",{"id":2129,"title":2130,"body":2131,"date":2119,"description":2305,"draft":1047,"extension":1048,"image":2306,"meta":2307,"navigation":1051,"path":2308,"pr":1047,"seo":2309,"stem":2310,"tags":2311,"updated":1058,"__hash__":2312},"blog\u002Fblog\u002F2026-07-08-kuinige-history.md","幻の第5作を発掘。食い逃げ野郎シリーズの歴史をまとめました",{"type":8,"value":2132,"toc":2295},[2133,2144,2148,2155,2161,2164,2170,2174,2181,2214,2217,2221,2232,2236,2239,2254,2260,2266,2269,2272,2275,2281,2288],[11,2134,2135,2136,2139,2140,2143],{},"食い逃げ野郎シリーズをブラウザで遊べるようにしたのを機に、",[1268,2137,2138],{},"このシリーズの歴史をちゃんと調べ直してみました","。Wayback Machine（昔のサイトの記録）やベクターの当時のレビューを掘り起こしていくと、自分たちでも忘れかけていた事実がいろいろ出てきて——最終的に、",[1268,2141,2142],{},"シリーズ完全ガイド","としてまとめることにしました。",[24,2145,2147],{"id":2146},"食い逃げごときでそこまでするのか","「食い逃げごときで、そこまでするのか？」",[11,2149,2150,2151,2154],{},"食い逃げ野郎シリーズが生まれたのは",[1268,2152,2153],{},"2002年","。作者フォウルが高校生の頃の作品です。",[11,2156,2157,2158,2160],{},"テーマはただひとつ、",[1268,2159,2147],{},"。料理を食い逃げした男を、店員だけでなく兵士・戦車・ゾンビ・幽霊までが本気で殺しにくる——という理不尽なギャグ世界。それを、大真面目に・シビアな操作で作り込んだのがこのシリーズです。",[11,2162,2163],{},"当時のベクターのレビュー（2002年10月19日）では、作者本人がこう語っていました。「逃げるゲームを作りたかった。でも、ただ逃げるだけでは面白くない。だから『食い逃げ』という犯罪を題材にした」。友人が「面白い」と言ってくれて、続編を作り続けることになった——そんな原点も残っていました。",[11,2165,2166],{},[1182,2167],{"alt":2168,"src":2169},"食い逃げ野郎（2002年）","\u002Fimages\u002Fgames\u002Fkuinige\u002Fss1.png",[24,2171,2173],{"id":2172},"ツクール製なのにアクションが進化し続けた","ツクール製なのに、アクションが進化し続けた",[11,2175,2176,2177,2180],{},"このシリーズの一番おもしろいところは、",[1268,2178,2179],{},"RPGツクール2000で作られているのに、作を重ねるごとにアクションがどんどん進化していった","ことです。",[998,2182,2183,2188,2194,2208],{},[1001,2184,2185,2187],{},[1268,2186,1975],{},"（2002）— ただ逃げるだけの原点",[1001,2189,2190,2193],{},[1268,2191,2192],{},"食い逃げ野郎2","（2002）— 世界マップ、時間帯で難易度が変化。ベクター掲載作",[1001,2195,2196,2199,2200,2203,2204,2207],{},[1268,2197,2198],{},"食い逃げ野郎3 FINAL ESCAPE","（2002）— ",[1268,2201,2202],{},"ジャンプ機能","と",[1268,2205,2206],{},"リトライ機能","を搭載",[1001,2209,2210,2213],{},[1268,2211,2212],{},"食い逃げ野郎4","（2003）— 横スクロール対応、シリーズ最大ボリューム",[11,2215,2216],{},"新作ほど、作者の技術力とセンスの高まりが感じられます。",[24,2218,2220],{"id":2219},"体験版で意見を募る時代を先取りした作り方","体験版で意見を募る——時代を先取りした作り方",[11,2222,2223,2224,2227,2228,2231],{},"特に食い逃げ野郎4は、",[1268,2225,2226],{},"体験版を先に公開 → 掲示板や専用メールで感想を募集 → 正式版を公開 → アップデートで改善","、という流れで作られていました。「ぜひプレーして感想をください」と当時のサイトに書かれていて、これは今でいう",[1268,2229,2230],{},"Early Access","（早期アクセス）そのもの。2003年当時としては、なかなか先進的だったと思います。",[24,2233,2235],{"id":2234},"そして幻の第5作","そして、幻の第5作",[11,2237,2238],{},"そして今回いちばんの発見が、これ。",[11,2240,2241,2242,2245,2246,2249,2250,2253],{},"シリーズは4作目のあと、",[1268,2243,2244],{},"第5作「食い逃げ野郎5」の制作にも着手していました","。でも完成には至らず、世に出たのは",[1268,2247,2248],{},"体験版のみ","。タイトル画面もRPGツクールのデフォルトのまま——完成を待たずに時が止まった、いわば",[1268,2251,2252],{},"“幻の続編”","です。",[11,2255,2256],{},[1182,2257],{"alt":2258,"src":2259},"幻の第5作・タイトル画面はデフォルトのまま","\u002Fimages\u002Fgames\u002Fkuinige5\u002Fss1.png",[11,2261,2262],{},[1182,2263],{"alt":2264,"src":2265},"食い逃げ野郎5 体験版のオープニング","\u002Fimages\u002Fgames\u002Fkuinige5\u002Fss2.png",[11,2267,2268],{},"その体験版が、今もブラウザでそのまま遊べます。シリーズの、もうひとつの結末をのぞいてみてください。",[24,2270,2271],{"id":2271},"完全ガイドにまとめました",[11,2273,2274],{},"これらをぜんぶ一枚にまとめたのが、こちらのページです。",[1013,2276,2278,2279],{"id":2277},"食い逃げ野郎シリーズ完全ガイド","👉 ",[15,2280,2277],{"href":1961},[11,2282,2283,2284,2287],{},"年表・スクリーンショット・当時のエピソード・幻の第5作まで、シリーズの歩みを一気に振り返れます。各作品は",[1268,2285,2286],{},"ダウンロード不要でそのままブラウザで遊べる","ほか、「みんなの到達度（クリア率）」も見られます。",[11,2289,2290,2291,2294],{},"あなたはどこまで行ける？ ——まずは",[15,2292,2293],{"href":1974},"初代・食い逃げ野郎","から、どうぞ。",{"title":68,"searchDepth":82,"depth":82,"links":2296},[2297,2298,2299,2300,2301],{"id":2146,"depth":82,"text":2147},{"id":2172,"depth":82,"text":2173},{"id":2219,"depth":82,"text":2220},{"id":2234,"depth":82,"text":2235},{"id":2271,"depth":82,"text":2271,"children":2302},[2303],{"id":2277,"depth":88,"text":2304},"👉 食い逃げ野郎シリーズ完全ガイド","2002年に始まった食い逃げ野郎シリーズ。Wayback Machineやベクターのアーカイブを掘り起こし、シリーズの歴史と“幻の第5作（体験版）”までをまとめた「完全ガイド」ページを公開しました。","\u002Fimages\u002Fblog\u002Feyecatch-kuinige-history.jpg",{},"\u002Fblog\u002F2026-07-08-kuinige-history",{"title":2130,"description":2305},"blog\u002F2026-07-08-kuinige-history",[1870,1871],"tILTt0QkmUaxTIkODerp7T3WztwV2ZgFCIxLbRnKKG0",{"id":2314,"title":2315,"body":2316,"date":2440,"description":2441,"draft":1047,"extension":1048,"image":2442,"meta":2443,"navigation":1051,"path":2444,"pr":1047,"seo":2445,"stem":2446,"tags":2447,"updated":1058,"__hash__":2448},"blog\u002Fblog\u002F2026-07-07-browser-games.md","全ゲームがブラウザでそのまま遊べるようになりました",{"type":8,"value":2317,"toc":2436},[2318,2321,2332,2339,2343,2399,2410,2413,2433],[11,2319,2320],{},"これまでSCGF.NETのゲームは、zipをダウンロードして解凍し、RPGツクールのランタイムを入れて…という手順が必要でした。",[11,2322,2323,2324,2327,2328,2331],{},"このたび、",[1268,2325,2326],{},"すべてのタイトルをブラウザ上でそのまま遊べる","ようにしました 🎮\nダウンロードもインストールも不要。ゲームページの緑色の「",[1268,2329,2330],{},"ブラウザで遊ぶ","」ボタンを押すだけで、その場でプレイできます(スマホでもOK)。",[11,2333,2334,2335,2338],{},"当初は一部の作品が対応できていませんでしたが、追加対応が完了し、",[1268,2336,2337],{},"全11作品","が遊べるようになりました。",[24,2340,2342],{"id":2341},"ブラウザで遊べるタイトル全作品","ブラウザで遊べるタイトル(全作品)",[998,2344,2345,2361,2380,2387,2393],{},[1001,2346,2347,2348,2351,2352,2351,2355,2351,2358],{},"食い逃げ野郎シリーズ: ",[15,2349,2350],{"href":1974},"1"," \u002F ",[15,2353,2354],{"href":1981},"2",[15,2356,2357],{"href":1988},"3",[15,2359,2360],{"href":1995},"4",[1001,2362,2363,2364,2351,2368,2351,2372,2351,2376],{},"マイケルシリーズ: ",[15,2365,2367],{"href":2366},"\u002Fgames\u002Fomimai\u002F","お見舞い野郎",[15,2369,2371],{"href":2370},"\u002Fgames\u002Fotsukai\u002F","おつかい野郎",[15,2373,2375],{"href":2374},"\u002Fgames\u002Frusuban\u002F","留守番野郎",[15,2377,2379],{"href":2378},"\u002Fgames\u002Fotetsudai\u002F","お手伝い野郎",[1001,2381,2382,2386],{},[15,2383,2385],{"href":2384},"\u002Fgames\u002Fdrift\u002F","ドリフト野郎","(峠を攻めるレースゲーム)",[1001,2388,2389],{},[15,2390,2392],{"href":2391},"\u002Fgames\u002Fpinpon\u002F","ピンポンダッシュ野郎",[1001,2394,2395],{},[15,2396,2398],{"href":2397},"\u002Fgames\u002Fjuken\u002F","受験戦争",[11,2400,2401,2405,2406,2409],{},[15,2402,2404],{"href":2403},"\u002Fgames\u002F","ゲーム一覧","では、ブラウザで遊べる作品に「",[1268,2407,2408],{},"▶ ブラウザで遊べる","」マークが付いています。",[24,2411,2412],{"id":2412},"操作方法",[998,2414,2415,2421,2427],{},[1001,2416,2417,2420],{},[1268,2418,2419],{},"移動",": 矢印キー(スマホは画面のボタン)",[1001,2422,2423,2426],{},[1268,2424,2425],{},"決定",": Enter または Z",[1001,2428,2429,2432],{},[1268,2430,2431],{},"キャンセル \u002F メニュー",": Esc または X",[11,2434,2435],{},"昔の作品も今の環境でサクッと遊べるようになりました。ぜひ触ってみてください。",{"title":68,"searchDepth":82,"depth":82,"links":2437},[2438,2439],{"id":2341,"depth":82,"text":2342},{"id":2412,"depth":82,"text":2412},"2026-07-07","SCGF.NETの全作品を、ダウンロード不要・ブラウザ上でそのままプレイできるようにしました。","\u002Fimages\u002Fblog\u002Feyecatch-browser-games.jpg",{},"\u002Fblog\u002F2026-07-07-browser-games",{"title":2315,"description":2441},"blog\u002F2026-07-07-browser-games",[1870,1871],"pxK8aclZqz28Yj50LGNOcO86T8yIwLy9Mip96zDNmFk",{"id":2450,"title":2451,"body":2452,"date":2465,"description":2466,"draft":1047,"extension":1048,"image":2467,"meta":2468,"navigation":1051,"path":2469,"pr":1047,"seo":2470,"stem":2471,"tags":2472,"updated":1058,"__hash__":2473},"blog\u002Fblog\u002F2026-07-05-hello.md","サイトをリニューアルしました",{"type":8,"value":2453,"toc":2463},[2454],[11,2455,2456,2457,2462],{},"新生scgf.netへようこそ。旧サイトは ",[15,2458,2461],{"href":2459,"rel":2460},"https:\u002F\u002Fold.scgf.net",[1237],"old.scgf.net"," でそのまま見られます。",{"title":68,"searchDepth":82,"depth":82,"links":2464},[],"2026-07-05","scgf.netを全面リニューアル。旧サイトはold.scgf.netで公開継続中です。","\u002Fimages\u002Fblog\u002Feyecatch-hello.jpg",{},"\u002Fblog\u002F2026-07-05-hello",{"title":2451,"description":2466},"blog\u002F2026-07-05-hello",[1870],"0ffT8gEt596ZjGe7fcQxO1BFBQQveDbqKP8qmFg0t8w",1785563146323]