首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> 網頁程式設計 >> PHP 程式設計 >> PHP 綜合知識

網頁遊戲開發入門教程(三):核心系統實戰——模板、地圖、角色屬性、道具、計時器、事件、戰鬥

     編輯:PHP 綜合知識

前言:從架構到落地的關鍵模組

接續前兩章「遊戲模式選擇」與「六大系統分析」,本章深入實作核心模組程式碼,採用 **PHP + JavaScript + MySQL** 技術棧,強調**資料交互設計**而非單純技術實現。

技術棧選型理由

  • PHP:開發效率高、部署簡單、生態豐富、適合快速迭代
  • JavaScript/Ajax:前端即時互動、無刷新體驗
  • MySQL:關係型資料強一致性、成熟穩定

核心難點不在語法,而在 **狀態同步、併發控制、資料完整性**。

---

一、模板引擎:Smarty 實戰整合

分離視圖邏輯,支援版面熱更新。現代專案建議改用 **Twig** 或 **Blade**。

<?php
// 初始化 Smarty
require 'Smarty.class.php';
$tpl = new Smarty();
$tpl->template_dir = 'templates/';
$tpl->compile_dir = 'templates_c/';
$tpl->cache_dir = 'cache/';
$tpl->config_dir = 'configs/';

// 控制器邏輯
$tpl->assign('user', $userData);
$tpl->assign('items', $itemList);
$tpl->display('game/main.tpl');
{* templates/game/main.tpl *}
<div class="game-container">
  <header>歡迎, {$user.name|escape} (Lv.{$user.level})</header>
  <main>
    {foreach $items as $item}
      <div class="item" data-id="{$item.id}">
        <img src="{$item.icon}" alt="{$item.name}">
        <span>{$item.name}</span>
      </div>
    {/foreach}
  </main>
</div>

注意: 模板中嵌入 PHP 代碼 (``) 雖被 Smarty 支援,但**違反分離原則**,不推薦。美術修改模板應僅涉及 HTML/CSS/模板語法。

---

二、地圖系統:大地圖切片 + 絕對定位 + 拖拽載入

資料結構

CREATE TABLE `map_tiles` (
    `id` INT AUTO_INCREMENT PRIMARY KEY,
    `map_x` INT NOT NULL, `map_y` INT NOT NULL,
    `tile_x` INT NOT NULL, `tile_y` INT NOT NULL, -- 切片在大圖中的像素座標
    `image_url` VARCHAR(255) NOT NULL,
    `terrain_type` TINYINT DEFAULT 0, -- 地形類型
    `passable` TINYINT DEFAULT 1,
    UNIQUE KEY `uk_coord` (`map_x`, `map_y`)
) ENGINE=InnoDB;

PHP 載入可視區域切片

<?php
// map.php?cx=1000&cy=500&vw=800&vh=600
$cx = (int)($_GET['cx'] ?? 0); // 中心點 X
$cy = (int)($_GET['cy'] ?? 0); // 中心點 Y
$vw = (int)($_GET['vw'] ?? 800); // 視窗寬
$vh = (int)($_GET['vh'] ?? 600); // 視窗高
$tileSize = 256; // 切片尺寸

// 計算需載入的切片範圍
$minX = floor(($cx - $vw/2) / $tileSize);
$maxX = floor(($cx + $vw/2) / $tileSize);
$minY = floor(($cy - $vh/2) / $tileSize);
$maxY = floor(($cy + $vh/2) / $tileSize);

$stmt = $pdo->prepare("SELECT * FROM map_tiles WHERE map_x BETWEEN ? AND ? AND map_y BETWEEN ? AND ?");
$stmt->execute([$minX, $maxX, $minY, $maxY]);
$tiles = $stmt->fetchAll(PDO::FETCH_ASSOC);

header('Content-Type: application/json');
echo json_encode(['tiles' => $tiles, 'bounds' => [$minX,$maxX,$minY,$maxY]]);

前端拖拽 + 無限地圖 (原生 JS)

<div id="map-container" style="position:relative;width:800px;height:600px;overflow:hidden;cursor:grab;">
  <div id="map-layer" style="position:absolute;left:0;top:0;transition:transform 0.1s linear;"></div>
</div>

<script>
const container = document.getElementById('map-container');
const layer = document.getElementById('map-layer');
let isDragging = false, startX, startY, layerX = 0, layerY = 0;
let currentCX = 1000, currentCY = 500; // 初始中心座標
const TILE_SIZE = 256;
const VIEW_W = 800, VIEW_H = 600;

// 載入切片
async function loadTiles(cx, cy) {
    const res = await fetch(`map.php?cx=${cx}&cy=${cy}&vw=${VIEW_W}&vh=${VIEW_H}`);
    const {tiles} = await res.json();
    layer.innerHTML = '';
    tiles.forEach(t => {
        const img = document.createElement('img');
        img.src = t.image_url;
        img.style.cssText = `position:absolute;left:${t.tile_x}px;top:${t.tile_y}px;width:${TILE_SIZE}px;height:${TILE_SIZE}px;`;
        layer.appendChild(img);
    });
}

// 拖拽邏輯
container.addEventListener('mousedown', e => {
    isDragging = true;
    startX = e.clientX; startY = e.clientY;
    container.style.cursor = 'grabbing';
});
window.addEventListener('mousemove', e => {
    if (!isDragging) return;
    const dx = e.clientX - startX;
    const dy = e.clientY - startY;
    layer.style.transform = `translate(${layerX + dx}px, ${layerY + dy}px)`;
});
window.addEventListener('mouseup', e => {
    if (!isDragging) return;
    isDragging = false;
    container.style.cursor = 'grab';
    // 更新層位置
    layerX += e.clientX - startX;
    layerY += e.clientY - startY;
    layer.style.transition = 'none';
    layer.style.transform = `translate(${layerX}px, ${layerY}px)`;
    // 計算新中心座標並載入新切片
    currentCX -= Math.round((e.clientX - startX) * (TILE_SIZE / VIEW_W));
    currentCY -= Math.round((e.clientY - startY) * (TILE_SIZE / VIEW_H));
    loadTiles(currentCX, currentCY);
});

// 初始載入
loadTiles(currentCX, currentCY);
</script>

此為簡易版 Google Maps 風格,**核心:視窗座標 ↔ 世界座標雙向映射 + 邊界預載**。

---

三、角色屬性系統:裝備/狀態/Buff 疊加計算

<?php
class Role {
    private $base;      // 基礎屬性
    private $equips;    // 裝備加成
    private $buffs;     // 狀態加成

    public function __construct($roleId, PDO $pdo) {
        $this->loadBase($roleId, $pdo);
        $this->loadEquips($roleId, $pdo);
        $this->loadBuffs($roleId, $pdo);
    }

    private function loadBase($id, $pdo) {
        $st = $pdo->prepare("SELECT * FROM roles WHERE id=?");
        $st->execute([$id]);
        $this->base = $st->fetch(PDO::FETCH_ASSOC) ?: [];
    }

    private function loadEquips($id, $pdo) {
        $st = $pdo->prepare("SELECT e.* FROM equipments e JOIN role_equip re ON e.id=re.equip_id WHERE re.role_id=?");
        $st->execute([$id]);
        $this->equips = $st->fetchAll(PDO::FETCH_ASSOC);
    }

    private function loadBuffs($id, $pdo) {
        $st = $pdo->prepare("SELECT * FROM role_buffs WHERE role_id=? AND expire_time > NOW()");
        $st->execute([$id]);
        $this->buffs = $st->fetchAll(PDO::FETCH_ASSOC);
    }

    // 核心:計算最終屬性
    public function getFinalStats() {
        $stats = $this->base;
        // 1. 裝備加成
        foreach ($this->equips as $eq) {
            foreach (['atk','def','hp','mp','spd'] as $attr) {
                if (!empty($eq[$attr])) $stats[$attr] = ($stats[$attr] ?? 0) + $eq[$attr];
            }
        }
        // 2. Buff 加成 (支援百分比)
        foreach ($this->buffs as $buff) {
            $type = $buff['attr_type']; // atk_pct, hp_flat 等
            $val = $buff['attr_value'];
            if (str_ends_with($type, '_pct')) {
                $baseAttr = substr($type, 0, -4);
                $stats[$baseAttr] = ($stats[$baseAttr] ?? 0) * (1 + $val / 100);
            } else {
                $stats[$type] = ($stats[$type] ?? 0) + $val;
            }
        }
        return $stats;
    }
}

設計要點: 將加成邏輯**前置封裝在 Role 類**,控制器/戰鬥系統直接呼叫 getFinalStats(),避免重複計算、邏輯分散。

---

四、道具系統:靈活的資料結構支援合成/附魔/唯一道具

基礎道具表

CREATE TABLE `items_base` (
    `id` INT PRIMARY KEY,
    `name` VARCHAR(50), `type` TINYINT, -- 1裝備 2消耗 3材料 4任務
    `price` INT, `icon` VARCHAR(100),
    `up_type` VARCHAR(100),   -- 加成屬性: atk|def|hp|crit
    `up_value` VARCHAR(100),  -- 對應數值: 10|5|100|2
    `add_type` VARCHAR(100),  -- 永久加成
    `add_value` VARCHAR(100),
    `clear_debuff` VARCHAR(100), -- 清除debuff類型
    `add_buff` VARCHAR(100)       -- 增加buff ID
);

PHP 解析管道分隔屬性

function parseItemEffects($item) {
    $effects = [];
    $types = explode('|', $item['up_type']);
    $values = explode('|', $item['up_value']);
    foreach ($types as $i => $t) {
        $effects['up'][$t] = (int)($values[$i] ?? 0);
    }
    // 同理解析 add_type, clear_debuff, add_buff
    return $effects;
}

唯一道具/附魔系統 (進階)

-- 唯一道具實例表
CREATE TABLE `items_unique` (
    `uid` BIGINT PRIMARY KEY,           -- 全域唯一ID (雪花算法)
    `base_id` INT NOT NULL,             -- 關聯基礎道具
    `enchant_id` INT DEFAULT 0,         -- 附魔ID
    `durability` INT DEFAULT 100,       -- 耐久度
    `bound` TINYINT DEFAULT 0,          -- 綁定狀態
    `owner_id` INT DEFAULT 0,           -- 擁有者
    `location` TINYINT DEFAULT 2,       -- 1倉庫 2背包 3裝備欄 4拍賣行 5郵件
    `expire_time` DATETIME DEFAULT NULL
);

-- 附魔屬性表
CREATE TABLE `enchants` (
    `id` INT PRIMARY KEY,
    `up_type` VARCHAR(50), `up_value` INT,
    `clear_debuff` VARCHAR(50), `add_buff` VARCHAR(50)
);

道具類統一介面

class Item {
    public static function getInfo($itemId, PDO $pdo) {
        // 判斷是否唯一道具 (UID > 1000000)
        if ($itemId > 1000000) {
            $st = $pdo->prepare("SELECT u.*, b.* FROM items_unique u JOIN items_base b ON u.base_id=b.id WHERE u.uid=?");
            $st->execute([$itemId]);
            $data = $st->fetch(PDO::FETCH_ASSOC);
            if ($data && $data['enchant_id']) {
                $ench = $pdo->query("SELECT * FROM enchants WHERE id={$data['enchant_id']}")->fetch();
                return array_merge(self::parseItemEffects($data), self::parseItemEffects($ench));
            }
            return self::parseItemEffects($data);
        } else {
            $st = $pdo->prepare("SELECT * FROM items_base WHERE id=?");
            $st->execute([$itemId]);
            return self::parseItemEffects($st->fetch(PDO::FETCH_ASSOC));
        }
    }
}

合成邏輯: A(基礎) + B(附魔卷軸) → 消耗 A,B → 建立唯一道具 C(UID) → 寫入 items_unique、扣除背包格位。

---

五、計時器系統:異步任務調度核心

解決「等待 N 秒後執行動作」:建築升級、部隊訓練、技能冷卻、資源產出。

資料表設計

CREATE TABLE `timers` (
    `id` BIGINT AUTO_INCREMENT PRIMARY KEY,
    `user_id` INT NOT NULL,
    `action_type` VARCHAR(50) NOT NULL, -- build_upgrade, train_troop, research...
    `action_params` JSON NOT NULL,      -- {building_id:5, level:3}
    `start_time` DATETIME NOT NULL,
    `end_time` DATETIME NOT NULL,
    `status` TINYINT DEFAULT 0,         -- 0進行中 1完成 2取消
    `server_node` VARCHAR(50) DEFAULT '', -- 叢集節點標識
    INDEX `idx_user_status` (`user_id`, `status`),
    INDEX `idx_end_time` (`end_time`, `status`)
) ENGINE=InnoDB;

三層處理架構

<?php
// 1. 寫入計時器 (發起動作)
function startTimer($userId, $type, $params, $durationSec, PDO $pdo) {
    $end = date('Y-m-d H:i:s', time() + $durationSec);
    $st = $pdo->prepare("INSERT INTO timers (user_id,action_type,action_params,start_time,end_time) VALUES (?,?,?,NOW(),?)");
    $st->execute([$userId, $type, json_encode($params), $end]);
    return $pdo->lastInsertId();
}

// 2. 前端輪詢/長連線查詢 (JS 端倒計時顯示)
/* JS: setInterval(() => fetch('/timer/list?user=123'), 1000); */

// 3. 後台 Worker 定時掃描執行 (Linux crontab 每分鐘)
/* crontab: * * * * * php /path/to/timer_worker.php */

Worker 處理邏輯

<?php
// timer_worker.php
$pdo = new PDO(...);
$pdo->beginTransaction();

// 鎖定待處理計時器 (避免多 Worker 衝突)
$st = $pdo->prepare("SELECT * FROM timers WHERE status=0 AND end_time <= NOW() ORDER BY end_time LIMIT 100 FOR UPDATE SKIP LOCKED");
$st->execute();
$timers = $st->fetchAll(PDO::FETCH_ASSOC);

foreach ($timers as $t) {
    $params = json_decode($t['action_params'], true);
    try {
        // 分發具體處理器
        $handler = "TimerAction_{$t['action_type']}";
        if (function_exists($handler)) {
            $handler($t['user_id'], $params, $pdo);
        }
        $pdo->prepare("UPDATE timers SET status=1 WHERE id=?")->execute([$t['id']]);
    } catch (Exception $e) {
        error_log("Timer {$t['id']} failed: " . $e->getMessage());
        // 可選:標記失敗、重試機制
    }
}
$pdo->commit();

多線程/用戶隔離

action_type 區分業務類型,同一用戶可並行多個計時器 (建築升級 + 部隊訓練)。FOR UPDATE SKIP LOCKED (MySQL 8.0+) 實現高併發 Worker 無衝突搶佔。

---

六、事件控制系統:介面導向設計

將「開始/過程/結束」標準化,支援熱插拔新玩法。

<?php
interface GameAction {
    public function begin(array $params): array;   // 返回計時器參數
    public function process(array $data): array;   // 返回前端顯示數據
    public function end(array $data): array;       // 返回獎勵/結果
}

// 簡單工廠
class ActionFactory {
    public static function create(string $type): GameAction {
        $class = "Action_" . ucfirst($type);
        if (!class_exists($class)) throw new Exception("Unknown action: $type");
        return new $class();
    }
}

// 範例:移動事件
class Action_Move implements GameAction {
    public function begin($params) {
        // 計算距離、時間、消耗體力
        return ['duration' => 300, 'params' => $params];
    }
    public function process($data) {
        // 返回進度百分比、當前座標
        return ['progress' => 45, 'x' => 120, 'y' => 300];
    }
    public function end($data) {
        // 到達目的地、觸發遭遇戰、獲得資源
        return ['result' => 'arrived', 'rewards' => ['gold'=>100]];
    }
}

// 使用流程
$action = ActionFactory::create('move');
$timerParams = $action->begin(['target_x'=>500, 'target_y'=>500]);
$timerId = startTimer($userId, 'move', $timerParams['params'], $timerParams['duration'], $pdo);

// 前端輪詢時
$timer = getTimer($timerId, $pdo);
$display = $action->process($timer);

// 計時器結束 (Worker)
$result = $action->end($timer);
giveRewards($timer['user_id'], $result['rewards']);
---

七、戰鬥系統:半即時回合制架構

核心要求:公平、可驗證、可回放、低延遲。

資料表

CREATE TABLE `battles` (
    `id` BIGINT PRIMARY KEY,
    `type` TINYINT, -- 1 PvE 2 PvP 3 GuildWar
    `attacker_id` INT, `defender_id` INT,
    `round` INT DEFAULT 0,
    `max_rounds` INT DEFAULT 20,
    `round_timeout` INT DEFAULT 30, -- 秒
    `state` JSON, -- 雙方即時狀態 HP/MP/Buff/位置
    `log` JSON,   -- 完整行動日誌
    `status` TINYINT, -- 0準備 1進行中 2結束
    `winner` INT,
    `created_at` DATETIME,
    `ended_at` DATETIME
) ENGINE=InnoDB;

核心流程

  1. 匹配/發起:寫入 battles,狀態=1,雙方進入戰鬥場景
  2. 回合循環:
    • 前端倒計時 (JS 30s)
    • 玩家提交指令 → 寫入 battle_actions 表
    • 超時/雙方提交 → 後端結算回合
  3. 結算邏輯 (PHP):
    function resolveRound($battleId, PDO $pdo) {
        $battle = getBattle($battleId);
        $actions = getActions($battleId, $battle['round']);
        // 1. 速度排序
        // 2. 依序執行技能、普攻、移動
        // 3. 計算傷害、Buff 疊加、觸發被動
        // 4. 更新 state、寫入 log
        // 5. 判斷勝利條件
        // 6. 下一回合或結束
    }
  4. 結束:生成戰報、發放獎勵、更新排行榜、寫入戰鬥回放記錄

關鍵設計

  • 指令驗證:服務端重放邏輯,防作弊
  • 狀態機:明確狀態轉換,避免臟讀
  • 回放系統:log 存儲完整指令流,支援戰鬥回放/舉報核查
  • 非同步通知:WebSocket/Server-Sent Events 推送回合結果,降低輪詢壓力
---

結語與遺憾

本系列原計劃深入每個模組,但因工作原因需暫停更新。核心思想已傳達:

網頁遊戲的靈魂在「資料交互設計」,而非渲染技術。
良好的資料表結構 + 清晰的狀態機 + 介面導向的模組劃分 = 可維護、可擴展、高併發的遊戲後端。

若有緣續寫,將涵蓋:叢集架構、熱更新、防作弊、數據分析、運營工具鏈。感謝閱讀,期待在代碼世界相遇。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved