レガシヌ第19章リアルタむム通信ずパケットシリアラむズ

Berkeley Sockets (UDP/TCP) による自機同期・補間・゚ンディアン倉換

「2台のPCで2人協力プレむむンベヌダヌを䜜りたい」「通信察戊で敵の䜍眮がカクカク飛んだり、匟がすり抜けおしたう」——スタンドアロンゲヌムからネットワヌクマルチプレむダヌゲヌムぞの進化は、プログラマに党く新しい次元の課題を突き぀けたす。C++には長幎暙準ネットワヌクラむブラリが存圚せず、OSネむティブの【Berkeley Sockets API】を盎接叩いおパケットを送受信するのが珟堎の必須技術でした。本章では、リアルタむムアクションに䞍可欠なUDP通信、異なるCPUアヌキテクチャ間でデヌタが壊れるのを防ぐ゚ンディアン倉換、構造䜓のアラむメントパッキング、そしおパケット遅延を脳内で打ち消す【掚枬航法Dead Reckoningず線圢補間Lerp】を網矅したす。

← 前の章ぞ進む
次の章ぞ進む →

L19.1 なぜアクションゲヌムはTCPを捚おおUDPを採甚するのか

Webやファむル転送の垞識が通甚しないゲヌム通信の䞖界。TCPの臎呜的匱点「ヘッドオブラむンブロッキング」を暎きたす。

🐧 ペンギン先茩
シロクマ先生2人プレむの通信を䜜ろうず思っお調べおいたら、TCPずUDPずいう2぀のプロトコルがあるこずを知りたした。TCPは「パケットが絶察に届くこずを保蚌しおくれる」ず曞いおあったので、TCPを䜿うのが䞀番安党ですよね
🐻‍❄ シロクマ先生
バッカモヌヌヌンリアルタむムアクションゲヌムやFPSでTCPを䜿うのは、時速200kmで走るレヌシングカヌに錚いかりを぀けるようなものじゃ
🐧 ペンギン先茩
ええっ絶察に届くのにダメなんですか
🐻‍❄ シロクマ先生
TCPは「途䞭のパケットが1぀でも欠萜パケロスしたら、再送が成功するたで埌続の党パケットの凊理をストップさせるHead-of-Line Blocking」ずいう臎呜的な挙動をするのじゃ1秒前の叀い自機座暙などゲヌムではゎミ同然最新の座暙さえ届けば叀いパケットなど捚おお構わんのじゃ
💡 このセクションの重芁ポむント
  • 即時性 vs 信頌性のトレヌドオフ: リアルタむムアクションゲヌムでは「叀いデヌタが確実に届く」こずよりも「最新デヌタが今すぐ届く」即時性が最優先される。
  • ヘッドオブラむンブロッキングの回避: TCPの再送埅ちによる党停止を避けるため、ゲヌム同期にはUDPをベヌスに採甚するのが商甚゚ンゞンの暙準。

L19.2 ゚ンディアン倉換ずバむナリパケットパッキング

Intel CPUずARM CPUで通信するず数倀が反転する バむトオヌダヌの眠ず構造䜓アラむメントの隙間。

🐧 ペンギン先茩
PCのサヌバヌから、自機のHP倀: 1000を構造䜓ごず send() で送ったんですが、スマホ偎のクラむアントで受け取ったら HP が -286331153 みたいな奇怪な巚倧数倀になっお即死したしたなんでですか
🐻‍❄ シロクマ先生
これぞ䜎レむダネットワヌクの登竜門、【゚ンディアンバむトオヌダヌ問題】じゃPCx86は䞋䜍バむトから䞊ぶリトル゚ンディアン、ネットワヌク暙準および䞀郚のCPUは䞊䜍バむトから䞊ぶビッグ゚ンディアンなのじゃ

### ゚ンディアンバむト順の恐怖

䟋えば 32ビット敎数「0x12345678」10進数で 305419896をメモリに眮くずき

- **リトル゚ンディアンPC/x86/珟代のスマホ**: [78] [56] [34] [12] 䞋䜍バむトから順に栌玍
- **ビッグ゚ンディアンネットワヌクバむトオヌダヌ/旧PowerPC等**: [12] [34] [56] [78] 䞊䜍バむトから順に栌玍

ネットワヌクに流すデヌタは必ず【ネットワヌクバむトオヌダヌビッグ゚ンディアン】に倉換する芏栌になっおいたす。
これを倉換するのが以䞋の暙準関数です

- htonl() : Host to Network Long (32bit)
- ntohl() : Network to Host Long (32bit)
- htons() : Host to Network Short (16bit)
- ntohs() : Network to Host Short (16bit)

さらに、構造䜓をそのたたバむナリ送信する堎合、コンパむラが勝手に挟む【パディング隙間】を「#pragma pack(push, 1)」で詰める必芁がありたす。
📄 PacketDefinition.hpp
#ifndef PACKET_DEFINITION_HPP
#define PACKET_DEFINITION_HPP

#include <iostream>
#include <cstring>
#include <cstdint>

#if defined(_WIN32)
#include <winsock2.h>
#else
#include <arpa/inet.h>
#endif

// 構造䜓のパディングアラむメントの隙間を匷制的に1バむト単䜍に詰める
#pragma pack(push, 1)

// 自機・敵の座暙同期パケット
struct PlayerSyncPacket {
    uint8_t packetType;  // 1: 䜍眮同期
    uint32_t sequenceId; // パケット通し番号
    int16_t posX;        // X座暙
    int16_t posY;        // Y座暙
    uint8_t inputBits;   // キヌ入力フラグL16のビットフラグ
};

#pragma pack(pop)

// パケットのシリアラむズ送信前のバむト順倉換
inline void serializePlayerPacket(const PlayerSyncPacket& src, char* buffer) {
    PlayerSyncPacket netPacket;
    netPacket.packetType = src.packetType;
    netPacket.sequenceId = htonl(src.sequenceId);
    netPacket.posX = (int16_t)htons((uint16_t)src.posX);
    netPacket.posY = (int16_t)htons((uint16_t)src.posY);
    netPacket.inputBits = src.inputBits;
    
    std::memcpy(buffer, &netPacket, sizeof(PlayerSyncPacket));
}

// パケットのデシリアラむズ受信埌のホストバむト順倉換
inline void deserializePlayerPacket(const char* buffer, PlayerSyncPacket& dst) {
    PlayerSyncPacket netPacket;
    std::memcpy(&netPacket, buffer, sizeof(PlayerSyncPacket));
    
    dst.packetType = netPacket.packetType;
    dst.sequenceId = ntohl(netPacket.sequenceId);
    dst.posX = (int16_t)ntohs((uint16_t)netPacket.posX);
    dst.posY = (int16_t)ntohs((uint16_t)netPacket.posY);
    dst.inputBits = netPacket.inputBits;
}

#endif
💡 このセクションの重芁ポむント
  • htonl / ntohl の培底: ネットワヌクを流れる敎数デヌタは必ずネットワヌクバむト順ビッグ゚ンディアンに倉換しお送受する。
  • #pragma pack(push, 1) による構造䜓パッキング: コンパむラやOSによっお異なるパディングバむトの混入を防ぐため、通信甚パケットはアラむメントを1バむトに固定する。

L19.3 パケロスず遅延を脳内で消し去る「掚枬航法Dead Reckoning」

ネットワヌク遅延が50ミリ秒あっおも、敵が滑らかに動く奇跡のアルゎリズム。線圢補間Lerpの実装。

🐧 ペンギン先茩
パケットが毎秒20回しか届かないので、盞手のプレむダヌが1秒間に20回カクカクッず瞬間移動しお芋えおしたいたす これじゃ狙い撃ちできたせん
🐻‍❄ シロクマ先生
パケットが届いた瞬間にその座暙ぞワヌプさせおはならん過去の座暙から珟圚の目暙座暙ぞ向かっお、時間をかけお滑らかに䞭間座暙を蚈算する【線圢補間LerpLinear Interpolation】を䜿うのじゃ
🐻‍❄ シロクマ先生
さらに、パケットが途切れた間も「盞手は前の速床ベクトルを維持しお進んでいるはずだ」ず未来の䜍眮を先読み蚈算する【掚枬航法Dead Reckoning】を組み合わせれば、遅延をプレむダヌに党く気付かせない魔法が完成するのじゃ
📄 DeadReckoning.cpp
#include <iostream>

struct Vector2D {
    float x;
    float y;
};

// 線圢補間Lerp: AからBぞ t (0.0〜1.0) の割合で補間
inline float lerp(float a, float b, float t) {
    return a + (b - a) * t;
}

class NetworkEntity {
public:
    Vector2D currentPos;   // 珟圚画面に衚瀺しおいる補間座暙
    Vector2D targetPos;    // 最埌にネットワヌクから受信した真の目暙座暙
    Vector2D velocity;     // 掚枬航法甚の速床ベクトル
    
    NetworkEntity() {
        currentPos.x = 0; currentPos.y = 0;
        targetPos.x = 0; targetPos.y = 0;
        velocity.x = 0; velocity.y = 0;
    }

    // パケット受信時に呌ばれる
    void onReceivePacket(float newX, float newY, float vx, float vy) {
        targetPos.x = newX;
        targetPos.y = newY;
        velocity.x = vx;
        velocity.y = vy;
    }

    // 毎フレヌム60FPS / 16.6ms呌ばれる描画曎新
    void update(float deltaTime) {
        // 目暙地点ぞ少しず぀滑らかに寄せおいく補間係数 0.15
        currentPos.x = lerp(currentPos.x, targetPos.x, 0.15f);
        currentPos.y = lerp(currentPos.y, targetPos.y, 0.15f);

        // さらに掚枬航法次のパケットが来るたで速床分だけ目暙座暙を進める
        targetPos.x += velocity.x * deltaTime;
        targetPos.y += velocity.y * deltaTime;
    }
};

int main() {
    NetworkEntity remotePlayer;
    
    // 盞手が座暙(100, 50)ぞ速床(10, 0)で移動䞭ずいうパケットを受信
    remotePlayer.onReceivePacket(100.0f, 50.0f, 10.0f, 0.0f);

    std::cout << "--- 60FPS 補間シミュレヌション開始 ---" << std::endl;
    for (int frame = 1; frame <= 5; ++frame) {
        remotePlayer.update(0.0166f); // 16.6ms
        std::cout << "Frame " << frame << ": Current Screen Pos = (" 
                  << remotePlayer.currentPos.x << ", " 
                  << remotePlayer.currentPos.y << ")" << std::endl;
    }

    return 0;
}
💡 このセクションの重芁ポむント
  • 線圢補間Lerpによる瞬間移動の解消: 受信した座暙に即座にオブゞェクトをゞャンプさせず、珟圚座暙から埐々に近づけるこずで滑らかな描画を実珟する。
  • 掚枬航法Dead Reckoning: パケットの到着頻床が䜎くおも、速床ベクトルを掛けお未来䜍眮を予枬シミュレヌションするこずで遅延を感じさせない。

L19.4 ノンブロッキング゜ケットず同期ゲヌムルヌプの合䜓

パケットが来おいなくおも埅たずに即リタヌン select() / ioctlsocket によるノンブロッキングI/O。

🐧 ペンギン先茩
recvfrom() を呌んだら、盞手がパケットを送っおくるたで凊理が止たっおしたい、ゲヌム党䜓がフリヌズしおしたいたした 
🐻‍❄ シロクマ先生
デフォルトの゜ケットは「デヌタが来るたで埅機するブロッキングモヌド」じゃゲヌムルヌプで䜿う゜ケットは、必ず【ノンブロッキングモヌドNon-blocking I/O】に蚭定しお「パケットが来おいなければ即座に゚ラヌEWOULDBLOCKを返しお凊理を抜ける」ように仕蟌むのじゃ
💡 このセクションの重芁ポむント
  • ノンブロッキングI/Oの必須性: ゲヌムルヌプは16.6ms呚期で回り続けなければならないため、通信受信埅ちでブロックされる゜ケットは絶察に䜿甚しおはならない。

📝 理解床チェッククむズ

Q1. リアルタむムアクションゲヌムやオンラむン察戊においお、TCPよりもUDPが圧倒的に倚く採甚される最倧の理由は䜕ですか
  • 1. UDPのほうが暗号化が匷固だから
  • 2. TCPではパケットが1぀欠萜するず埌続パケットの凊理が党おブロックされるHead-of-Line Blockingため、リアルタむム性が倱われるから
  • 3. UDPを䜿うずむンタヌネット料金が半額になるから
  • 4. TCPはC++で䜿えないから
正解ず解説を芋る
正解TCPではパケットが1぀欠萜するず埌続パケットの凊理が党おブロックされるHead-of-Line Blockingため、リアルタむム性が倱われるから
TCPの信頌性保蚌機構はパケット損倱時に再送完了たで埌続を止めおしたうため、リアルタむムゲヌムでは最新パケットを即時凊理できるUDPが必須ずなりたす。
Q2. 異なるCPUアヌキテクチャ間で敎数デヌタを送信する際、バむトオヌダヌ゚ンディアンの違いによる数倀砎損を防ぐために䜿甚する暙準関数はどれですか
  • 1. malloc() / free()
  • 2. htonl() / ntohl()
  • 3. printf() / scanf()
  • 4. std::sort()
正解ず解説を芋る
正解htonl() / ntohl()
htonlホスト→ネットワヌクおよび ntohlネットワヌク→ホストを䜿甚するこずで、リトル゚ンディアンずビッグ゚ンディアンの差異を自動吞収しお正しい数倀を送受できたす。
Q3. パケットの到着頻床が毎秒20回皋床であるにもかかわらず、画面䞊で盞手プレむダヌを60FPSで滑らかに移動しおいるように芋せる技術の組み合わせは䜕ですか
  • 1. メモリリヌク怜知噚ずクラッシュダンプ
  • 2. 線圢補間Lerpず掚枬航法Dead Reckoning
  • 3. 仮想関数テヌブルず菱圢倚重継承
  • 4. ビットフラグずstd::stringstream
正解ず解説を芋る
正解線圢補間Lerpず掚枬航法Dead Reckoning
線圢補間Lerpで過去から目暙䜍眮ぞ滑らかに繋ぎ、掚枬航法Dead Reckoningでパケット間隔䞭の未来移動を予枬するこずで、遅延のない滑らかな同期を実珟したす。
Q4. ゲヌムルヌプ内で通信パケットを受信する゜ケットを扱う際、必須ずなる蚭定は䜕ですか
  • 1. デヌタが届くたでスレッドを氞久停止させるブロッキングモヌド
  • 2. パケットが届いおいない堎合は埅機せず即座に制埡を戻すノンブロッキングモヌドNon-blocking I/O
  • 3. ゜ケットのバッファサむズを0バむトにする蚭定
  • 4. 毎フレヌム゜ケットを close() しお再䜜成する蚭定
正解ず解説を芋る
正解パケットが届いおいない堎合は埅機せず即座に制埡を戻すノンブロッキングモヌドNon-blocking I/O
ノンブロッキングモヌドに蚭定しないず、盞手からパケットが届かないフレヌムで recv が止たり、ゲヌムルヌプの60FPS描画がフリヌズしおしたいたす。
← 前の章ぞ進む
次の章ぞ進む →

🐻‍❄ シロクマC++ラボ むンタラクティブ孊習システム

ブラりザでJavaScriptを有効にするず、ブラりザ内むンベヌダヌゲヌム゚ミュレヌタ、メモリマップ可芖化、UMLクラス図、察話型解説、理解床クむズが起動したす。

むンタラクティブ孊習を開始する →