联机游戏原理入门即入土 -- 入门篇

发表于 2年以前  | 总阅读数:314 次

一、背景

联机游戏是指多个客户端共同参与的游戏, 这里主要有以下三种方式

1 . 玩家主机的 P2P 联机模式, 比如流星蝴蝶剑、以及破解游戏(盗版)

2 . 玩家进入公共服务器进行游戏,玩家资料由服务器储存的网络游戏, 比如星际争霸、魔兽等

3 . 可以在单人模式中开启局域网来与他人进行多人游戏,但仅限于连接同一局域网的玩家使用

二、服务器架构历史

大多数联机游戏采用的是 CS 架构, 使用独立设备作为主机与玩家进行交互通信

image.png

client/server 架构

第一代架构(一个服):

这种模式, 将所有玩家的请求发送到同一个线程中进行处理, 主线程每隔一段时间对所有对象进行更新. 适合一些回合制以及运算量小的游戏

第二代架构(分服):

后来随着玩家越来越多, 第一代架构已经不堪重负, 于是就产生了第二种架构 --- 分服, 这样对玩家进行分流, 让玩家在不同的服务器上玩, 不同服之间就像不同的平行世界

第三代架构(世界服):

虽然第二代架构已经可以满足玩家增长的需求 (人满了就再开个服), 但是又出现了玩家开始想跨服玩或者时间长了, 单服务器上没有多少活跃玩家, 所以又出现了世界服模型

基础三层架构

这种设计将网关、和数据存储进行分离, 数据使用同一个数据服务器, 不同游戏服务器的数据交换由网关进行交换

进阶三层架构

在基础三层架构的基础上再进行拆分, 将不同的功能进行抽离独立, 提高性能

无缝地图架构

在进阶三层架构中, 地图的切换总是需要loading (DNF), 为了解决这个问题, 在无缝地图架构中, 由一组节点 (Node) 服务器来管理地图区域, 这个组就是 NodeMaster, 它来进行整体管理, 如果还有更大的就再又更大的 WorldMaster 来进行管理

玩家在地图上进行移动其实就是在 Node 服务器间进行移动, 比如从 A ----> B, 需要由 NodeMaster 把数据从 NodeA 复制到 NodeB 后, 再移除 NodeA 的数据

三、通信

联机最大特点便是多玩家之间的交互, 保证每个玩家的数据和显示一致是必不可少的步骤, 在介绍同步方案之前, 我们先来了解一下如何实现两端的通信

长连接通信 (Socket.io)

极度简陋的聊天室 Demo (React + node)[1]

实现步骤:

1 . 前后端建立连接

2 . 前端发送消息至服务端

3 . 服务端收到消息后对当前所有用户进行广播

4 . 前端收到广播, 更新状态

// client
import React, { memo, useEffect, useState, useRef } from "react";
import { io } from "socket.io-client";
import { nanoid } from "nanoid";

import "./index.css";

const host = "192.168.0.108",
  port = 3101;

const ChatRoom = () => {
  const [socket, setSocket] = useState(io());
  const [message, setMessage] = useState("");
  const [content, setContent] = useState<
    {
      id: string;
      message: string;
      type?: string;
    }[]
  >([]);
  const [userList, setUserList] = useState<string[]>([]);

  const userInfo = useRef({ id: "", enterRoomTS: 0 });
  const roomState = useRef({
    content: [] as {
      id: string;
      message: string;
      type?: string;
    }[],
  });

  useEffect(() => {
    // 初始化 Socket
    initSocket();

    // 初始化用户信息
    userInfo.current = {
      id: nanoid(),
      enterRoomTS: Date.now(),
    };
  }, []);

  useEffect(() => {
    roomState.current.content = content;
  }, [content]);

  const initSocket = () => {
    const socket = io(`ws://${host}:${port}`);
    setSocket(socket);

    // 建立连接
    socket.on("connect", () => {
      console.log("连接成功");
      //用户加入
      socket.emit("add user", userInfo.current);
    });

    //用户加入聊天室
    socket.on("user joined", ({ id, userList }) => {
      const newContent = [...roomState.current.content];
      newContent.push({ id, message: `${id}加入`, type: "tip" });

      setContent(newContent);
      setUserList(userList);
    });

    //新消息
    socket.on("new message", ({ id, message }) => {
      const newContent = [...roomState.current.content];
      newContent.push({ id, message });

      setContent(newContent);
    });

    //用户离开聊天室
    socket.on("user leave", function ({ id, userList }) {
      const newContent = [...roomState.current.content];
      newContent.push({ id, message: `${id}离开`, type: "tip" });

      setContent(newContent);
      setUserList(userList);
    });
  };

  const handleEnterSend: React.KeyboardEventHandler<HTMLTextAreaElement> = (
    e
  ) => {
    if (e.key === "Enter") {
      //客户端发送新消息
      socket.emit("new message", {
        id: userInfo.current.id,
        message,
      });
      setMessage("");
      e.preventDefault();
    }
  };

  const handleButtonSend = () => {
    //客户端发送新消息
    socket.emit("new message", {
      id: userInfo.current.id,
      message,
    });
    setMessage("");
  };

  const handleChange: React.ChangeEventHandler<HTMLTextAreaElement> = (e) => {
    const val = e.target.value ?? "";
    setMessage(val);
  };

  const handleQuit = () => {
    //断开连接
    socket.disconnect();
  };

  return (
    <div>
      //...
    </div>
  );
};

export default memo(ChatRoom);
// server
import { Server } from "socket.io";

const host = "192.168.0.108",
  port = 3101;

const io = new Server(port, { cors: true });
const sessionList = [];

io.on("connection", (socket) => {
  console.log("socket connected successful");

  //用户进入聊天室
  socket.on("add user", ({ id }) => {
    socket.id = id;
    if (!sessionList.includes(id)) {
      sessionList.push(id);
    }

    console.log(`${id} 已加入房间, 房间人数: ${sessionList.length}`);
    console.log(JSON.stringify(sessionList));

    io.emit("user joined", { id, userList: sessionList });
  });

  //发送的新消息
  socket.on("new message", ({ id, message }) => {
    io.emit("new message", { id, message });
  });

  socket.on("disconnect", () => {
    sessionList.splice(sessionList.indexOf(socket.id), 1);
    socket.broadcast.emit("user leave", {
      id: socket.id,
      userList: sessionList,
    });
  });
});

四、同步策略

现在大多游戏常用的两种同步技术方向分别是: 帧同步状态同步

帧同步

帧同步的方式服务端很简单, 只承担了操作转发的操作, 你给我了什么, 我就通知其他人你怎么了, 具体的执行是各个客户端拿到操作后自己执行

image.png

状态同步

状态同步是客户端将操作告诉服务端, 然后服务端拿着操作进行计算, 最后把结果返给各个客户端, 然后客户端根据新数据进行渲染即可

image.png

延时同步处理

我们先看看不处理延时的情况:

image.png

网络延时是无法避免的, 但我们可以通过一些方法让玩家感受不到延时, 主要有以下三个步骤

预测

先说明预测不是预判, 也需要玩家进行操作, 只是 客户端 不再等待 服务端 的返回, 先自行计算操作展示给玩家, 等 服务端 状态返回后再次渲染:

image.png

虽然在客户端通过预测的方式提前模拟了玩家的操作, 但是服务端返回的状态始终是之前的状态, 所以我们会发现有状态回退的现象发生

和解

预测能让客户端流畅的运行, 如果我们在此基础上再做一层处理是否能够避免状态回退的方式呢? 如果我们在收到服务端的延迟状态的时候, 在这个延迟基础上再进行预测就可以避免回退啦! 看看下面的流程:

image.png

我们把服务端返回老状态作为基础状态, 然后再筛选出这个老状态之后的操作进行预测, 这样就可以避免客户端回退的现象发生

插值

我们通过之前的 预测、和解 两个步骤, 已经可以实现 客户端 无延迟且不卡顿的效果, 但是联机游戏是多玩家交互, 自己虽然不卡了, 但是在别的玩家那里却没有办法做预测和和解, 所以在其他玩家的视角中, 我们仍然是一卡一卡的

我们这时候使用一些过渡动画, 让移动变得丝滑起来, 虽然本质上接受到的实际状态还是一卡一卡的, 但是至少看起来不卡

五、同步策略主要实现[2]

// index.tsx
type Action = {
  actionId: string;
  actionType: -1 | 1;
  ts: number;
};

const GameDemo = () => {
  const [socket, setSocket] = useState(io());
  const [playerList, setPlayerList] = useState<Player[]>([]);
  const [serverPlayerList, setServerPlayerList] = useState<Player[]>([]);
  const [query, setQuery] = useUrlState({ port: 3101, host: "localhost" });

  const curPlayer = useRef(new Player({ id: nanoid(), speed: 5 }));
  const btnTimer = useRef<number>(0);
  const actionList = useRef<Action[]>([]);
  const prePlayerList = useRef<Player[]>([]);

  useEffect(() => {
    initSocket();
  }, []);

  const initSocket = () => {
    const { host, port } = query;
    console.error(host, port);

    const socket = io(`ws://${host}:${port}`);
    socket.id = curPlayer.current.id;

    setSocket(socket);

    socket.on("connect", () =>  {
      // 创建玩家
      socket.emit("create-player", { id: curPlayer.current.id });
    });

    socket.on("create-player-done", ({ playerList }) =>  {
      setPlayerList(playerList);
      const curPlayerIndex = (playerList as Player[]).findIndex(
        (player) =>  player.id === curPlayer.current.id
      );
      curPlayer.current.socketId = playerList[curPlayerIndex].socketId;
    });

    socket.on("player-disconnect", ({ id, playerList }) =>  {
      setPlayerList(playerList);
    });

    socket.on("interval-update", ({ state }) => {
      curPlayer.current.state = state;
    });


    socket.on(
      "update-state",
      ({
        playerList,
        actionId: _actionId,
      }: {
        playerList: Player[];
        actionId: string;
        ts: number;
      }) => {
        setPlayerList(playerList);

        const player = playerList.find((p) => curPlayer.current.id === p.id);
        if (player) {
          // 和解
          if (player.reconciliation &&  _actionId) {
            const actionIndex = actionList.current.findIndex(
              (action) =>  action.actionId ===  _actionId
            );

            // 偏移量计算
            let pivot = 0;
            // 过滤掉状态之前的操作, 留下预测操作
            for (let i = actionIndex; i < actionList.current.length; i++) {
              pivot += actionList.current[i].actionType;
            }

            const newPlayerState = cloneDeep(player);
            // 计算和解后的位置
            newPlayerState.state.x += pivot * player.speed;
            curPlayer.current = newPlayerState;
          } else {
            curPlayer.current = player;
          }
        }

        playerList.forEach((player) => {
          // 其他玩家
          if (player.interpolation && player.id !== curPlayer.current.id) {
            // 插值
            const prePlayerIndex = prePlayerList.current.findIndex(
              (p) =>  player.id === p.id
            );
            // 第一次记录
            if (prePlayerIndex === -1) {
              prePlayerList.current.push(player);
            } else {
              // 如果已经有过去的状态
              const thumbEl = document.getElementById(`thumb-${player.id}`);

              if (thumbEl) {
                const prePos = {
                  x: prePlayerList.current[prePlayerIndex].state.x,
                };

                new TWEEN.Tween(prePos)
                  .to({ x: player.state.x }, 100)
                  .onUpdate(() =>  {
                    thumbEl.style.setProperty(
                      "transform",
                      `translateX(${prePos.x}px)`
                    );
                    console.error("onUpdate", 2, prePos.x);
                  })
                  .start();
              }
              prePlayerList.current[prePlayerIndex] = player;
            }
          }
        });
      }
    );

    // 服务端无延迟返回状态
    socket.on("update-real-state", ({ playerList }) => {
      setServerPlayerList(playerList);
    });
  };

  // 玩家操作 (输入)
  // 向左移动
  const handleLeft = () =>  {
    const { id, predict, speed, reconciliation } = curPlayer.current;
    // 和解
    if (reconciliation) {
      const actionId = uuidv4();
      actionList.current.push({ actionId, actionType: -1, ts: Date.now() });
      socket.emit("handle-left", { id, actionId });
    } else {
      socket.emit("handle-left", { id });
    }

    // 预测
    if (predict) {
      curPlayer.current.state.x -= speed;
    }

    btnTimer.current = window.requestAnimationFrame(handleLeft);
    TWEEN.update();
  };

  // 向右移动
  const handleRight = (time?: number) =>  {
    const { id, predict, speed, reconciliation } = curPlayer.current;
    // 和解
    if (reconciliation) {
      const actionId = uuidv4();
      actionList.current.push({ actionId, actionType: 1, ts: Date.now() });
      socket.emit("handle-right", { id, actionId });
    } else {
      socket.emit("handle-right", { id });
    }
    // 预测
    if (predict) {
      curPlayer.current.state.x += speed;
    }

    // socket.emit("handle-right", { id });

    btnTimer.current = window.requestAnimationFrame(handleRight);
    TWEEN.update();
  };

  return (
    <div>
      <div>
        当前用户
        <div>{curPlayer.current.id}</div>
        在线用户
        {playerList.map((player) => {
          return (
            <div
              key={player.id}
              style={{ display: "flex", justifyContent: "space-around" }}
            >
              <div>{player.id}</div>
              <div>{moment(player.enterRoomTS).format("HH:mm:ss")}</div>
            </div>
          );
        })}
      </div>

      {playerList.map((player, index) => {
        const mySelf = player.id === curPlayer.current.id;
        const disabled = !mySelf;

        return (
          <div className="player-wrapper" key={player.id}>
            <div style={{ display: "flex", justifyContent: "space-evenly" }}>
              <div style={{ color: mySelf ? "red" : "black" }}>{player.id}</div>
              <div>
                预测
                <input
                  disabled={disabled}
                  type="checkbox"
                  checked={player.predict}
                  onChange={() => {
                    socket.emit("predict-change", {
                      id: curPlayer.current.id,
                      predict: !player.predict,
                    });
                  }}
                ></input>
              </div>
              <div>
                和解
                <input
                  disabled={disabled}
                  type="checkbox"
                  checked={player.reconciliation}
                  onChange={() => {
                    socket.emit("reconciliation-change", {
                      id: curPlayer.current.id,
                      reconciliation: !player.reconciliation,
                    });
                  }}
                ></input>
              </div>
              <div>
                插值
                <input
                  // disabled={!disabled}
                  disabled={true}
                  type="checkbox"
                  checked={player.interpolation}
                  onChange={() => {
                    socket.emit("interpolation-change", {
                      id: player.id,
                      interpolation: !player.interpolation,
                    });
                  }}
                ></input>
              </div>
            </div>

            <div>Client</div>
            {mySelf ? (
              <div className="track">
                <div
                  id={`thumb-${player.id}`}
                  className="left"
                  style={{
                    backgroundColor: teamColor[player.state.team],
                    transform: `translateX(${
                      // 是否预测
                      curPlayer.current.predict
                        ? curPlayer.current.state.x
                        : player.state.x
                    }px)`,
                  }}
                >
                  自己
                </div>
              </div>
            ) : (
              <div className="track">
                <div
                  id={`thumb-${player.id}`}
                  className="left"
                  style={
                    // 是否插值
                    player.interpolation
                      ? {
                          backgroundColor: teamColor[player.state.team],
                        }
                      : {
                          backgroundColor: teamColor[player.state.team],
                          transform: `translateX(${player.state.x}px)`,
                        }
                  }
                >
                  别人
                </div>
              </div>
            )}

            <div>Server</div>
            {serverPlayerList.length && (
              <div className="server-track">
                <div
                  className="left"
                  style={{
                    backgroundColor: teamColor[player.state.team],
                    transform: `translateX(${
                      serverPlayerList[index]?.state?.x ?? 0
                    }px)`,
                  }}
                ></div>
              </div>
            )}

            <div>
              delay:
              <input
                type="number"
                min={1}
                max={3000}
                onChange={(e) => {
                  const val = parseInt(e.target.value);
                  socket.emit("delay-change", {
                    delay: val,
                    id: curPlayer.current.id,
                  });
                }}
                value={player.delay}
                disabled={disabled}
              ></input>
              speed:
              <input
                onChange={(e) => {
                  const val =
                    e.target.value === "" ? 0 : parseInt(e.target.value);
                  socket.emit("speed-change", {
                    speed: val,
                    id: curPlayer.current.id,
                  });
                }}
                value={player.speed}
                disabled={disabled}
              ></input>
            </div>
            <button
              onMouseDown={() => {
                window.requestAnimationFrame(handleLeft);
              }}
              onMouseUp={() => {
                cancelAnimationFrame(btnTimer.current);
              }}
              disabled={disabled}
            >
              左
            </button>
            <button
              onMouseDown={() => {
                window.requestAnimationFrame(handleRight);
              }}
              onMouseUp={() => {
                cancelAnimationFrame(btnTimer.current);
              }}
              disabled={disabled}
            >
              右
            </button>
          </div>
        );
      })}
    </div>
  );
};

export default memo(GameDemo);

六、结束语

首先感谢在学习过程中给我提供帮助的大佬King[3]. 我先模仿着他的动图[4]和讲解的思路自己实现了一版动图里面的效果[5], 我发现我的效果总是比较卡顿, 于是我拿到了动图demo的代码进行学习, 原来只是一个纯前端的演示效果, 所以与我使用 socket 的效果有所不同.

为什么说标题是入门即入土? 网络联机游戏的原理还有很多很多, 通信和同步测量只是基础中的基础, 在学习的过程中才发现, 联机游戏的领域还很大, 这对我来说是一个很大的挑战.

七、参考

  • 如何设计大型游戏服务器架构?-今日头条[6]
  • 2 天做了个多人实时对战,200ms 延迟竟然也能丝滑流畅? - 掘金[7]
  • 如何做一款网络联机的游戏? - 知乎[8]

参考资料

[1]极度简陋的聊天室 Demo (React + node): https://github.com/SmaIIstars/react-demo/tree/master/src/pages/socket/chat-room

[2]同步策略主要实现: https://github.com/SmaIIstars/react-demo/tree/master/src/pages/socket/game-demo

[3]大佬King: https://juejin.cn/user/3272618092799501

[4]他的动图: https://juejin.cn/post/7041560950897377293

[5]动图里面的效果: https://github.com/SmaIIstars/react-demo/tree/master/src/pages/socket/game-demo

[6]如何设计大型游戏服务器架构?-今日头条: https://www.toutiao.com/article/6768682173030466051/

[7]2 天做了个多人实时对战,200ms 延迟竟然也能丝滑流畅? - 掘金: https://juejin.cn/post/7041560950897377293

[8]如何做一款网络联机的游戏? - 知乎: https://www.zhihu.com/question/275075420

  • END -

本文由哈喽比特于2年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/7NJjBG9AdFQtJ3BjqiSbXg

 相关推荐

刘强东夫妇:“移民美国”传言被驳斥

京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。

发布于:1年以前  |  808次阅读  |  详细内容 »

博主曝三大运营商,将集体采购百万台华为Mate60系列

日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。

发布于:1年以前  |  770次阅读  |  详细内容 »

ASML CEO警告:出口管制不是可行做法,不要“逼迫中国大陆创新”

据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。

发布于:1年以前  |  756次阅读  |  详细内容 »

抖音中长视频App青桃更名抖音精选,字节再发力对抗B站

今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。

发布于:1年以前  |  648次阅读  |  详细内容 »

威马CDO:中国每百户家庭仅17户有车

日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。

发布于:1年以前  |  589次阅读  |  详细内容 »

研究发现维生素 C 等抗氧化剂会刺激癌症生长和转移

近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。

发布于:1年以前  |  449次阅读  |  详细内容 »

苹果据称正引入3D打印技术,用以生产智能手表的钢质底盘

据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。

发布于:1年以前  |  446次阅读  |  详细内容 »

千万级抖音网红秀才账号被封禁

9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...

发布于:1年以前  |  445次阅读  |  详细内容 »

亚马逊股东起诉公司和贝索斯,称其在购买卫星发射服务时忽视了 SpaceX

9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。

发布于:1年以前  |  444次阅读  |  详细内容 »

苹果上线AppsbyApple网站,以推广自家应用程序

据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。

发布于:1年以前  |  442次阅读  |  详细内容 »

特斯拉美国降价引发投资者不满:“这是短期麻醉剂”

特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。

发布于:1年以前  |  441次阅读  |  详细内容 »

光刻机巨头阿斯麦:拿到许可,继续对华出口

据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。

发布于:1年以前  |  437次阅读  |  详细内容 »

马斯克与库克首次隔空合作:为苹果提供卫星服务

近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。

发布于:1年以前  |  430次阅读  |  详细内容 »

𝕏(推特)调整隐私政策,可拿用户发布的信息训练 AI 模型

据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。

发布于:1年以前  |  428次阅读  |  详细内容 »

荣耀CEO谈华为手机回归:替老同事们高兴,对行业也是好事

9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。

发布于:1年以前  |  423次阅读  |  详细内容 »

AI操控无人机能力超越人类冠军

《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。

发布于:1年以前  |  423次阅读  |  详细内容 »

AI生成的蘑菇科普书存在可致命错误

近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。

发布于:1年以前  |  420次阅读  |  详细内容 »

社交媒体平台𝕏计划收集用户生物识别数据与工作教育经历

社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”

发布于:1年以前  |  411次阅读  |  详细内容 »

国产扫地机器人热销欧洲,国产割草机器人抢占欧洲草坪

2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。

发布于:1年以前  |  406次阅读  |  详细内容 »

罗永浩吐槽iPhone15和14不会有区别,除了序列号变了

罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。

发布于:1年以前  |  398次阅读  |  详细内容 »
 相关文章
Android插件化方案 5年以前  |  237236次阅读
vscode超好用的代码书签插件Bookmarks 2年以前  |  8071次阅读
 目录