经典复刻|Cocos Creator 游戏 AI 编程实战

作者介绍:“忙着赚钱”,游戏行业10年后端开发经验,目前在研究使用前端引擎开发游戏。期间已用 Cocos 引擎实现了一款三消类小游戏,并上架微信、抖音平台。

二十年前,Mat Buckland 先生创作了《游戏人工智能编程案例精粹》这本经典。二十多年来,它依然是全球开发者学习游戏 AI 的必读之作。

它不是空洞的理论,而是能让你亲手打造一个会“自己思考”的智能体的实战指南。

原书呈现了大量 AI 技术方案,包括状态机、操控行为、空间划分、图的各种算法、触发器、路径规划、目标驱动、模糊逻辑等等。

Book

笔者就是在学习本书的过程中,通过 Cocos Creator 实现了书中的案例,可以查看视频

下面会通过几个例子,来为大家呈现原书的魅力。

操控行为

原书第3章,作者通过对操控行为的运用,来实现可以自治的智能体。

所谓操控行为,就是操控智能体,让它来完成指定的行为,这些行为有很多种。

gif2

例如:靠近指定的目标、远离逃避目标、追逐目标、漫无目的的徘徊、避开墙壁和障碍物、利用障碍物来躲避目标、跟随指定的路径运动等。

当然还有看起来就让人兴奋的集群行为。

多种行为之间可以进行组合,从而实现多种自治的效果。

按照笔者的理解,操控行为本质上是利用了初中物理的几个基础公式:

  • 加速度计算公式:a = F / M
  • 速度与加速度之间的公式:v = v0 + a * t

每种类型的操控行为,都会为智能体身上施加一个作用力 F。

靠近目标的话,就需要一个指向目标的作用力,来拉着智能体朝目标的方向移动,远离目标的话,则正好相反。

而所谓的作用力 F,在二维、三维坐标系里,就是带方向和大小的向量,Cocos Creator 里面的 Vec2Vec3 正是为此而设计的。

施加在智能体身上多个操控行为,就会产生多个作用力。通过向量加法,我们就可以计算出最终的合力 F。

带入上面提到的两个公式后,我们可以计算出a。

通过 a,结合当前经过的时间 deltaTime,我们可以计算出当前帧最新的速度 v

有了当前速度,之后就好办了,利用公式“X = X0 + v * t”就可以计算出智能体最新的位置。瞧,操控行为就是这样。

当然操控行为真正实现起来,远比上面说的要复杂,书中的核心就是如何计算这些“力”。

原作者用 C++ 实现,而本项目则用 TS 语言,结合 Cocos 引擎,移植了这些操控行为的核心逻辑。

这里定义了操控行为的枚举类型,定义一个操控行为类,包含开启、关闭各行为的接口:

// 控制行为种类
enum RavenBehaviorType {
    none               = 0,
    seek               = 1 << 1, //靠近
    flee               = 1 << 2, //离开
    arrive             = 1 << 3, //抵达
    wander             = 1 << 4, //徘徊
    cohesion           = 1 << 5, //聚集
    separation         = 1 << 6, //分离
    ...
};

@ccclass('RavenSteering')
export class RavenSteering {

    // 是否开启指定类型的控制行为
    private on(bType: RavenBehaviorType): boolean {
        return (this._flags & bType) == bType;
    }
    // 开启指定类型的控制行为
    private onType(bType: RavenBehaviorType) {
        this._flags |= bType;
    }
    // 关闭指定类型的控制行为
    private offType(bType: RavenBehaviorType) {
        if (this.on(bType)) {
            this._flags ^= bType;
        }
    }

    // 开启靠近行为
    seekOn() {
        this.onType(RavenBehaviorType.seek);
    }
    // 开启徘徊行为
    wanderOn() {
        this.onType(RavenBehaviorType.wander);
    }

    ...

    // 关闭靠近行为
    seekOff() {
        this.offType(RavenBehaviorType.seek);
    }
    // 关闭徘徊行为
    wanderOff() {
        this.offType(RavenBehaviorType.wander);
    }

    ...

    // 靠近行为是否开启
    seekIsOn(): boolean {
        return this.on(RavenBehaviorType.seek);
    }
    // 徘徊行为是否开启
    wanderIsOn(): boolean {
        return this.on(RavenBehaviorType.wander);
    }

    ...

}

下面实现了靠近、徘徊操控行为作用力的计算:

    /**
     * 实现seek(前往)行为
     */
    private seekBH(target: Vec3): Vec3 {
        let desireVelocity = new Vec3();
        Vec3.subtract(desireVelocity, target, this._ravenBot.pos);
        desireVelocity.normalize();
        desireVelocity.multiplyScalar(this._ravenBot.maxSpeed);
        desireVelocity.subtract(this._ravenBot.velocity);
        return desireVelocity;
    }

    // 实现wander(徘徊)行为
    private wanderBH(): Vec3 {
        // 更新目标位置
        this._wanderTarget.add3f(Util.getRandomFloat(-1, 1) * RavenCons.Steering_Wander_Jitter,
                                Util.getRandomFloat(-1, 1) * RavenCons.Steering_Wander_Jitter, 0);
        this._wanderTarget.normalize();
        this._wanderTarget.multiplyScalar(RavenCons.Steering_Wander_Rad);
        let realTarget = new Vec3(this._wanderTarget);
        realTarget.add3f(RavenCons.Steering_Wander_Dist, 0, 0);

        // 转化为目标对象bot所处的空间
        realTarget = GeometryHandyFuncs.pointToWorldSpace(realTarget, this._ravenBot.heading,
                        this._ravenBot.side, this._ravenBot.pos);
        realTarget.subtract(this._ravenBot.pos);
        return realTarget;
    }

    ...

下面是计算作用力合力的接口:

    // 尝试将力加到总转向力上,最大不超过maxforce,返回加成功与否
    private accumulateForce(totalForceVec: Vec3, forceAddVec: Vec3): boolean {
        let remainForce = this._ravenBot.maxForce - totalForceVec.length();
        if (remainForce < math.EPSILON) return false;

        let wantAddForce = forceAddVec.length();
        if (wantAddForce < remainForce) {
            totalForceVec.add(forceAddVec);
        } else {
            let canAddVec = new Vec3();
            Vec3.normalize(canAddVec, forceAddVec);
            canAddVec.multiplyScalar(remainForce);
            totalForceVec.add(canAddVec);
        }
        return true;
    }

    /**
     * 计算各操控力合并到一起的效果,受到maxforce限制
     */
    private sumForces(): Vec3 {
        // 分操控类型计算合力
        let totalForce = new Vec3();
        let force:Vec3 = null;

        if (this.on(RavenBehaviorType.seek)) {
            force = this.seekBH(this._target);
            force.multiplyScalar(RavenCons.Steering_Seek_Weight);
            if (!this.accumulateForce(totalForce, force)) {
                return totalForce;
            }
        }

        if (this.on(RavenBehaviorType.wander)) {
            force = this.wanderBH();
            force.multiplyScalar(RavenCons.Steering_Wander_Weight);
            if (!this.accumulateForce(totalForce, force)) {
                return totalForce;
            }
        }

        ...

        return totalForce;
    }

顺带一提,项目内大量使用了 Vec3 对象。

由于该对象每次使用都需要 new 出来,会出现频繁创建和销毁该对象内存的情况,影响运行效率。

本着使用简单的考量,项目内对接口内部要用到的临时 Vec3 变量,采用以下方式定义静态变量,接口内部只需要调用相应静态变量使用即可。

private static _???Temp: Vec3 = new Vec3();

或者

private static _???Vec: Vec3 = new Vec3() 

这样避免了多次创建销毁,也免去使用对象池等技术,简化代码书写。

需要注意的是,这种用法尽量用于接口内部的临时变量,对于需要作为返回值使用的 Vec3,还是需要 new 出来才安全

    /**
     * 调整bot转向来转到指定位置方向,如果转向成功则返回真。该方法受到bot的转向速率限制
     */
    private static _rotateFacingTemp: Vec3 = new Vec3();
    rotateFacingTowardPos(target: Vec3): boolean {
        // 计算目标位置,与bot当前朝向的夹角
        Vec3.subtract(RavenBot._rotateFacingTemp, target, this.pos);
        RavenBot._rotateFacingTemp.normalize();
        let dot = this._facing.dot(RavenBot._rotateFacingTemp);
        if (dot < -1) dot = -1;
        if (dot > 1) dot = 1;
        let angle = Math.acos(dot);

        // 角度很小时,直接设置转向
        if (angle <= 0.01) {
            this._facing.set(RavenBot._rotateFacingTemp);
            return true;
        }

        ...
    }

路径规划

原书第8章讲述了路径规划技术,路径规划的核心是图搜索算法

游戏中常用的图搜索算法有2种:Dijkstra 搜索算法和A星寻路算法。

根据不同的情况,这两种算法在项目内都有被用到:

1. A星寻路算法

当智能体需要攻击敌人时,起点和目标点都很明确,可以使用A星寻路算法。

该算法通过选择合适的启发函数,能够快速找到一条相对最优的路径。

2. Dijkstra 搜索算法

当智能体需要找到最近的道具时,由于地图上可能有很多道具,目标点不确定,此时更适合使用 Dijkstra 算法。

该算法会从起点开始遍历,直到找到离智能体最近的道具,从而规划出一条可行的路径。

以上两张图中,蓝色粗线是最终找到的路径,红色细线是算法搜索过的所有节点。

可以看出,A星算法在寻路时的效率更高。

但即使获得了路径节点,智能体也可能沿着曲折的路径前进,看起来不够智能。

为了解决这个问题,原书提供了路径平滑处理方案。

平滑处理的方案有两种实现:一种快速但效果粗糙,另一种精确但效率较低。

大家可以根据项目需求,选择最合适的方案,让寻路路径更加自然流畅。

/**
 * Bot使用的路径规划类
 */
@ccclass('RavenPathPlanner')
export class RavenPathPlanner {
    /**
     * 快速去除多余边来优化路径
     */
    private smoothPathEdgesQuick(path: Array<PathEdge>) {
        let e1 = 0;
        let e2 = 1;
        while (e2 < path.length) {
            if (path[e2].behavior == NavGraphEdgeFlagType.normal &&
                this._owner.canWalkBetween(path[e1].source, path[e2].destination))
            {
                // 如果e1的起点到e2的终点,之间可以穿行,将e1的终点设置为e2的终点,则并移除e2这条边
                path[e1].destination = path[e2].destination;
                path.splice(e2, 1);
            } else {
                // e1到当前e2之间已不可穿行,则切换e1到e2的位置,e2定位到下一条边,继续路径优化
                e1 = e2;
                ++e2;
            }
        }
    }

    /**
     * 精确去除多余边来优化路径
     */
    private smoothPathEdgesPrecise(path: Array<PathEdge>) {
        let e1 = 0;
        let e2 = 0;
        while (e1 < path.length) {
            e2 = e1 + 1; // 循环开始,e2指向e1的下一条边

            while (e2 < path.length) {
                if (path[e2].behavior == NavGraphEdgeFlagType.normal &&
                    this._owner.canWalkBetween(path[e1].source, path[e2].destination))
                {
                    // 终于找到一个可以通行的e2终点,此时将e1的终点设置为e2的终点。
                    // 并且将,e1的下一条边到e2这条边之间的,所有边的移除,他们都用不到了
                    path[e1].destination = path[e2].destination;
                    let removeStart = e1 + 1;
                    let removeCount = e2 - removeStart + 1;
                    path.splice(removeStart, removeCount);

                    // 重新设置e2的位置,e1保持不变,接续前进
                    e2 = e1 + 1;
                } else {
                    // e1到当前e2之间已不可穿行, 则仍不罢休,继续向着最后的边前进,直到所有的边都校验完毕
                    ++e2;
                }
            }

            // 以当前e1为起点的边已校验完毕,开启下一条边的校验
            ++e1;
        }
    }

    ...
}

在只有几个智能体的游戏里,无论采用 A 星还是 Dijkstra 的路径规划请求,所产生的运算消耗都是在可承受范围内。

但对于像拥有成百上千个智能体的 RTS 游戏,当需要频繁寻路时,性能会成为灾难。

为了解决这个问题,原书提供了时间片路径规划的解决方案。

它的核心思想是:

不一次性完成所有计算,而是将每个寻路请求拆分成多个小任务,按顺序分批处理。每次只执行一小部分计算,直到所有请求完成。

这能有效控制每帧的计算开销,避免游戏卡顿。

通过代码说明如下:

/**
 * 统一调配所有路径规划实例
 */
@ccclass('PathManager')
export class PathManager {
    private _searchRequests: Array<RavenPathPlanner> = []; // 所有请求搜索计划的列表
    private _searchTimePerUpdate: number = 0; // 每次更新搜索时,最多占用的时间 ms

    constructor(timeRatio: number) {
        // 路径规划耗时占当前帧率时间的比例
        if (timeRatio < 0.1) timeRatio = 0.1;
        if (timeRatio > 0.5) timeRatio = 0.5;

        let costTime = 1000 / (game.frameRate as number) * timeRatio; // 毫秒
        this._searchTimePerUpdate  = Math.max(1, costTime); // 至少1毫秒
        console.log(`PathManager searchTimePerUpdate ${costTime} ms`);
    }

    /**
     * 更新当前搜索路径列表
     */
    updateSearchs() {
        let startTime = Date.now();
        let searchIndex = 0;
        let checkCount = 0; //累计检测的数量,不需要每次循环都检测时间,这允许一定的时间误差
        
        while (this._searchRequests.length > 0) {
            // 对于每个搜索请求,通过cycleOnce接口,可以保证搜索循环只执行1次
            let result = this._searchRequests[searchIndex].cycleOnce();

            // 清理已经搜索结束的规划
            if (result == TimeSlicedSearchState.targetFound ||
                result == TimeSlicedSearchState.targetNotFound) 
            {
                this._searchRequests.splice(searchIndex, 1);
            } else {
                ++searchIndex;
            }

            // 循环搜索
            if (searchIndex >= this._searchRequests.length) {
                searchIndex = 0;
            }

            // 判断是否到最大搜索时间
            ++checkCount;
            if (checkCount == 10) { // 循环每执行10次,进行一次超时判定
                checkCount = 0;
                if (Date.now() - startTime >= this._searchTimePerUpdate) {
                    break;
                }
            }
        }
    }

    ...
}

/**
 * 时间片路径搜索的状态枚举
 */
export enum TimeSlicedSearchState {
    searIncomplete, // 搜索尚未完成
    targetFound, // 已经发现目标
    targetNotFound, // 未发现目标
}

/**
 * Bot使用的路径规划类
 */
@ccclass('RavenPathPlanner')
export class RavenPathPlanner {
    private _owner: RavenBot = null; // 实例所属Bot
    private _graph: RavenSparseGraph = null; // 导航图
    private _curSearch: GraphSearchTimeSliced<RavenGraphEdge> = null; // 对应搜索算法实例
    private _destPos: Vec3 = new Vec3(); // 目标点位置

    /**
     * 走一遍查找,根据查找结果,触发不同的消息
     */
    cycleOnce(): TimeSlicedSearchState {
        assert(!!this._curSearch, '<RavenPathPlanner.cycleOnce> No search object instantiated');

        let result = this._curSearch.cycleOnce();
        if (result == TimeSlicedSearchState.targetNotFound) {
            // 通知查找失败
            MessageDispatcher.instance().dispatchMsg(0, -1, this._owner.id,
                    RavenMsgType.msgNoPathAvailable);
        } else if (result == TimeSlicedSearchState.targetFound) {
            // 通知查找成功
            let trigger: Trigger<RavenBot> = null;
            if (this._curSearch.targetIdx() != invalidNodeIndex) {
                trigger = this._graph.getNode(this._curSearch.targetIdx())?.extraInfo;
            }
            MessageDispatcher.instance().dispatchMsg(0, -1, this._owner.id, 
                    RavenMsgType.msgPathReady, trigger);
        }

        return result;
    }

    ...
}

该项目的地图创建方法也值得一提。

由于原书没有提供地图生成工具,笔者自制了一个简易的地图生成结构,可以快速构建地图数据。

原书作者提到可以通过洪水填充算法,来动态创建导航图数据。

笔者根据自己的理解,在本项目中实现了一版,其核心思路是广度优先遍历,让地图生成更加灵活。

代码如下:

/**
 * 洪水填充算法,构建导航图数据
 */
public static createGridByFloodFill<T, NodeType extends NavGraphNode<T>, EdgeType extends GraphEdge>
    (graph: SparseGraph<NodeType, EdgeType>, firstNodePos: Vec3, edgeLen: number,
    extraLen: number, wallPrefArr: Array<WallPref>)
{
    let catchPos2NodeMap = new Map<string, NodeType>(); //已创建的图节点key缓存
    let waitHandlePosQueue = new StackQueue<Vec3>(); // 待处理图节点队列

    // 第一个节点先入队列
    [firstNodePos.x, firstNodePos.y] = [Math.floor(firstNodePos.x), Math.floor(firstNodePos.y)];
    let newNode = graph.nodeFactory(graph.nextFreeNodeIndex, {pos: firstNodePos});
    graph.addNode(newNode as NodeType); // 图对象新增一个节点
    let cposKey = `${firstNodePos.x}_${firstNodePos.y}`;
    catchPos2NodeMap.set(cposKey, newNode as NodeType);
    waitHandlePosQueue.push(firstNodePos.clone());

    // 依次处理队列中的节点信息
    while (!waitHandlePosQueue.empty()) {
        // 将队首节点可连接的节点,添加进图
        let handlePos = waitHandlePosQueue.pop();
        // 取得跟指定节点相连接的8个边节点的坐标数据
        let connectPosList = this.getConnectEdgeNodePosList(handlePos, edgeLen);
        for (let cpos of connectPosList) {
            // 排除跟墙壁相交的边
            if (this.checkEdgeCrossWall(handlePos, cpos, extraLen, wallPrefArr)) continue;

            // 添加节点
            cposKey = `${cpos.x}_${cpos.y}`;
            let isNewPos = !catchPos2NodeMap.has(cposKey);
            if (isNewPos) {
                newNode = graph.nodeFactory(graph.nextFreeNodeIndex, {pos: cpos});
                graph.addNode(newNode as NodeType); // 图对象新增一个节点
                catchPos2NodeMap.set(cposKey, newNode as NodeType);
                waitHandlePosQueue.push(cpos.clone());
            }

            // 添加边
            let handleKey = `${handlePos.x}_${handlePos.y}`;
            let handleNode = catchPos2NodeMap.get(handleKey);
            let cposNode = catchPos2NodeMap.get(cposKey);
            if (!graph.isEdgePresent(handleNode.index, cposNode.index)) {
                let dist = Vec3.distance(handlePos, cpos);
                let newEdge = graph.edgeFactory(handleNode.index, cposNode.index, dist);
                graph.addEdge(newEdge as EdgeType); // 图对象新增一个边
            }
        }
    }
}

...

目标驱动

如果说寻路是AI的“脚”,那么目标驱动就是AI的“大脑”,它负责思考和决策。

对于目标驱动的技术,笔者拿上面的路径规划来举例。

大目标(如“前往指定位置”)可以分解为一系列可执行的子目标(如“跟随路径”)。

每个子目标再分解为更小的、可直接执行的动作(如“移动到下一个点”)。

通过这种层级分解,智能体能将复杂的任务转化为简单的步骤,最终利用像“靠近”这样的操控行为来完成每个小任务。

这正是目标驱动的精髓所在。

原书详细讲解了每种类型目标驱动的实现解析与实现逻辑,下面简单列出2类目标类型的实现代码。

/**
 * 前往指定的位置
 */
@ccclass('GoalMoveToPosition')
export class GoalMoveToPosition extends GoalComposite<RavenBot> {
    private _targetPos: Vec3 = new Vec3();

    constructor(bot: RavenBot, targetPos: Vec3) {
        super(bot, RavenGoalType.goal_move_to_position);
        this._targetPos.set(targetPos);
    }

    /**
     * 激活目标处理
     */
    activate() {
        this._state = Goal_State.active;
        
        this.removeAllSubgoals();
        // 请求前往目标点的路径,在路径消息返回前,采用seek到目标位置的目标
        if (this._owner.pathPlanner.requestPathToPos(this._targetPos)) {
            this.addSubgoal(new GoalSeekToPosition(this._owner, this._targetPos));
        }
    }

    /**
     * 更新目标处理
     */
    process(): Goal_State {
        this.activateIfInactive();

        this._state = this.processSubgoals();

        // 如果未能成功到达目标点,则重新规划
        this.reactivateIfFailed();
        return this._state;
    }

    /**
     * 终止目标处理
     */
    terminate() {
        super.terminate();
    }

    /**
     * 处理消息
     */
    handleMessage(msg: Telegram): boolean {
        let isHandled = this.forwardMsgToSubgoal(msg);
        if (isHandled) {
            return true;
        }

        switch (msg.msg) {
            case RavenMsgType.msgPathReady:
                {
                    this.removeAllSubgoals();
                    this.addSubgoal(new GoalFollowPath(this._owner, this._owner.pathPlanner.getPath()));
                    return true;
                }
                break;

            case RavenMsgType.msgNoPathAvailable:
                {
                    this._state = Goal_State.failed;
                    return true;
                }
                break;

            default:
                return false;
        }
        return false;
    }

}
/**
 * 沿指定路径边移动
 */
@ccclass('GoalTraverseEdge')
export class GoalTraverseEdge extends Goal<RavenBot> {
    private _edge: PathEdge = null; // 当前边

    constructor(bot: RavenBot, edge: PathEdge, lastEdge: boolean) {
        super(bot, RavenGoalType.goal_traverse_edge);
        this._edge = edge;
    }

    /**
     * 激活目标处理
     */
    activate() {
        this._state = Goal_State.active;

        // 设置操控行为
        this._owner.steering.target = this._edge.destination;
        this._owner.steering.seekOn();
    }

    /**
     * 更新目标处理
     */
    process(): Goal_State {
        this.activateIfInactive();

        if (this._owner.isAtPosition(this._edge.destination)) {
            this._state = Goal_State.completed;
        }
        return this._state;
    }

    /**
     * 终止目标处理
     */
    terminate() {
        this._owner.steering.seekOff();
    }
}

上面提到,智能体可以采用的大方向目标,有捡取回血道具、捡取武器、攻击其他智能体、前往地图中的指定位置等多种。

那么他又是怎么确定当前最适合自己的大目标是什么呢?

目标仲裁实现了这一选择方案。

通过获取智能体的信息(当前的健康度、距离最近的回血道具或者武器的距离、当前装备的强弱等),目标仲裁系统会给上述每个大方向目标进行打分。

得分最高的目标,就是当下采取的最优目标。

/**
 * 顶层目标
 */
@ccclass('GoalThink')
export class GoalThink extends GoalComposite<RavenBot> {
    private _evaluators: Array<GoalEvaluator<RavenBot>> = []; // 所有评估目标的列表

    /**
     * 评估后确定当前的目标
     */
    arbitrate() {
        let bestDesirability = 0; // 得分最高的目标分值
        let bestEvaluator: GoalEvaluator<RavenBot> = null; // 得分最高的目标对象

        for (let evaluator of this._evaluators) {
            let desirability = evaluator.calcDesirability(this._owner);
            if (desirability > bestDesirability) {
                bestDesirability = desirability;
                bestEvaluator = evaluator;
            }
        }

        bestEvaluator.setGoal(this._owner);
    }

    ...
}

目标驱动的应用远不止于此。你可以通过添加个性因子,让智能体变得胆小或勇敢,赋予其独特的行为倾向。

它还可以用来存储状态,按顺序执行多层子目标,实现命令排队,甚至用于编写脚本来控制 NPC、相机和动画。

总之,目标驱动技术能为你处理游戏逻辑带来全新的思路。一旦掌握,你可能也会像笔者一样,爱不释手。

写在最后

项目中还包括原书中讲解的其他技术,像状态机、触发器、图搜索算法、模糊逻辑等等。

对于上文未提到的技术,还请大家到原书中阅读。

希望通过该项目的辅助,大家可以更快更好的在 Cocos Creator 中,实现自己需要的技术。

即拿即用

原书中所有的 C++ 案例,本项目都用 TS 语言在 Cocos Creator 3.8.6 中完整实现。

希望能通过这种方式,让大家理解起来可以轻松一些。

性能考虑

原书的示例没有考虑帧率对物理计算的影响,这在真实项目中是行不通的。

本项目的所有物理计算都考虑了 deltaTime 参数,确保游戏在不同设备上运行时的表现一致。

游戏 AI 的本质

好的技术就像养料,它会变成巨人的肩膀,新技术的基石。学习这些经典技术,能为你打开处理游戏逻辑的新思路。

最后,引用原作者 Mat Buckland 先生的一句话作为结束:

“一个极其聪明,几乎无可匹敌的对手,对游戏人工智能程序员来说,这是罕见的目标。一个好的人工智能应具有一个目的:

就是让玩游戏变得比较有趣。

经常提醒自己这一点是明智的,请相信我,这一点很容易错过并能使你陷入困境。

因为你试图制造人类已知的最聪明的游戏智能体,而不是试图制造哪种能使玩家大笑,并且开心雀跃的游戏智能体。”

项目源码可以点击这里获取。

感谢阅读,希望这篇文章能给大家带来帮助!

9赞

不错哦,我也买一本压泡面

支持玉兔,玉兔出品必是精品

作者不是我 :rofl:,见文章开头的介绍

玉兔美少女都推荐了,我也买一本压泡面

十分感谢官方的推荐,真心希望可以对大家有所帮助 :grin:

mark!

666,太高端了

通过百度网盘分享的文件:[游戏人工智能编…pdf
链接:https://pan.baidu.com/s/1ctEW6VBNUdJN2HP_ebIReA
提取码:75T3

1赞