Bootstrap

一个完整的python webSockets游戏服务器,每100ms接收并广播玩家位置

Python WebSockets 游戏服务器

下面是一个完整的 Python WebSockets 游戏服务器实现,它每 100ms 接收并广播玩家位置信息。这个服务器使用 websocketsasyncio 库来处理 WebSocket 连接和异步操作。

完整代码

#!/usr/bin/env python3
"""
实时游戏位置广播服务器
每100ms接收玩家位置并广播给所有连接的客户端
"""

import asyncio
import json
import logging
import uuid
import websockets
from websockets.exceptions import ConnectionClosed

# 配置日志
logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s [%(levelname)s] %(message)s',
    handlers=[
        logging.StreamHandler()
    ]
)
logger = logging.getLogger(__name__)

# 存储所有连接的玩家
PLAYERS = {}

# 最后一次广播的时间
last_broadcast_time = 0

# 广播间隔 (毫秒)
BROADCAST_INTERVAL = 100

async def register_player(websocket, player_id=None):
    """
    注册新玩家
    :param websocket: WebSocket连接
    :param player_id: 玩家ID (如果未提供则生成)
    :return: 玩家ID
    """
    if player_id is None:
        player_id = str(uuid.uuid4())
    
    PLAYERS[player_id] = {
        "websocket": websocket,
        "position": {"x": 0, "y": 0, "z": 0},
        "rotation": {"x": 0, "y": 0, "z": 0},
        "last_update": asyncio.get_event_loop().time(),
        "color": f"#{uuid.uuid4().hex[:6]}"  # 随机颜色
    }
    
    logger.info(f"玩家 {player_id} 已连接. 当前玩家数: {len(PLAYERS)}")
    return player_id

async def unregister_player(player_id):
    """
    取消注册玩家
    :param player_id: 玩家ID
    """
    if player_id in PLAYERS:
        del PLAYERS[player_id]
        logger.info(f"玩家 {player_id} 已断开连接. 当前玩家数: {len(PLAYERS)}")

async def broadcast_positions():
    """
    向所有玩家广播位置信息
    """
    if not PLAYERS:
        return
    
    # 准备广播数据
    positions = {
        player_id: {
            "position": player["position"],
            "rotation": player["rotation"],
            "color": player["color"]
        }
        for player_id, player in PLAYERS.items()
    }
    
    message = json.dumps({
        "type": "positions_update",
        "players": positions
    })
    
    # 广播到所有客户端
    disconnected_players = []
    for player_id, player in PLAYERS.items():
        try:
            await player["websocket"].send(message)
        except websockets.exceptions.ConnectionClosed:
            disconnected_players.append(player_id)
    
    # 移除断开连接的玩家
    for player_id in disconnected_players:
        await unregister_player(player_id)

async def broadcast_loop():
    """
    定期广播玩家位置的循环
    """
    while True:
        await broadcast_positions()
        await asyncio.sleep(BROADCAST_INTERVAL / 1000)  # 转换为秒

async def handle_join(websocket, message):
    """
    处理玩家加入请求
    """
    player_id = message.get("player_id")
    
    # 如果玩家已存在且提供了ID,使用现有ID
    # 否则生成新ID
    if player_id in PLAYERS:
        logger.info(f"玩家 {player_id} 重新连接")
    else:
        player_id = await register_player(websocket, player_id)
    
    # 发送加入确认
    await websocket.send(json.dumps({
        "type": "join_ack",
        "player_id": player_id,
        "color": PLAYERS[player_id]["color"]
    }))
    
    # 告诉新玩家关于现有玩家的信息
    await websocket.send(json.dumps({
        "type": "positions_update",
        "players": {
            pid: {
                "position": player["position"],
                "rotation": player["rotation"],
                "color": player["color"]
            }
            for pid, player in PLAYERS.items()
        }
    }))
    
    return player_id

async def handle_position_update(player_id, message):
    """
    处理位置更新消息
    """
    if player_id not in PLAYERS:
        return
    
    # 更新玩家位置
    position = message.get("position", {})
    rotation = message.get("rotation", {})
    
    PLAYERS[player_id]["position"] = {
        "x": position.get("x", PLAYERS[player_id]["position"]["x"]),
        "y": position.get("y", PLAYERS[player_id]["position"]["y"]),
        "z": position.get("z", PLAYERS[player_id]["position"]["z"])
    }
    
    PLAYERS[player_id]["rotation"] = {
        "x": rotation.get("x", PLAYERS[player_id]["rotation"]["x"]),
        "y": rotation.get("y", PLAYERS[player_id]["rotation"]["y"]),
        "z": rotation.get("z", PLAYERS[player_id]["rotation"]["z"])
    }
    
    PLAYERS[player_id]["last_update"] = asyncio.get_event_loop().time()

async def game_server(websocket, path):
    """
    处理WebSocket连接的主函数
    """
    player_id = None
    
    try:
        async for message_str in websocket:
            try:
                message = json.loads(message_str)
                message_type = message.get("type", "")
                
                if message_type == "join":
                    player_id = await handle_join(websocket, message)
                
                elif message_type == "position_update":
                    if player_id is None:
                        player_id = message.get("player_id")
                        if player_id not in PLAYERS:
                            player_id = await register_player(websocket, player_id)
                    
                    await handle_position_update(player_id, message)
                
                elif message_type == "chat":
                    # 处理聊天消息
                    if player_id and "text" in message:
                        chat_message = {
                            "type": "chat",
                            "player_id": player_id,
                            "text": message["text"],
                            "color": PLAYERS[player_id]["color"] if player_id in PLAYERS else "#ffffff"
                        }
                        
                        # 广播聊天消息给所有玩家
                        for pid, player in PLAYERS.items():
                            try:
                                await player["websocket"].send(json.dumps(chat_message))
                            except:
                                pass
            
            except json.JSONDecodeError:
                logger.warning(f"收到无效的JSON消息: {message_str}")
    
    except ConnectionClosed:
        logger.info(f"连接已关闭")
    finally:
        # 确保玩家取消注册
        if player_id:
            await unregister_player(player_id)

async def main():
    """
    主函数 - 启动WebSocket服务器和广播循环
    """
    # 启动广播循环
    broadcast_task = asyncio.create_task(broadcast_loop())
    
    # 启动WebSocket服务器
    host = "0.0.0.0"
    port = 8765
    
    logger.info(f"游戏服务器正在启动于 ws://{host}:{port}")
    
    async with websockets.serve(game_server, host, port):
        await asyncio.Future()  # 保持服务器运行

if __name__ == "__main__":
    try:
        asyncio.run(main())
    except KeyboardInterrupt:
        logger.info("服务器正在关闭...")

HTML 客户端示例

以下是一个简单的 HTML 客户端示例,可以连接到上面的 WebSocket 服务器并显示玩家位置:

html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>WebSocket 游戏客户端</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            margin: 0;
            padding: 20px;
            background-color: #f0f0f0;
        }
        
        #gameCanvas {
            background-color: #222;
            display: block;
            margin: 0 auto;
            border: 2px solid #333;
        }
        
        .controls {
            max-width: 800px;
            margin: 20px auto;
            padding: 15px;
            background-color: #fff;
            border-radius: 5px;
            box-shadow: 0 0 10px rgba(0,0,0,0.1);
        }
        
        .chat-box {
            max-width: 800px;
            margin: 20px auto;
            padding: 15px;
            background-color: #fff;
            border-radius: 5px;
            box-shadow: 0 0 10px rgba(0,0,0,0.1);
            display: flex;
            flex-direction: column;
        }
        
        #chatMessages {
            min-height: 150px;
            max-height: 150px;
            overflow-y: auto;
            margin-bottom: 10px;
            padding: 10px;
            background-color: #f9f9f9;
            border: 1px solid #ddd;
            border-radius: 3px;
        }
        
        #chatForm {
            display: flex;
        }
        
        #chatInput {
            flex-grow: 1;
            padding: 8px;
            border: 1px solid #ddd;
            border-radius: 3px;
        }
        
        button {
            padding: 8px 15px;
            background-color: #4CAF50;
            color: white;
            border: none;
            border-radius: 3px;
            cursor: pointer;
            margin-left: 5px;
        }
        
        button:hover {
            background-color: #45a049;
        }
        
        .message {
            margin-bottom: 5px;
            line-height: 1.4;
        }
        
        .status {
            margin-top: 10px;
            color: #666;
        }
    </style>
</head>
<body>
    <h1 style="text-align: center;">WebSocket 游戏客户端</h1>
    
    <canvas id="gameCanvas" width="800" height="600"></canvas>
    
    <div class="controls">
        <p>使用方向键或WASD移动你的角色</p>
        <p class="status">状态: <span id="connectionStatus">未连接</span></p>
        <p>玩家ID: <span id="playerIdDisplay">-</span></p>
        <p>在线玩家: <span id="playerCount">0</span></p>
    </div>
    
    <div class="chat-box">
        <h3>聊天</h3>
        <div id="chatMessages"></div>
        <form id="chatForm">
            <input type="text" id="chatInput" placeholder="输入消息..." autocomplete="off">
            <button type="submit">发送</button>
        </form>
    </div>
    
    <script>
        // 游戏状态
        const gameState = {
            playerId: localStorage.getItem('playerId') || null,
            players: {},
            playerPosition: { x: 400, y: 300, z: 0 },
            playerRotation: { x: 0, y: 0, z: 0 },
            playerColor: '#ffffff',
            keysPressed: {},
            movementSpeed: 5,
            websocket: null,
            connected: false
        };
        
        // 获取DOM元素
        const canvas = document.getElementById('gameCanvas');
        const ctx = canvas.getContext('2d');
        const connectionStatus = document.getElementById('connectionStatus');
        const playerIdDisplay = document.getElementById('playerIdDisplay');
        const playerCount = document.getElementById('playerCount');
        const chatMessages = document.getElementById('chatMessages');
        const chatForm = document.getElementById('chatForm');
        const chatInput = document.getElementById('chatInput');
        
        // 连接到WebSocket服务器
        function connectToServer() {
            connectionStatus.textContent = '正在连接...';
            
            // 创建WebSocket连接
            gameState.websocket = new WebSocket('ws://localhost:8765');
            
            // 连接打开时
            gameState.websocket.onopen = function(event) {
                connectionStatus.textContent = '已连接';
                gameState.connected = true;
                
                // 发送加入消息
                sendJoinMessage();
            };
            
            // 收到消息时
            gameState.websocket.onmessage = function(event) {
                const message = JSON.parse(event.data);
                
                if (message.type === 'join_ack') {
                    gameState.playerId = message.player_id;
                    gameState.playerColor = message.color;
                    playerIdDisplay.textContent = gameState.playerId;
                    
                    // 保存玩家ID到本地存储,以便重连
                    localStorage.setItem('playerId', gameState.playerId);
                }
                else if (message.type === 'positions_update') {
                    gameState.players = message.players;
                    playerCount.textContent = Object.keys(gameState.players).length;
                }
                else if (message.type === 'chat') {
                    addChatMessage(message.player_id, message.text, message.color);
                }
            };
            
            // 连接关闭时
            gameState.websocket.onclose = function(event) {
                connectionStatus.textContent = '连接已关闭';
                gameState.connected = false;
                
                // 尝试重新连接
                setTimeout(connectToServer, 3000);
            };
            
            // 连接错误时
            gameState.websocket.onerror = function(event) {
                connectionStatus.textContent = '连接错误';
                console.error('WebSocket错误:', event);
            };
        }
        
        // 发送加入消息
        function sendJoinMessage() {
            if (gameState.connected) {
                gameState.websocket.send(JSON.stringify({
                    type: 'join',
                    player_id: gameState.playerId
                }));
            }
        }
        
        // 发送位置更新消息
        function sendPositionUpdate() {
            if (gameState.connected && gameState.playerId) {
                gameState.websocket.send(JSON.stringify({
                    type: 'position_update',
                    player_id: gameState.playerId,
                    position: gameState.playerPosition,
                    rotation: gameState.playerRotation
                }));
            }
        }
        
        // 添加聊天消息到聊天框
        function addChatMessage(senderId, text, color) {
            const messageElement = document.createElement('div');
            messageElement.className = 'message';
            
            // 使用玩家颜色作为名称颜色
            const senderLabel = senderId === gameState.playerId ? '你' : `玩家 ${senderId.slice(0, 6)}`;
            messageElement.innerHTML = `<span style="color:${color}"><strong>${senderLabel}:</strong></span> ${text}`;
            
            chatMessages.appendChild(messageElement);
            chatMessages.scrollTop = chatMessages.scrollHeight;
        }
        
        // 发送聊天消息
        function sendChatMessage(text) {
            if (gameState.connected && gameState.playerId) {
                gameState.websocket.send(JSON.stringify({
                    type: 'chat',
                    player_id: gameState.playerId,
                    text: text
                }));
            }
        }
        
        // 监听聊天表单提交
        chatForm.addEventListener('submit', function(e) {
            e.preventDefault();
            const text = chatInput.value.trim();
            if (text) {
                sendChatMessage(text);
                chatInput.value = '';
            }
        });
        
        // 更新玩家位置
        function updatePlayerPosition() {
            // 根据按键更新位置
            if (gameState.keysPressed['ArrowUp'] || gameState.keysPressed['w'] || gameState.keysPressed['W']) {
                gameState.playerPosition.y -= gameState.movementSpeed;
            }
            if (gameState.keysPressed['ArrowDown'] || gameState.keysPressed['s'] || gameState.keysPressed['S']) {
                gameState.playerPosition.y += gameState.movementSpeed;
            }
            if (gameState.keysPressed['ArrowLeft'] || gameState.keysPressed['a'] || gameState.keysPressed['A']) {
                gameState.playerPosition.x -= gameState.movementSpeed;
            }
            if (gameState.keysPressed['ArrowRight'] || gameState.keysPressed['d'] || gameState.keysPressed['D']) {
                gameState.playerPosition.x += gameState.movementSpeed;
            }
            
            // 边界检查
            gameState.playerPosition.x = Math.max(20, Math.min(canvas.width - 20, gameState.playerPosition.x));
            gameState.playerPosition.y = Math.max(20, Math.min(canvas.height - 20, gameState.playerPosition.y));
            
            // 发送位置更新到服务器
            sendPositionUpdate();
        }
        
        // 绘制游戏
        function drawGame() {
            // 清除画布
            ctx.clearRect(0, 0, canvas.width, canvas.height);
            
            // 绘制所有玩家
            for (const [id, playerData] of Object.entries(gameState.players)) {
                const position = playerData.position;
                const color = playerData.color;
                
                // 绘制玩家圆形
                ctx.beginPath();
                ctx.arc(position.x, position.y, 20, 0, Math.PI * 2);
                ctx.fillStyle = color;
                ctx.fill();
                ctx.strokeStyle = '#fff';
                ctx.lineWidth = 2;
                ctx.stroke();
                
                // 绘制玩家ID
                ctx.fillStyle = '#fff';
                ctx.font = '12px Arial';
                ctx.textAlign = 'center';
                ctx.fillText(id.slice(0, 6), position.x, position.y - 30);
            }
            
            // 如果玩家尚未加入游戏但已连接,显示"正在加入..."
            if (gameState.connected && !gameState.players[gameState.playerId]) {
                ctx.fillStyle = '#fff';
                ctx.font = '20px Arial';
                ctx.textAlign = 'center';
                ctx.fillText('正在加入游戏...', canvas.width / 2, canvas.height / 2);
            }
            
            // 如果未连接到服务器,显示"未连接"
            if (!gameState.connected) {
                ctx.fillStyle = '#ff3333';
                ctx.font = '24px Arial';
                ctx.textAlign = 'center';
                ctx.fillText('未连接到服务器', canvas.width / 2, canvas.height / 2);
            }
        }
        
        // 游戏主循环
        function gameLoop() {
            updatePlayerPosition();
            drawGame();
            requestAnimationFrame(gameLoop);
        }
        
        // 键盘事件监听
        window.addEventListener('keydown', function(e) {
            gameState.keysPressed[e.key] = true;
        });
        
        window.addEventListener('keyup', function(e) {
            gameState.keysPressed[e.key] = false;
        });
        
        // 启动游戏
        connectToServer();
        gameLoop();
    </script>
</body>
</html>

html 4

在画布上打开

如何使用

  1. 将 Python 服务器代码保存为 game_server.py
  2. 将 HTML 客户端代码保存为 game_client.html
  3. 安装所需的 Python 依赖:

    pip install websockets
    
  4. 运行服务器:

    python game_server.py
    
  5. 在浏览器中打开 game_client.html 文件
  6. 可以打开多个浏览器窗口,模拟多个玩家

功能特点

  1. 玩家管理

    • 自动生成唯一玩家 ID
    • 支持断开重连(使用本地存储记住玩家 ID)
    • 每个玩家有唯一颜色标识
  2. 位置同步

    • 每 100ms 广播更新所有玩家位置
    • 使用 WASD 或方向键移动角色
    • 支持位置和旋转数据
  3. 聊天系统

    • 玩家可以发送和接收聊天消息
    • 消息显示发送者 ID 和颜色
  4. 可靠性

    • 自动处理连接中断和重连
    • 优雅处理异常和关闭连接

扩展建议

  1. 添加游戏物理

    • 添加碰撞检测
    • 实现简单的物理效果
  2. 游戏进程

    • 添加游戏目标或游戏规则
    • 实现计分系统
  3. 优化

    • 只发送变化的数据以减少带宽使用
    • 添加数据压缩
    • 使用更高效的位置预测算法
  4. 安全性

    • 添加简单的认证系统
    • 服务器端验证以防止作弊

这个实现提供了一个功能完整的实时多人游戏的基础框架,您可以根据需要进一步扩展和改进。

;