用ai制作剪刀石头布游戏

这篇文章是一个模板,展示了如何利用人工智能或计算机视觉制作多人游戏,比如“石头、剪刀、布”,这些游戏涉及手部和身体的动作。

包含的多人游戏:

代码库目前包含三个完整的双人游戏:

  1. 石头、剪刀、布

  2. 对视比赛

  3. 007(对峙或阻挡、重新装填、射击和霰弹枪)——如何玩

img

关于双人游戏的代码

代码文件非常简短,你可以随意修改它们,制作新的游戏,或者学习 WebSockets 的工作原理。它具有最基本的匹配功能,以及可共享的链接。

拥有相同链接的用户将进入同一个“房间”。

技术非常简单,它是一个非常基础的 Express JS 服务器,包含三个多人游戏文件夹。游戏使用 WebRTC 和原生 WebSockets 实现实时连接。人工智能视觉模型由 Roboflow 提供。

眨眼和“石头、剪刀、布”模型已经在 Roboflow Universe 上制作完成。

用人工智能制作“石头、剪刀、布”:如何从零开始制作游戏

我开始自己训练其中一个模型,通过拍摄一段视频并标注约200帧用于目标检测。

img

这个过程听起来比实际操作要难。你只需上传视频,然后围绕手部拖动方框,并写下你希望识别的游戏动作。可能需要通过尝试类似的手势动作来调整。

当我意识到一些手势动作非常相似时,我尝试通过增加更多图像来区分它们,使模型更容易识别玩家的一个动作而不是另一个动作。

例如,在“007”游戏中,玩家通常会将手直直地举起来重新装填,这与射击动作(也是指向前方)看起来非常相似。因此,我尝试在重新装填动作中包含更多手臂的部分,以便模型不会轻易混淆这两种动作。

img

我还包含了各种姿势,以及不同的衣服、房间和朋友,以使模型尽可能灵活。

img

你可以选择大致围绕手部绘制方框进行标注,也可以花费更多时间仅对手部和手指进行分割标注。

我发现,即使一开始花费更多时间对手部和手指进行分割标注也是值得的。经过一段时间后,智能标注工具会变得非常擅长为你完成这项工作。

在线游戏中的视频流

网络摄像头通过 WebRTC 以点对点的方式通过 stunprotocol 和谷歌服务器进行流式传输。这是一种确保你能够在复杂网络环境中实现点对点连接的方法。

你不需要理解它是如何工作的,所有功能都在 common/common.js 文件中:

1
2
3
4
5
// 调用 start(true) 将开始流式传输你的网络摄像头functionstart(isCaller){    peerConnection =newRTCPeerConnection(peerConnectionConfig);    peerConnection.onicecandidate= gotIceCandidate;    peerConnection.ontrack= gotRemoteStream;    for(const track of localStream.getTracks()){        peerConnection.addTrack(track, localStream);    }    if(isCaller){            peerConnection.createOffer().then(createdDescription).catch(errorHandler);    }}// 你的设备 uuid 被发送到服务器,以便它可以将你分配到一个房间functiongotIceCandidate(event){    if(event.candidate!=null){        serverConnection.send(JSON.stringify({'ice': event.candidate,'uuid'uuid}));    }}
```
### 服务器匹配

server/main.js 文件中,服务器有两个“全局”字典,用于跟踪所有正在进行的游戏状态。

let clients = {};let gameStates = {};

1
2
3
4
5
6
7
8
9
10
11
  
这两个字典都会从 WebSocket 中接收 uuid 和“房间”,分别是 ws.uuid 和 ws.room。
- uuid 是之前提到的唯一用户设备 ID

- room 是用户所在的 URL

每个 URL 都是一个随机字符串,例如 
https://handland.lol/random
。如果其他两个用户处于同一个 URL,他们将进入同一个“房间”。这使得事情变得简单且易于分享。

房间也用作 clients 和 gameStates 字典的键名。

if (clients[ws.room].length>=2){  ws.send(“房间已满”);  ws.close();return;}clients[ws.room].push(ws);if(game ===‘rps’){  gameStates[ws.room]={…defaultGameState_rps };if(clients[ws.room].length===2){    gameStates[ws.room].id_p1= clients[ws.room][0].uuid;    gameStates[ws.room].id_p2= clients[ws.room][1].uuid;    roundLoop_rps(gameStates, ws, clients);}}

1
2
3
4
5
6
7
8
9
10
11
  
可以将 WebSocket(或 ws)视为一个玩家。它会将玩家推送到一个房间中,并检查房间是否恰好有两个人,然后才允许游戏开始。
### 多人游戏逻辑

由于游戏是在每几秒钟的节拍上进行的,所有游戏逻辑都在服务器上完成。

在“石头、剪刀、布”游戏中,我们不希望一个玩家提前出拳,从而反复输掉游戏。

所有内容都在 games/rps/server.js 文件中处理。

在 roundLoop_rps 函数中,我们在 2000 毫秒后运行一系列嵌套的计时器。它会更新 gameStates[ws.room] 中的信息。

function roundLoop_rps(gameStates, ws, clients){    gameStates[ws.room].remoteVideoVisible=false;    broadcast_game_rps(…);    setTimeout(()=>{        gameStates[ws.room].gameStateText=“1”;        broadcast_game_rps(…);        setTimeout(()=>{            gameStates[ws.room].gameStateText=“1, 2”;            broadcast_game_rps(…);            setTimeout(()=>{                gameStates[ws.room].gameStateText=“1, 2, Go!”;                gameStates[ws.room].is_resolving=true;                gameStates[ws.room].remoteVideoVisible=true;                handleRound_rps(gameStates, ws, clients);                broadcast_game_rps(…);                if(!gameStates[ws.room].end_game){                    setTimeout(()=>{                        roundLoop_rps(gameStates, ws, clients);                        gameStates[ws.room].gameStateText=“”;                        gameStates[ws.room].is_resolving=false;                        broadcast_game_rps(…);                    },2000);                }                broadcast_game_rps(…);            },2000);        },2000);    },2000);}

1
2
3
4
  
这将在服务器决定 gameStates[ws.room].end_game 为 true 之前无限循环运行。

每次调用 broadcast_game_rps 函数时,它都会告诉房间中的每个玩家更新后的游戏状态。

function broadcast_game_rps(data, room, clients){  clients[room].forEach(client=>{      if(client.readyState===WebSocket.OPEN){          let gameData =JSON.parse(data);          // 根据玩家的 UUID 设置“你”和“对方”字段。          if(client.uuid=== gameData.gameState.id_p1){              …          }else{              …          client.send(JSON.stringify(gameData),{binary:false});      }});}

1
2
3
4
5
6
7
8
9
  
两个玩家不应该从服务器接收到完全相同的信息。甚至有些游戏会故意隐藏某些信息!

在“石头、剪刀、布”游戏中,服务器会告诉一个玩家他们赢了,另一个玩家输了。它通过检查 client.uuid 和 id_p1 或 id_p2 来确定向哪个玩家发送什么信息。你可以在上面的“服务器匹配”部分中查看这些值的设置位置。
### 谁赢了以及如何计分

在多人游戏逻辑的最深层循环中,调用了 handleRound_rps 函数来决定谁赢了。这只是一个包含许多 if 语句的函数,用于判断是否平局或谁赢了这一轮。

服务器会根据谁输了,通过 gameStates[ws.room].health_p1 或 gameStates[ws.room].health_p2 减少一个生命值。

function handleRound_rps(gameStates, ws, clients){if(gameStates[ws.room].move_p1==“ROCK”){      if(gameStates[ws.room].move_p2==“SCISSORS”){          gameStates[ws.room].health_p2= gameStates[ws.room].health_p2-1;      }elseif(gameStates[ws.room].move_p2==“PAPER”){          gameStates[ws.room].health_p1= gameStates[ws.room].health_p1-1;      }elseif(gameStates[ws.room].move_p2==“NOTHING”){          gameStates[ws.room].health_p2= gameStates[ws.room].health_p2-1;      }}elseif(gameStates[ws.room].move_p1==“PAPER”){      if(gameStates[ws.room].move_p2==“ROCK”){          gameStates[ws.room].health_p2= gameStates[ws.room].health_p2-1;      }elseif(gameStates[ws.room].move_p2==“SCISSORS”){          gameStates[ws.room].health_p1= gameStates[ws.room].health_p1-1;      }elseif(gameStates[ws.room].move_p2==“NOTHING”){          gameStates[ws.room].health_p2= gameStates[ws.room].health_p2-1;      }}elseif(gameStates[ws.room].move_p1==“SCISSORS”){      if(gameStates[ws.room].move_p2==“PAPER”){          gameStates[ws.room].health_p2= gameStates[ws.room].health_p2-1;      }elseif(gameStates[ws.room].move_p2==“ROCK”){          gameStates[ws.room].health_p1= gameStates[ws.room].health_p1-1;      }elseif(gameStates[ws.room].move_p2==“NOTHING”){          gameStates[ws.room].health_p2= gameStates[ws.room].health_p2-1;      }}else{      gameStates[ws.room].health_p1= gameStates[ws.room].health_p1-1;      if(gameStates[ws.room].move_p2==“NOTHING”){          gameStates[ws.room].health_p2= gameStates[ws.room].health_p2-1;      }}if(gameStates[ws.room].health_p1<=0|| gameStates[ws.room].health_p2<=0){      gameStates[ws.room].end_game=true;      if(gameStates[ws.room].health_p1>0){          gameStates[ws.room].winnerText_p1=“WINNER!”          gameStates[ws.room].winnerText_p2=“DEFEAT!”      }elseif(gameStates[ws.room].health_p2>0){          gameStates[ws.room].winnerText_p1=“DEFEAT!”          gameStates[ws.room].winnerText_p2=“WINNER!”      }else{          gameStates[ws.room].winnerText_p1=“DRAW!”          gameStates[ws.room].winnerText_p2=“DRAW!”      }      broadcast_game_rps(…);}}

  
我将两个玩家的默认生命值都设置为 3。当其中一个玩家的生命值降到 0 以下时,gameStates[ws.room].end_game 被设置为 True,游戏结束。  
  
  





![江达小记](/images/wechatmpscan.png)