接續前兩章「遊戲模式選擇」與「六大系統分析」,本章深入實作核心模組程式碼,採用 **PHP + JavaScript + MySQL** 技術棧,強調**資料交互設計**而非單純技術實現。
核心難點不在語法,而在 **狀態同步、併發控制、資料完整性**。
---分離視圖邏輯,支援版面熱更新。現代專案建議改用 **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
// 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]]);
<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 風格,**核心:視窗座標 ↔ 世界座標雙向映射 + 邊界預載**。
---<?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
);
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 */
<?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;
battles,狀態=1,雙方進入戰鬥場景battle_actions 表function resolveRound($battleId, PDO $pdo) {
$battle = getBattle($battleId);
$actions = getActions($battleId, $battle['round']);
// 1. 速度排序
// 2. 依序執行技能、普攻、移動
// 3. 計算傷害、Buff 疊加、觸發被動
// 4. 更新 state、寫入 log
// 5. 判斷勝利條件
// 6. 下一回合或結束
}
log 存儲完整指令流,支援戰鬥回放/舉報核查本系列原計劃深入每個模組,但因工作原因需暫停更新。核心思想已傳達:
網頁遊戲的靈魂在「資料交互設計」,而非渲染技術。
良好的資料表結構 + 清晰的狀態機 + 介面導向的模組劃分 = 可維護、可擴展、高併發的遊戲後端。
若有緣續寫,將涵蓋:叢集架構、熱更新、防作弊、數據分析、運營工具鏈。感謝閱讀,期待在代碼世界相遇。