【Cocos 3d】从零开始自制3d出租车小游戏

这篇具有很好参考价值的文章主要介绍了【Cocos 3d】从零开始自制3d出租车小游戏。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

引言

本文很长,建议收藏食用。

课程来源:
游戏开发教程 | 零基础也可以用18堂课自制一款3D小游戏 | Cocos Creator 3D 中文教程(合集)p1~p6

简介:
资源下载:https://github.com/cocos-creator/tutorial-taxi-game
适合学习人群:本教程假定你对编程有一定的了解,ts,js 学习过其中之一。
如果不曾了解过,可以参考 js 教程(https://developer.mozilla.org/zh-CN/docs/Web/JavaScript)或者 ts 教程(https://www.tslang.cn/)
下载引擎:http://www.cocos.com/
关注官方微信公众号(CocosEngine),不定期推送福利活动、新手教程还有好玩的活动噢!

本文作者的bb:
  项目已上传至:https://gitcode.net/qq_36286039/dusttaxi
  引擎版本为Cocos 3d 1.2.0
  本文毕竟是以文字的形式讲解视频的内容,因此部分重复的步骤(新建节点、设置对应的位置、属性等重复且类似的工作)不会在文中过多赘述,也不会逐一截图。
  此外,本人对Cocos Creator 2.x版本已经掌握,可以制作2d小游戏,所以也并不完全是“零基础”。完全没用过Cocos 引擎肯定是不行的,本文也不会介绍@property是什么意思、脚本、组件是什么之类的知识点。Cocos初学者请移步Cocos官网去看文档启蒙!
  其他如遇看不懂的地方,可以私信本人补充,但仍然建议移步上方链接去看看视频,视频一共才9小时,大家一起加油!
  本文只讲述从下载引擎,新建项目,到让小车运动、乘客运动内容。更多特效、音效、碰撞、界面部分不在本文展示。以下内容已经完全可以满足新手入门的需求!

下载引擎

  • 打开CocosDashboard,下载一个cocos 3d 1.2.0引擎。虽然显示废弃,但是视频教程用的是这个,而且根据官网的描述,3d 1.2.0的项目可以直接移植到Cocos Creator 3.x版本上。因此为了学习方便,下一个即将废弃的3d 1.2.0吧。

  • 下好后是这样的:
    【Cocos 3d】从零开始自制3d出租车小游戏

  • 下载时要往下滑,滑到最底下找到废弃的版本。
    【Cocos 3d】从零开始自制3d出租车小游戏

新建项目

  • 在右下角新建项目
    【Cocos 3d】从零开始自制3d出租车小游戏

  • 记得修改项目名字,叫Taxi或者什么,随意。
    【Cocos 3d】从零开始自制3d出租车小游戏

  • 新建完成后,我们就得到了一个空的项目

导入资源

  1. 上这个网站把资源下载下来。
    资源下载:https://github.com/cocos-creator/tutorial-taxi-game
  2. 找到这个文件夹:
    【Cocos 3d】从零开始自制3d出租车小游戏
  3. 打开,把里面的东西全部拖到引擎内的资源管理器下的asserts文件夹下。
    【Cocos 3d】从零开始自制3d出租车小游戏
  4. 等待资源导入,观察控制台,无报错就算成功。

搭建场景

1. 观察资源

  • 由于是3d游戏,所以我们用的都是模型资源。打开model文件夹,找到下面的road文件夹,打开,观察一下这些资源里有几种路面。【Cocos 3d】从零开始自制3d出租车小游戏
  • 观察sign文件夹中,可以看到一些标识资源。在cars文件夹中,可以看到我们的车模型。

2. 查看草图

策划给我们画的草图如下:
【Cocos 3d】从零开始自制3d出租车小游戏
其中从start到end一共有4个建筑物,分别对应的是接客、送客、接客、送客操作。

3. 搭建路面

  • 新建一个地图节点(空节点),并添加路面管理节点(空节点),并将路面资源拖入路面管理节点。
    【Cocos 3d】从零开始自制3d出租车小游戏
  • 放入后如下所示
    【Cocos 3d】从零开始自制3d出租车小游戏

4. 搭建路线

  • 如图所示,路线只需要直线路面和十字路口即可完成搭建。
    【Cocos 3d】从零开始自制3d出租车小游戏
  • 在添加直线路面后,通过修改scale属性里的z值来修改长度。图中3段直线路面的z值分别是15、10、30.
    【Cocos 3d】从零开始自制3d出租车小游戏

windows系统下,ctrl+d复制当前节点。

  • 给十字路口部分补充上一点道路,没有细调。
  • (我做的时候:能看就行。)
    【Cocos 3d】从零开始自制3d出租车小游戏

5. 放置标记

  • 在map下新建空节点sign,用于放置标记。
  • 放置start标记,如下所示:
    【Cocos 3d】从零开始自制3d出租车小游戏
  • 同理放置end标记,移动到终点位置
  • 放置两个向前引导的标记
    【Cocos 3d】从零开始自制3d出租车小游戏
  • 到这里整个的路线已经出来了。

6. 放置建筑物

  • 定义黄色的建筑物是接客建筑物,蓝灰色建筑物为送客建筑物。
  • 新建building节点用于管理建筑物。
  • 放置完成大致为如下样式,不必细调位置。
    【Cocos 3d】从零开始自制3d出租车小游戏

7. 放置装饰物

  • 在map下新建decoration节点用于管理装饰物。
  • 按自己喜好放置就好。
  • 我因为懒,一样随便放了一点。
    【Cocos 3d】从零开始自制3d出租车小游戏

保存地图

  • 因为我们的地图是要复用的,因此我们将它存为预制。

  • 在asserts文件夹下新建一个prefab文件夹,再在prefab下建一个map文件夹,将层级管理器里的map101直接拖到这个文件夹里,保存这个预制。
    【Cocos 3d】从零开始自制3d出租车小游戏

  • 至此场景已经搭建完了,和2d里的没有太大区别。

录入路径

  • 至此,之前的一切都是我们肉眼可见的范围,比如我们知道在哪接客,在哪送客。但是程序是不知道的,因此我们需要通过程序来处理接送客人的逻辑。

  • 因此我们需要一个路径,来让程序得以控制它。

  • 在map101节点下新建空节点path,并在path下新建空节点path01,这就是给我们当前的玩家小车来定制的路线。

  • 同时,我们将小车模型资源放上去。
    【Cocos 3d】从零开始自制3d出租车小游戏

  • 保存玩家可用的小车为预制。(将taxi01改名为car101)
    【Cocos 3d】从零开始自制3d出租车小游戏

  • 把玩家可用的小车都做成这样的预制,并保存。

  • 我们现在要控制小车从一个点到下一个点,并且根据路面来看,是有曲线的。因此我们需要根据道路来定制路线。

  • 以下罗列了这些点应该有的结构。

  • 框中的意思为:这些点分为开始点、普通点、接客点、送客点、结束点。

  • 右侧的意思为:除了终点以外,对于每个点都有一个“下一站”。并且,我们移动的类型也有两种,有直线行驶,也有拐弯行驶。

  • 左侧的意思为:每条线的障碍物小车的产生频率、产生的延迟时间、车速、车的种类等。
    【Cocos 3d】从零开始自制3d出租车小游戏

  • 新建一个脚本,用枚举的方式来定义这些点的属性和路面样式。
    【Cocos 3d】从零开始自制3d出租车小游戏

import { _decorator, Component, Node } from 'cc';
const { ccclass, property } = _decorator;


/** 路上的点的种类*/
/** 路上的点的种类*/
enum ROAD_POINT_TYPE {
    NORMAL = 1,
    START,
    GREETING,
    GOODBYE,
    END,
    AISTART,//AISTART点用于区分小车行为,用于控制AI小车
}

//将enum序列化识别,cocos 3d专属,否则无法在引擎的组件里显示
Enum(ROAD_POINT_TYPE)

/**路的种类 */
enum ROAD_MOVE_TYPE {
    LINE = 1,
    CURVE,
}

Enum(ROAD_MOVE_TYPE)
  • 加上一些属性的定义
  • 用displayOrder控制在属性检查器中显示的顺序。但我是顺的,所以我没有设置。
  • 通过visible控制是否在属性检查器中显示,显示的条件。
    @property({
        type: ROAD_POINT_TYPE,
        displayOrder: 1//如果在引擎里属性检查器里这些property乱序了,用这个来排序。
    })
    type = ROAD_POINT_TYPE.NORMAL;

    @property({
        type: Node,
        /**不是end类型的点就不显示这个属性 */
        visible: function (this: RoadPoint) {
            return this.type != ROAD_POINT_TYPE.END
        }
    })
    nextStation: Node = null;

    @property({ type: ROAD_MOVE_TYPE })
    moveType = ROAD_MOVE_TYPE.LINE;

    @property({
        visible: function (this: RoadPoint) {
            return this.type != ROAD_POINT_TYPE.END && this.moveType === ROAD_MOVE_TYPE.CURVE
        }
    })
    clockwise = true;//默认顺时针

    @property({
        type: Vec3,
        visible: function (this: RoadPoint) {
            return this.type === ROAD_POINT_TYPE.GREETING || this.type === ROAD_POINT_TYPE.GOODBYE
        }
    })
    direction = new Vec3(1, 0, 0);//接送客的方向,默认右边

    @property({
        visible: function (this: RoadPoint) {
            return this.type === ROAD_POINT_TYPE.AISTART
        }
    })
    interval = 3;//AI产出间隔

    @property({
        visible: function (this: RoadPoint) {
            return this.type === ROAD_POINT_TYPE.AISTART
        }
    })
    delayTime = 0;

    @property({
        visible: function (this: RoadPoint) {
            return this.type === ROAD_POINT_TYPE.AISTART
        }
    })
    speed = 0.05;//小车速度

    @property({
        visible: function (this: RoadPoint) {
            return this.type === ROAD_POINT_TYPE.AISTART
        }
    })
    /**如果要产出不同的车,用逗号分开。如"201,202" */
    cars = '201';//当前路径产出的小车的类型
  • 将这个脚本挂载在point节点上。
    【Cocos 3d】从零开始自制3d出租车小游戏
  • 删掉point上的car,我们的路线上并不需要这个。
  • 将point复制几份,分别放在路上不同的位置。
  • 其中包含一个start点-接客点-送客点-接客点-送客点-end点
  • 在组件中配置每个点的信息,一第二个点(第一个接客点)为例:
    【Cocos 3d】从零开始自制3d出租车小游戏
  • RoadPoint完整脚本
import { _decorator, Component, Node, Vec3, Enum } from 'cc';
const { ccclass, property } = _decorator;


/** 路上的点的种类*/
enum ROAD_POINT_TYPE {
    NORMAL = 1,
    START,
    GREETING,
    GOODBYE,
    END,
    AISTART,//AISTART点用于区分小车行为,用于控制AI小车
}

//将enum序列化识别,cocos 3d专属,否则无法在引擎的组件里显示
Enum(ROAD_POINT_TYPE)

/**路的种类 */
enum ROAD_MOVE_TYPE {
    LINE = 1,
    BEND,
}

Enum(ROAD_MOVE_TYPE)

@ccclass('RoadPoint')
export class RoadPoint extends Component {
    public static RoadPointType = ROAD_POINT_TYPE;
    public static RoadMoveType = ROAD_MOVE_TYPE;


    @property({
        type: ROAD_POINT_TYPE,
        displayOrder: 1//如果在引擎里属性检查器里这些property乱序了,用这个来排序。
    })
    type = ROAD_POINT_TYPE.NORMAL;

    @property({
        type: Node,
        /**不是end类型的点就不显示这个属性 */
        visible: function (this: RoadPoint) {
            return this.type != ROAD_POINT_TYPE.END
        }
    })
    nextStation: Node = null;

    @property({ type: ROAD_MOVE_TYPE })
    moveType = ROAD_MOVE_TYPE.LINE;

    @property({
        visible: function (this: RoadPoint) {
            return this.type != ROAD_POINT_TYPE.END && this.moveType === ROAD_MOVE_TYPE.BEND
        }
    })
    clockwise = true;//默认顺时针

    @property({
        type: Vec3,
        visible: function (this: RoadPoint) {
            return this.type === ROAD_POINT_TYPE.GREETING || this.type === ROAD_POINT_TYPE.GOODBYE
        }
    })
    direction = new Vec3(1, 0, 0);//接送客的方向,默认右边

    @property({
        visible: function (this: RoadPoint) {
            return this.type === ROAD_POINT_TYPE.AISTART
        }
    })
    interval = 3;//AI产出间隔

    @property({
        visible: function (this: RoadPoint) {
            return this.type === ROAD_POINT_TYPE.AISTART
        }
    })
    delayTime = 0;

    @property({
        visible: function (this: RoadPoint) {
            return this.type === ROAD_POINT_TYPE.AISTART
        }
    })
    speed = 0.05;//小车速度

    @property({
        visible: function (this: RoadPoint) {
            return this.type === ROAD_POINT_TYPE.AISTART
        }
    })
    /**如果要产出不同的车,用逗号分开。如"201,202" */
    cars = '201';//当前路径产出的小车的类型
}

  • 搭建完了玩家的小车路径,还要搭建几条AI小车的路径。这个先随意搭建两条就好,后面需要用上。
    在这里path02和path03都是AI小车的路径,随意摆放一下。注意复制的时候需要重新绑定一下节点喔~
    【Cocos 3d】从零开始自制3d出租车小游戏

游戏逻辑

1. 游戏逻辑梳理

根据游戏逻辑,可以整理出游戏中包含的几个大类:界面(UI)、音频资源、特效、车(玩家车、AI车)、乘客、关卡(地图)。

  1. 点击开始界面游戏开始,此时切换到游戏界面,并播放背景音乐
  2. 点按屏幕任意处小加速,松开刹车并根据当前车速播放刹车特效和刹车音效
  3. 到达指定地点接送乘客,播放乘客动画并更新相对应界面订单进度
  4. 第一次接乘客的时候,触发关卡内A小车运作
  5. 送乘客时播放金币奖励特效和音效
  6. 到达终点或者出车祸游戏结束,进入结算界面,出车祸不予以金币奖励
  7. 结算界面点击领取后更新自身拥有金币数量并回到主界面

对于游戏的几个大类,我们应当对其设置不同的管理类:
MapManager CarManager CustomerManager AudioManager EffectManager UIManager
这些管理部分,还要统一被一个流程控制的控制类来控制流程,来负责管理这几个类:GameCtrl
这些管理部分又可以衍生出自身要管理的部分,大致如下所示:
【Cocos 3d】从零开始自制3d出租车小游戏
在本游戏中,我们采用事件的方式播放音效和特效。

2. 编写部分GameCtrl脚本和CustomEventListener脚本

  • 新建一个关于游戏运行的文件夹game,并把之前的RoadPoint拖动到该文件夹下。
  • 新建GameCtrl、CarManager、MapManager的脚本。
  • 新建一个scenes文件夹,并新建一个场景:Game(下图中的GameScene不用理会)
    【Cocos 3d】从零开始自制3d出租车小游戏
  • 选中Game场景进行编辑
  • 新建Canvas节点
    【Cocos 3d】从零开始自制3d出租车小游戏【Cocos 3d】从零开始自制3d出租车小游戏
  • 将GameCtrl脚本挂在Canvas上
    【Cocos 3d】从零开始自制3d出租车小游戏

GameCtrl

  • 编写GameCtrl脚本,让它管理上CarManager和MapManager,并在属性检查器中绑定。
import { _decorator, Component, Node } from 'cc';
import { CarManager } from './CarManager';
import { MapManager } from './MapManager';
const { ccclass, property } = _decorator;

@ccclass('GameCtrl')
export class GameCtrl extends Component {
    @property({ type: MapManager })
    mapManager: MapManager = null;

    @property({ type: CarManager })
    carManager: CarManager = null;

}

【Cocos 3d】从零开始自制3d出租车小游戏

  • 在层级管理器中添加map101预制(之前做好的那个预制)
  • 新建GameMap脚本,并挂在map101上。
    【Cocos 3d】从零开始自制3d出租车小游戏
  • 在这里点击应用
    【Cocos 3d】从零开始自制3d出租车小游戏
  • 同理,新建Car脚本,挂在小车预制上,应用一下。
  • 新建消息监听器脚本
    【Cocos 3d】从零开始自制3d出租车小游戏

CustomEventListener

  • 编写监听器CustomEventListener脚本,其中包含事件监听、取消监听、派发三个方法。
import { _decorator, Component, Node } from 'cc';
const { ccclass, property } = _decorator;

interface IEventData {
    func: Function;//回调
    target: any;//回调目标
}

interface IEvent {
    [eventName: string]: IEventData[];
}

@ccclass('CustomEventListener')
export class CustomEventListener extends Component {
    public static handle: IEvent = {};

    /**
     * 事件的注册
     * @param eventName 事件名
     * @param cb 回调
     * @param target 回调的对象
     */
    public static on(eventName: string, cb: Function, target?: any) {
        //如果当前处理器没有相对应的事件名,则建立这个事件名的数据
        if (!this.handle[eventName]) {
            this.handle[eventName] = [];
        }
        //定义数据的结构,其中target与传入参数同名,可以将target:target简写如下
        const data: IEventData = { func: cb, target };
        //将这个事件记录上去
        this.handle[eventName].push(data);
    }

    /**
     * 事件的注销
     * @param eventName 事件名
     * @param cb 回调
     * @param target 回调的对象
     * @returns 
     */
    public static off(eventName: string, cb: Function, target?: any) {
        const list = this.handle[eventName];//判断是否有注册过这个事件
        //假如没注册过或者长度小于等于零
        if (!list || list.length <= 0) {
            return;
        }
        //遍历
        for (let i = 0; i < list.length; i++) {
            const event = list[i];
            if (event.func === cb && (!target || target === event.target)) {
                list.splice(i, 1);//注销掉这个事件
                break;
            }
        }
    }

    /**
     * 事件派发
     * @param eventName 事件名
     * @param args 需要传递的参数
     */
    public static dispatchEvent(eventName: string, ...args: any) {
        const list = this.handle[eventName];//判断是否有注册过这个事件
        //假如没注册过或者长度小于等于零
        if (!list || list.length <= 0) {
            return;
        }
        for (let i = 0; i < list.length; i++) {
            const event = list[i];
            event.func.apply(event.target, args);
        }
    }
}

3. 小车和相机的初始化

第一步:小车的开始位置摆放
第二步:小车的运动,匀速从开始到结束
第三步:小车拐弯
第四步:小车匀加速和刹车

在GameMap脚本里,规定每个Map的起始点。

只要规定了起始点,小车自然能找到后面的道路。

import { _decorator, Component, Node } from 'cc';
const { ccclass, property } = _decorator;

@ccclass('GameMap')
export class GameMap extends Component {
    /**记录所有的开始点 */
    @property({
        type: [Node]
    })
    path: Node[] = [];
}
  • 将map101预制拖入mapManager下,并设定path数量为3,规定它们的起始点。
    【Cocos 3d】从零开始自制3d出租车小游戏
  • 应用一下,因为路径都是跟随地图的。

在MapManager里加入属性和重置函数

import { _decorator, Component, Node } from 'cc';
import { GameMap } from './GameMap';
const { ccclass, property } = _decorator;

@ccclass('MapManager')
export class MapManager extends Component {
    public currPath: Node[] = [];//当前的路径

    /**重新更新当前关卡的数据 */
    public resetMap() {
        const currMap = this.node.children[0].getComponent(GameMap);
        this.currPath = currMap.path;
    }
}

在CarManager里加入属性和重置函数

import { _decorator, Component, Node } from 'cc';
import { Car } from './Car';
const { ccclass, property } = _decorator;

@ccclass('CarManager')
export class CarManager extends Component {
    @property({
        type: Car
    })
    mainCar: Car = null;

    public resetCars(points: Node[]) {
        //没有点,抛出警告
        if (points.length <= 0) {
            console.warn("There is no points in this map");
            return;
        }
        //有点
        this._createMainCar(points[0]);
    }

    private _createMainCar(point: Node) {
        this.mainCar.setEntry(point);
    }
}

在Car脚本中加入重置自己世界坐标的函数、

  • 为什么在这里使用世界坐标,因为setPosition只设置相对父节点的位置,而使用世界坐标则是绝对的位置。
import { _decorator, Component, Node } from 'cc';
const { ccclass, property } = _decorator;

@ccclass('Car')
export class Car extends Component {

    /**根据这个点设置自己的位置 */
    public setEntry(entry: Node) {
        this.node.setWorldPosition(entry.worldPosition);
    }
}
  • 在carManager节点上绑定一下MainCar
    【Cocos 3d】从零开始自制3d出租车小游戏

在GameCtrl中加入onLoad生命周期函数,进行初始化。

import { _decorator, Component, Node } from 'cc';
import { CarManager } from './CarManager';
import { MapManager } from './MapManager';
const { ccclass, property } = _decorator;

@ccclass('GameCtrl')
export class GameCtrl extends Component {
    @property({ type: MapManager })
    mapManager: MapManager = null;

    @property({ type: CarManager })
    carManager: CarManager = null;

    public onLoad() {
        this.mapManager.resetMap();
        this.carManager.resetCars(this.mapManager.currPath);
    }
}
  • 把相机摆放在小车上面,并调整一个合适的角度。

  • 这里就随便调调,合适就行。
    【Cocos 3d】从零开始自制3d出租车小游戏

  • 运行看看:
    【Cocos 3d】从零开始自制3d出租车小游戏

4. 小车的运动方向

  • 根据下一个点到这个点的偏移量来摆放小车方向。

改写Car.ts里初始化进入函数

    public _currRoadPoint: RoadPoint = null;
    private _pointA = new Vec3();
    private _pointB = new Vec3();

    /**根据这个点设置自己的位置 */
    public setEntry(entry: Node) {
        this.node.setWorldPosition(entry.worldPosition);
        this._currRoadPoint = entry.getComponent(RoadPoint);
        if (!this._currRoadPoint) {
            console.warn("There is no RoadPoint in ", entry.name);
            return;
        }
        this._pointA.set(entry.worldPosition);
        this._pointB.set(this._currRoadPoint.nextStation.worldPosition);

        //计算朝向
        const z = this._pointB.z - this._pointA.z;
        if (z !== 0) {//排除干扰因素
            if (z < 0) {
                this.node.eulerAngles = new Vec3();
            } else {
                //翻180度
                this.node.eulerAngles = new Vec3(0, 180, 0);
            }
        } else {
            const x = this._pointB.x - this._pointA.x;
            if (x > 0) {
                this.node.eulerAngles = new Vec3(0, 270, 0);
            } else {
                this.node.eulerAngles = new Vec3(0, 90, 0);
            }
        }
    }

注册运动事件

  • GameCtrl.ts里添加
    public start() {
        this.node.on(Node.EventType.TOUCH_START, this._touchStart, this);
        this.node.on(Node.EventType.TOUCH_END, this._touchEnd, this);
    }

    private _touchStart() {
        this.carManager.controlMoving();
    }

    private _touchEnd() {
        this.carManager.controlMoving(false);
    }
  • CarManager.ts里添加对应函数
    private _createMainCar(point: Node) {
        this.mainCar.setEntry(point);
    }

    public controlMoving(isRunning = true) {
        if (isRunning) {
            this.mainCar.startRunning();
        } else {
            this.mainCar.stopRunning();
        }
    }
  • Car.ts里添加
    public update(dt: number) {
        if (this._isMoving) {
            console.log("Moving");
        }
    }
    
    public startRunning() {
        if (this._currRoadPoint) {
            //游戏已经初始化
            this._isMoving = true;
        }
    }

    public stopRunning() {
        this._isMoving = false;
    }
  • 运行看看,发现按下时就打印Moving,则为成功接收到运动事件。

改写Car.ts里的update函数,让小车朝正确的方向动起来

    public update(dt: number) {
        if (this._isMoving) {
            this._offset.set(this.node.worldPosition);
            //朝向哪就往哪运动
            switch (this._currRoadPoint.moveType) {
                case RoadPoint.RoadMoveType.BEND:
                    break;
                default:
                    const z = this._pointB.z - this._pointA.z;
                    if (z !== 0) {
                        if (z > 0) {
                            this._offset.z += this._curSpeed;
                        } else {
                            this._offset.z -= this._curSpeed;
                        }
                    } else {
                        const x = this._pointB.x - this._pointA.x;
                        if (x > 0) {
                            this._offset.x += this._curSpeed;
                        } else {
                            this._offset.x -= this._curSpeed;
                        }
                    }
                    break;
            }
            this.node.setWorldPosition(this._offset);
        }
    }
  • 运行,点下屏幕,此时小车已经可以向前运动起来啦!
    【Cocos 3d】从零开始自制3d出租车小游戏

继续改写update,让小车到达站点

  • 在外部添加一个中间变量
const _tempVec = new Vec3();
  • 改写update
    public update(dt: number) {
        if (this._isMoving) {
            this._offset.set(this.node.worldPosition);
            //朝向哪就往哪运动
            switch (this._currRoadPoint.moveType) {
                case RoadPoint.RoadMoveType.BEND:
                    break;
                default:
                    const z = this._pointB.z - this._pointA.z;
                    if (z !== 0) {
                        if (z > 0) {
                            this._offset.z += this._curSpeed;
                            //容错
                            if (this._offset.z > this._pointB.z) {
                                this._offset.z = this._pointB.z;
                            }
                        } else {
                            this._offset.z -= this._curSpeed;
                            //容错
                            if (this._offset.z < this._pointB.z) {
                                this._offset.z = this._pointB.z;
                            }
                        }
                    } else {
                        const x = this._pointB.x - this._pointA.x;
                        if (x > 0) {
                            this._offset.x += this._curSpeed;
                            //容错
                            if (this._offset.x > this._pointB.x) {
                                this._offset.x = this._pointB.x;
                            }
                        } else {
                            this._offset.x -= this._curSpeed;
                            //容错
                            if (this._offset.x < this._pointB.x) {
                                this._offset.x = this._pointB.x;
                            }
                        }
                    }
                    break;
            }
            this.node.setWorldPosition(this._offset);
            Vec3.subtract(_tempVec, this._pointB, this._offset);
            if (_tempVec.length() <= 0.01) {
                this._arrivalStation();
            }
        }
    }
  • 添加到站函数,更新当前点和下一个点。
    private _arrivalStation() {
        console.log("arrval.....");

        this._pointA.set(this._pointB);
        this._currRoadPoint = this._currRoadPoint.nextStation.getComponent(RoadPoint);
        if (this._currRoadPoint.nextStation) {
            this._pointB.set(this._currRoadPoint.nextStation.worldPosition)
        } else {
            this._isMoving = false;
            this._currRoadPoint = null;
        }
    }
  • 运行看看有无打印
    【Cocos 3d】从零开始自制3d出租车小游戏

让小车可以弯道行驶

  • 改写到达站点的函数,判断它要弯道行驶时,求初始旋转角度originRotation、最终旋转角度targetRotation以及中心点centerPoint。
  • centerPoint实际上就是取点A到点B的中心点,也就是去A的x和B的z,也可能是B的x和A的z,这个情况需要自己去画图分析。
  • 下图介绍了顺时针时,该取谁的x和谁的z
    【Cocos 3d】从零开始自制3d出租车小游戏
    private _arrivalStation() {
        console.log("arrval.....");

        this._pointA.set(this._pointB);
        this._currRoadPoint = this._currRoadPoint.nextStation.getComponent(RoadPoint);
        if (this._currRoadPoint.nextStation) {
            this._pointB.set(this._currRoadPoint.nextStation.worldPosition)
            if (this._currRoadPoint.moveType === RoadPoint.RoadMoveType.BEND) {
                if (this._currRoadPoint.clockwise) {//判断顺时针或逆时针
                    this._originRotation = this._conversion(this.node.eulerAngles.y);
                    this._targetRotation = this._originRotation - 90;
                    //求顺时针centerPoint
                    if ((this._pointB.z < this._pointA.z && this._pointB.x > this._pointA.x) || (this._pointB.z > this._pointA.z && this._pointB.x < this._pointA.x)) {
                        this._centerPoint.set(this._pointB.x, 0, this._pointA.z);
                    } else {
                        this._centerPoint.set(this._pointA.x, 0, this._pointB.z);
                    }
                } else {
                    this._originRotation = this._conversion(this.node.eulerAngles.y);
                    this._targetRotation = this._originRotation + 90;
                    //求逆时针centerPoint
                    if ((this._pointB.z > this._pointA.z && this._pointB.x > this._pointA.x) || (this._pointB.z < this._pointA.z && this._pointB.x < this._pointA.x)) {
                        this._centerPoint.set(this._pointB.x, 0, this._pointA.z);
                    } else {
                        this._centerPoint.set(this._pointA.x, 0, this._pointB.z);
                    }
                }
                Vec3.subtract(_tempVec, this._pointA, this._centerPoint);
                const r = _tempVec.length();
                this._rotMeasure = 90 / (Math.PI * r / 2);
            }
        } else {
            this._isMoving = false;
            this._currRoadPoint = null;
        }
    }

    /**工具函数:将所有负数角度都转化为正数角度 */
    private _conversion(value: number) {
        let a = value;
        if (a <= 0) {
            a += 360;
        }
        return a;
    }
  • 利用以上参数,改写update中BEND的情况。
switch (this._currRoadPoint.moveType) {
    case RoadPoint.RoadMoveType.BEND:
        const offsetRotation = this._targetRotation - this._originRotation;
        const currRotation = this._conversion(this.node.eulerAngles.y)
        let nextStation = (currRotation - this._originRotation) + (this._currSpeed * this._rotMeasure * (this._targetRotation > this._originRotation ? 1 : -1));
        if (Math.abs(nextStation) > Math.abs(offsetRotation)) {
            nextStation = offsetRotation;
        }
        const target = nextStation + this._originRotation;
        _tempVec.set(0, target, 0);
        this.node.eulerAngles = _tempVec;
        const sin = Math.sin(nextStation * Math.PI / 180);//通过角度求出弧度
        const cos = Math.cos(nextStation * Math.PI / 180);
        const xLength = this._pointA.x - this._centerPoint.x;
        const zLength = this._pointA.z - this._centerPoint.z;
        const offx = xLength * cos + zLength * sin + this._centerPoint.x;
        const offz = -xLength * sin + zLength * cos + this._centerPoint.z;
        this._offset.set(offx, 0, offz)
        break;
  • 其中要运用到矩阵计算公式中的旋转公式,来计算小车的最终坐标。
    【Cocos 3d】从零开始自制3d出租车小游戏
  • 编辑一下路线,注意转弯的节点必须要精确。设置从A点转弯到B点时,需要两个点刚好位于角度为90度的圆弧上,否则将无法识别。
  • 通过上面的代码也可以理解这一点。
  • 下面这个示意图也许不标准,但大家理解代码并在编辑器里尝试一下就可以解决这个小问题。
    【Cocos 3d】从零开始自制3d出租车小游戏
  • 如果都能识别上,小车的道路应该可以以“直-弯-直”行驶,也可以行驶更长的路径(只要你一直正确地设置下去)
  • 我设置了一个如下所示的路径:
    【Cocos 3d】从零开始自制3d出租车小游戏
  • 运行一下:
    【Cocos 3d】从零开始自制3d出租车小游戏
  • 但我数学不好啊,我哪会矩阵乘法,用矩阵算旋转太麻烦了,就没个API吗?
  • 当然有!一行代码就能代替上面注释掉的这么多行,真香啊
// const sin = Math.sin(nextStation * Math.PI / 180);//通过角度求出弧度
// const cos = Math.cos(nextStation * Math.PI / 180);
// const xLength = this._pointA.x - this._centerPoint.x;
// const zLength = this._pointA.z - this._centerPoint.z;
// const offx = xLength * cos + zLength * sin + this._centerPoint.x;
// const offz = -xLength * sin + zLength * cos + this._centerPoint.z;
// this._offset.set(offx, 0, offz)

//但是我数学不好,我不会用矩阵算旋转,我直接用下面这个API一步到位,真香
//绕轴旋转的API
Vec3.rotateY(this._offset, this._pointA, this._centerPoint, nextStation * Math.PI / 180);
  • 运行一下,也是一样的可以转得很丝滑!

小车加速度

  • 定义一个加速度,并将之前设置的匀速运动改为0,在设置一个最大限速
    @property
    maxSpeed = 0.2;//最大限速
    private _currSpeed = 0;
    private _acceleration = 0.2;//加速度
  • 改写开始和停止的代码
    public startRunning() {
        if (this._currRoadPoint) {
            //游戏已经初始化
            this._isMoving = true;
            this._currSpeed = 0;
            this._acceleration = 0.2;
        }
    }

    public stopRunning() {
        this._acceleration = -0.3;
        // this._isMoving = false;
    }
  • 运行一下看看小车能否加速和刹车了
    【Cocos 3d】从零开始自制3d出租车小游戏

完整的Car.ts代码

import { _decorator, Component, Node, Vec3, CurveRange } from 'cc';
import { RoadPoint } from './RoadPoint';
const { ccclass, property } = _decorator;
const _tempVec = new Vec3();

@ccclass('Car')
export class Car extends Component {
    @property
    maxSpeed = 0.2;//最大限速

    public _currRoadPoint: RoadPoint = null;
    private _pointA = new Vec3();
    private _pointB = new Vec3();
    private _currSpeed = 0;
    private _acceleration = 0.2;//加速度
    private _isMoving: boolean = false;//标记游戏是否已经开始
    private _offset = new Vec3();
    private _originRotation = 0;//旋转相关
    private _targetRotation = 0;//旋转相关
    private _centerPoint = new Vec3();//旋转相关
    private _rotMeasure = 0;//旋转相关度量值


    public update(dt: number) {
        if (this._isMoving) {
            this._offset.set(this.node.worldPosition);
            this._currSpeed += this._acceleration * dt;
            if (this._currSpeed > this.maxSpeed) {
                this._currSpeed = this.maxSpeed;//限速
            }
            if (this._currSpeed <= 0.001) {
                this._isMoving = false;//刹车到已经停止
            }
            //朝向哪就往哪运动
            switch (this._currRoadPoint.moveType) {
                case RoadPoint.RoadMoveType.BEND:
                    const offsetRotation = this._targetRotation - this._originRotation;
                    const currRotation = this._conversion(this.node.eulerAngles.y)
                    let nextStation = (currRotation - this._originRotation) + (this._currSpeed * this._rotMeasure * (this._targetRotation > this._originRotation ? 1 : -1));
                    if (Math.abs(nextStation) > Math.abs(offsetRotation)) {
                        nextStation = offsetRotation;
                    }
                    const target = nextStation + this._originRotation;
                    _tempVec.set(0, target, 0);
                    this.node.eulerAngles = _tempVec;
                    //绕轴旋转的API
                    Vec3.rotateY(this._offset, this._pointA, this._centerPoint, nextStation * Math.PI / 180);
                    break;
                default:
                    const z = this._pointB.z - this._pointA.z;
                    if (z !== 0) {
                        if (z > 0) {
                            this._offset.z += this._currSpeed;
                            //容错
                            if (this._offset.z > this._pointB.z) {
                                this._offset.z = this._pointB.z;
                            }
                        } else {
                            this._offset.z -= this._currSpeed;
                            //容错
                            if (this._offset.z < this._pointB.z) {
                                this._offset.z = this._pointB.z;
                            }
                        }
                    } else {
                        const x = this._pointB.x - this._pointA.x;
                        if (x > 0) {
                            this._offset.x += this._currSpeed;
                            //容错
                            if (this._offset.x > this._pointB.x) {
                                this._offset.x = this._pointB.x;
                            }
                        } else {
                            this._offset.x -= this._currSpeed;
                            //容错
                            if (this._offset.x < this._pointB.x) {
                                this._offset.x = this._pointB.x;
                            }
                        }
                    }
                    break;
            }
            this.node.setWorldPosition(this._offset);
            Vec3.subtract(_tempVec, this._pointB, this._offset);
            if (_tempVec.length() <= 0.01) {
                this._arrivalStation();
            }
        }
    }

    /**根据这个点设置自己的位置 */
    public setEntry(entry: Node) {
        this.node.setWorldPosition(entry.worldPosition);
        this._currRoadPoint = entry.getComponent(RoadPoint);
        if (!this._currRoadPoint) {
            console.warn("There is no RoadPoint in ", entry.name);
            return;
        }
        this._pointA.set(entry.worldPosition);
        this._pointB.set(this._currRoadPoint.nextStation.worldPosition);

        //计算朝向
        const z = this._pointB.z - this._pointA.z;
        if (z !== 0) {//排除干扰因素
            if (z < 0) {
                this.node.eulerAngles = new Vec3();
            } else {
                //翻180度
                this.node.eulerAngles = new Vec3(0, 180, 0);
            }
        } else {
            const x = this._pointB.x - this._pointA.x;
            if (x > 0) {
                this.node.eulerAngles = new Vec3(0, 270, 0);
            } else {
                this.node.eulerAngles = new Vec3(0, 90, 0);
            }
        }
    }
    /**为什么在这里使用世界坐标,因为setPosition只设置相对父节点的位置,而使用世界坐标则是绝对的位置。 */

    public startRunning() {
        if (this._currRoadPoint) {
            //游戏已经初始化
            this._isMoving = true;
            this._currSpeed = 0;
            this._acceleration = 0.2;
        }
    }

    public stopRunning() {
        this._acceleration = -0.3;
        // this._isMoving = false;
    }

    private _arrivalStation() {
        console.log("arrval.....");

        this._pointA.set(this._pointB);
        this._currRoadPoint = this._currRoadPoint.nextStation.getComponent(RoadPoint);
        if (this._currRoadPoint.nextStation) {
            this._pointB.set(this._currRoadPoint.nextStation.worldPosition)
            if (this._currRoadPoint.moveType === RoadPoint.RoadMoveType.BEND) {
                if (this._currRoadPoint.clockwise) {//判断顺时针或逆时针
                    this._originRotation = this._conversion(this.node.eulerAngles.y);
                    this._targetRotation = this._originRotation - 90;
                    //求顺时针centerPoint
                    if ((this._pointB.z < this._pointA.z && this._pointB.x > this._pointA.x) || (this._pointB.z > this._pointA.z && this._pointB.x < this._pointA.x)) {
                        this._centerPoint.set(this._pointB.x, 0, this._pointA.z);
                    } else {
                        this._centerPoint.set(this._pointA.x, 0, this._pointB.z);
                    }
                } else {
                    this._originRotation = this._conversion(this.node.eulerAngles.y);
                    this._targetRotation = this._originRotation + 90;
                    //求逆时针centerPoint
                    if ((this._pointB.z > this._pointA.z && this._pointB.x > this._pointA.x) || (this._pointB.z < this._pointA.z && this._pointB.x < this._pointA.x)) {
                        this._centerPoint.set(this._pointB.x, 0, this._pointA.z);
                    } else {
                        this._centerPoint.set(this._pointA.x, 0, this._pointB.z);
                    }
                }
                Vec3.subtract(_tempVec, this._pointA, this._centerPoint);
                const r = _tempVec.length();
                this._rotMeasure = 90 / (Math.PI * r / 2);
            }
        } else {
            this._isMoving = false;
            this._currRoadPoint = null;
        }
    }

    /**将所有负数角度都转化为正数角度 */
    private _conversion(value: number) {
        let a = value;
        if (a <= 0) {
            a += 360;
        }
        return a;
    }
}

5. 顾客

  • 用回我们之前使用的直线地图。

  • 将customer模型加入场景中,并更改名字
    【Cocos 3d】从零开始自制3d出租车小游戏
    【Cocos 3d】从零开始自制3d出租车小游戏

  • 新建CustomerManager脚本并挂在customerManger节点上
    【Cocos 3d】从零开始自制3d出租车小游戏文章来源地址https://www.toymoban.com/news/detail-433354.html

改写Car.ts

  • 在Car.ts里派发接客和送客的事件
  • 并且定义一个变量,判断现在是否在订单中。在订单中,也就是乘客正在走动,车是不能动的。
    /**接客 */
    private _greetingCustomer() {
        this._isInOrder = true;
        CustomEventListener.dispatchEvent(EventName.GREETING, this.node.worldPosition, this._currRoadPoint.direction);
    }

    /**送客 */
    private _takingCustomer() {
        this._isInOrder = true;
        CustomEventListener.dispatchEvent(EventName.GOODBYE, this.node.worldPosition, this._currRoadPoint.direction);
    }

    private finishedWalk() {
        this._isInOrder = false;
    }
  • 稍微改写一下update,让车在乘客走动的时候不动。
    public update(dt: number) {
        if (!this._isMoving || this._isInOrder) return;
  • 添加_isMainCar变量,并改写一下setEntry函数,判断这辆车是否为玩家车
    public setEntry(entry: Node, isMain = false) {
        this._isMainCar = isMain;
  • 完整Car.ts
import { _decorator, Component, Node, Vec3, CurveRange } from 'cc';
import { Constants } from '../data/Constants';
import { CustomEventListener } from '../data/CustomEventListener';
import { RoadPoint } from './RoadPoint';
const { ccclass, property } = _decorator;

const _tempVec = new Vec3();
const EventName = Constants.EventName;

@ccclass('Car')
export class Car extends Component {
    @property
    maxSpeed = 0.2;//最大限速

    public _currRoadPoint: RoadPoint = null;
    private _pointA = new Vec3();
    private _pointB = new Vec3();
    private _currSpeed = 0;
    private _acceleration = 0.2;//加速度
    private _isMoving: boolean = false;//标记游戏是否已经开始
    private _offset = new Vec3();
    private _originRotation = 0;//旋转相关
    private _targetRotation = 0;//旋转相关
    private _centerPoint = new Vec3();//旋转相关
    private _rotMeasure = 0;//旋转相关度量值
    private _isMainCar: boolean = false;//是否玩家小车
    private _isInOrder = false;//是否在订单内

    public start(): void {
        CustomEventListener.on(EventName.FINISHEDWALK, this.finishedWalk, this);
    }


    public update(dt: number) {
        if (!this._isMoving || this._isInOrder) return;

        this._offset.set(this.node.worldPosition);
        this._currSpeed += this._acceleration * dt;
        if (this._currSpeed > this.maxSpeed) {
            this._currSpeed = this.maxSpeed;//限速
        }
        if (this._currSpeed <= 0.001) {
            this._isMoving = false;//刹车到已经停止
        }
        //朝向哪就往哪运动
        switch (this._currRoadPoint.moveType) {
            case RoadPoint.RoadMoveType.BEND:
                const offsetRotation = this._targetRotation - this._originRotation;
                const currRotation = this._conversion(this.node.eulerAngles.y)
                let nextStation = (currRotation - this._originRotation) + (this._currSpeed * this._rotMeasure * (this._targetRotation > this._originRotation ? 1 : -1));
                if (Math.abs(nextStation) > Math.abs(offsetRotation)) {
                    nextStation = offsetRotation;
                }
                const target = nextStation + this._originRotation;
                _tempVec.set(0, target, 0);
                this.node.eulerAngles = _tempVec;
                //绕轴旋转的API
                Vec3.rotateY(this._offset, this._pointA, this._centerPoint, nextStation * Math.PI / 180);
                break;
            default:
                const z = this._pointB.z - this._pointA.z;
                if (z !== 0) {
                    if (z > 0) {
                        this._offset.z += this._currSpeed;
                        //容错
                        if (this._offset.z > this._pointB.z) {
                            this._offset.z = this._pointB.z;
                        }
                    } else {
                        this._offset.z -= this._currSpeed;
                        //容错
                        if (this._offset.z < this._pointB.z) {
                            this._offset.z = this._pointB.z;
                        }
                    }
                } else {
                    const x = this._pointB.x - this._pointA.x;
                    if (x > 0) {
                        this._offset.x += this._currSpeed;
                        //容错
                        if (this._offset.x > this._pointB.x) {
                            this._offset.x = this._pointB.x;
                        }
                    } else {
                        this._offset.x -= this._currSpeed;
                        //容错
                        if (this._offset.x < this._pointB.x) {
                            this._offset.x = this._pointB.x;
                        }
                    }
                }
                break;
        }
        this.node.setWorldPosition(this._offset);
        Vec3.subtract(_tempVec, this._pointB, this._offset);
        if (_tempVec.length() <= 0.01) {
            this._arrivalStation();
        }

    }

    /**根据这个点设置自己的位置 */
    public setEntry(entry: Node, isMain = false) {
        this.node.setWorldPosition(entry.worldPosition);
        this._currRoadPoint = entry.getComponent(RoadPoint);
        this._isMainCar = isMain;
        if (!this._currRoadPoint) {
            console.warn("There is no RoadPoint in ", entry.name);
            return;
        }
        this._pointA.set(entry.worldPosition);
        this._pointB.set(this._currRoadPoint.nextStation.worldPosition);

        //计算朝向
        const z = this._pointB.z - this._pointA.z;
        if (z !== 0) {//排除干扰因素
            if (z < 0) {
                this.node.eulerAngles = new Vec3();
            } else {
                //翻180度
                this.node.eulerAngles = new Vec3(0, 180, 0);
            }
        } else {
            const x = this._pointB.x - this._pointA.x;
            if (x > 0) {
                this.node.eulerAngles = new Vec3(0, 270, 0);
            } else {
                this.node.eulerAngles = new Vec3(0, 90, 0);
            }
        }
    }
    /**为什么在这里使用世界坐标,因为setPosition只设置相对父节点的位置,而使用世界坐标则是绝对的位置。 */

    public startRunning() {
        if (this._currRoadPoint) {
            //游戏已经初始化
            this._isMoving = true;
            this._currSpeed = 0;
            this._acceleration = 0.2;
        }
    }

    public stopRunning() {
        this._acceleration = -0.3;
        // this._isMoving = false;
    }

    private _arrivalStation() {
        console.log("arrval.....");

        this._pointA.set(this._pointB);
        this._currRoadPoint = this._currRoadPoint.nextStation.getComponent(RoadPoint);
        if (this._currRoadPoint.nextStation) {
            this._pointB.set(this._currRoadPoint.nextStation.worldPosition)
            //假如是玩家小车并且是接客点或者送客点
            if (this._isMainCar) {
                if (this._currRoadPoint.type === RoadPoint.RoadPointType.GREETING) {
                    this._greetingCustomer();
                }
                else if (this._currRoadPoint.type === RoadPoint.RoadPointType.GOODBYE) {
                    this._takingCustomer();
                }
            }

            if (this._currRoadPoint.moveType === RoadPoint.RoadMoveType.BEND) {
                if (this._currRoadPoint.clockwise) {//判断顺时针或逆时针
                    this._originRotation = this._conversion(this.node.eulerAngles.y);
                    this._targetRotation = this._originRotation - 90;
                    //求顺时针centerPoint
                    if ((this._pointB.z < this._pointA.z && this._pointB.x > this._pointA.x) || (this._pointB.z > this._pointA.z && this._pointB.x < this._pointA.x)) {
                        this._centerPoint.set(this._pointB.x, 0, this._pointA.z);
                    } else {
                        this._centerPoint.set(this._pointA.x, 0, this._pointB.z);
                    }
                } else {
                    this._originRotation = this._conversion(this.node.eulerAngles.y);
                    this._targetRotation = this._originRotation + 90;
                    //求逆时针centerPoint
                    if ((this._pointB.z > this._pointA.z && this._pointB.x > this._pointA.x) || (this._pointB.z < this._pointA.z && this._pointB.x < this._pointA.x)) {
                        this._centerPoint.set(this._pointB.x, 0, this._pointA.z);
                    } else {
                        this._centerPoint.set(this._pointA.x, 0, this._pointB.z);
                    }
                }
                Vec3.subtract(_tempVec, this._pointA, this._centerPoint);
                const r = _tempVec.length();
                this._rotMeasure = 90 / (Math.PI * r / 2);
            }
        } else {
            this._isMoving = false;
            this._currRoadPoint = null;
        }
    }

    /**接客 */
    private _greetingCustomer() {
        this._isInOrder = true;
        CustomEventListener.dispatchEvent(EventName.GREETING, this.node.worldPosition, this._currRoadPoint.direction);
    }

    /**送客 */
    private _takingCustomer() {
        this._isInOrder = true;
        CustomEventListener.dispatchEvent(EventName.GOODBYE, this.node.worldPosition, this._currRoadPoint.direction);
    }

    private finishedWalk() {
        this._isInOrder = false;
    }

    /**将所有负数角度都转化为正数角度 */
    private _conversion(value: number) {
        let a = value;
        if (a <= 0) {
            a += 360;
        }
        return a;
    }
}

在CarManager里改写一下生成MainCar的函数

  • CarManager.ts
import { _decorator, Component, Node } from 'cc';
import { Car } from './Car';
const { ccclass, property } = _decorator;

@ccclass('CarManager')
export class CarManager extends Component {
    @property({
        type: Car
    })
    mainCar: Car = null;

    public resetCars(points: Node[]) {
        //没有点,抛出警告
        if (points.length <= 0) {
            console.warn("There is no points in this map");
            return;
        }
        //有点
        this._createMainCar(points[0]);
    }

    private _createMainCar(point: Node) {
        this.mainCar.setEntry(point, true);
    }

    public controlMoving(isRunning = true) {
        if (isRunning) {
            this.mainCar.startRunning();
        } else {
            this.mainCar.stopRunning();
        }
    }
}

在Constants里定义顾客的状态

  • Constants.ts
import { _decorator, Component, Node } from 'cc';
const { ccclass, property } = _decorator;

enum EventName {
    GREETING = 'greeting',
    GOODBYE = 'goodbye',
    FINISHEDWALK = 'finishedd-walk'
}

enum CustomerState {
    NONE,
    GREETING,
    GOODBYE
}

@ccclass('Constants')
export class Constants {
    public static EventName = EventName;
    public static CustomerState = CustomerState;
}

在CustomerManager里加入接客、送客、客人运动的函数。

  • CustomerManager.ts
import { _decorator, Component, Node, Vec2, Vec3, AnimationComponent } from 'cc';
import { Constants } from '../data/Constants';
import { CustomEventListener } from '../data/CustomEventListener';
const { ccclass, property } = _decorator;

const EventName = Constants.EventName;
const _tempVec = new Vec3();

@ccclass('CustomerManager')
export class CustomerManager extends Component {
    @property({
        type: [Node]
    })
    customers: Node[] = [];

    @property
    walkTime = 2;//运动时间

    private _currCustomer: Node = null;
    private _startPos = new Vec3();
    private _endPos = new Vec3();
    private _inTheOrder: boolean = false;//处于订单状态
    private _deltaTime = 0;
    private _state = Constants.CustomerState.NONE;

    public start(): void {
        CustomEventListener.on(EventName.GREETING, this._greetingCustomer, this);
        CustomEventListener.on(EventName.GOODBYE, this._takingCustomer, this);
    }

    public update(dt: number): void {
        //在订单中
        if (this._inTheOrder) {
            this._deltaTime += dt;
            if (this._deltaTime > this.walkTime) {
                //乘客已经走到
                this._deltaTime = 0;
                this._inTheOrder = false;
                this._currCustomer.active = false;
                if (this._state === Constants.CustomerState.GOODBYE) {
                    this._currCustomer = null;
                }
                //给小车派发事件,你可以继续运动了
                CustomEventListener.dispatchEvent(EventName.FINISHEDWALK);
            } else {
                //逐元素向量线性插值
                Vec3.lerp(_tempVec, this._startPos, this._endPos, this._deltaTime / this.walkTime);
                this._currCustomer.setWorldPosition(_tempVec);
            }

        }
    }

    private _greetingCustomer(...args: any[]) {
        this._currCustomer = this.customers[Math.floor(Math.random() * this.customers.length)];
        this._state = Constants.CustomerState.GREETING;
        this._inTheOrder = true;
        if (!this._currCustomer) return;//没有顾客,不接
        const carPos = args[0];//小车的位置
        const direction = args[1];//方向
        Vec3.multiplyScalar(this._startPos, direction, 1.4);
        this._startPos.add(carPos);
        Vec3.multiplyScalar(this._endPos, direction, 0.5);
        this._endPos.add(carPos);

        this._currCustomer.setWorldPosition(this._startPos);
        this._currCustomer.active = true;


        if (direction.x !== 0) {
            //人处于我们的右边
            if (direction.x > 0) {
                this._currCustomer.eulerAngles = new Vec3(0, -90, 0);
            } else {
                this._currCustomer.eulerAngles = new Vec3(0, 90, 0);
            }
        } else {
            if (direction.z > 0) {
                this._currCustomer.eulerAngles = new Vec3(0, 180, 0);
            } else {
                this._currCustomer.eulerAngles = new Vec3();
            }
        }

        const animComp = this._currCustomer.getComponent(AnimationComponent);
        animComp.play('walk');

    }

    private _takingCustomer(...args: any[]) {
        this._state = Constants.CustomerState.GOODBYE;
        this._inTheOrder = true;
        const carPos = args[0];//小车的位置
        const direction = args[1];//方向
        Vec3.multiplyScalar(this._startPos, direction, 0.5);
        this._startPos.add(carPos);
        Vec3.multiplyScalar(this._endPos, direction, 1.4);
        this._endPos.add(carPos);

        this._currCustomer.setWorldPosition(this._startPos);
        this._currCustomer.active = true;


        if (direction.x !== 0) {
            //人处于我们的右边
            if (direction.x > 0) {
                this._currCustomer.eulerAngles = new Vec3(0, 90, 0);
            } else {
                this._currCustomer.eulerAngles = new Vec3(0, -90, 0);
            }
        } else {
            if (direction.z > 0) {
                this._currCustomer.eulerAngles = new Vec3();
            } else {
                this._currCustomer.eulerAngles = new Vec3(0, 180, 0);
            }
        }

        const animComp = this._currCustomer.getComponent(AnimationComponent);
        animComp.play('walk');
    }
}

  • 运行一下
    【Cocos 3d】从零开始自制3d出租车小游戏

到了这里,关于【Cocos 3d】从零开始自制3d出租车小游戏的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 基于单片机出租车计价器设计

     功能介绍 以51单片机作为主控系统; 1602液晶屏显示最初的起步价,里程收费,等待时间收费; 按键调整起步价,里程收费,等待时间收费; 电机旋转,通过霍尔传感器检测转速,来模拟出租车行驶; 电路图 仿真图 元器件清单 B1 5V直流电机+托盘(粘好磁铁) BT1 2032纽扣电

    2024年02月11日
    浏览(52)
  • 企业spark案例 —— 出租车轨迹分析(Python)

    头歌的大数据作业,答案没找着,遂自己整了一份 第1关:SparkSql 数据清洗 任务描述 本关任务:将出租车轨迹数据规整化,清洗掉多余的字符串。 相关知识 为了完成本关任务,你需要掌握:1. 如何使用 SparkSQL 读取 CSV 文件,2. 如何使用正则表达式清洗掉多余字符串。 编程要

    2024年02月03日
    浏览(46)
  • 【Vivado】基于FPGA的出租车计价表设计

    学校FPGA设计结课课设 主要做了出租车计价表,一个比较旧的课题,代码如下: 分模块编程,按照价目表写代码,具体注释见代码。 在module里新加一个 input 变量 key_stage ,用 key_stage 表示不同车流量段,用以计数 在module里新增一个变量 state 来限定是在白天还是夜间

    2024年02月04日
    浏览(53)
  • LeetCode 2008. 出租车的最大盈利:动态规划 + 哈希表

    力扣题目链接:https://leetcode.cn/problems/maximum-earnings-from-taxi/ 你驾驶出租车行驶在一条有 n  个地点的路上。这 n  个地点从近到远编号为  1  到  n  ,你想要从 1  开到 n  ,通过接乘客订单盈利。你只能沿着编号递增的方向前进,不能改变方向。 乘客信息用一个下标从 0  开

    2024年02月04日
    浏览(47)
  • 0097-基于单片机的出租车计价器仿真设计

    1、采用51/52单片机作为主控芯片; 2、采用1602液晶显示:里程、计价、实时时间、实时单价、本次行程计时; 3、采用DS1302作为时钟芯片; 4、支持切换显示界面、设置日期时间、设置白天单价、设置夜晚单价; 5、支持分别设置3千米内的单价、3千米外的单价、等待时的单价

    2024年02月20日
    浏览(47)
  • 仿滴滴打车百度地图定位查找附近出租车或门店信息

    随着技术的发展,开发的复杂度也越来越高,传统开发方式将一个系统做成了整块应用,经常出现的情况就是一个小小的改动或者一个小功能的增加可能会引起整体逻辑的修改,造成牵一发而动全身。通过组件化开发,可以有效实现单独开发,单独维护,而且他们之间可以随

    2024年02月09日
    浏览(57)
  • 使用TransBigData快速高效地处理、分析、挖掘出租车GPS数据

    TransBigData是一个为交通时空大数据处理、分析和可视化而开发的Python包。TransBigData为处理常见的交通时空大数据(如出租车GPS数据、共享单车数据和公交车GPS数据等)提供了快速而简洁的方法。TransBigData为交通时空大数据分析的各个阶段提供了多种处理方法,代码简洁、高效、

    2024年02月14日
    浏览(45)
  • transbigdata 笔记:官方文档案例1(出租车GPS数据处理)

    官方文档中给定的出租车数据在transbigdata/docs/source/gallery/data/TaxiData-Sample.csv at main · ni1o1/transbigdata (github.com)     transbigdata笔记:数据预处理-CSDN博客 transbigdata笔记:数据预处理-CSDN博客 异常记录点,指的是记录点前后的出租车状态(有乘客/无乘客)和自己的出租车状态不一

    2024年01月21日
    浏览(47)
  • 出租车模拟计费Verilog代码AX301开发板Quartus

    名称:出租车模拟计费Verilog代码AX301开发板Quartus 软件:Quartus 语言:Verilog 代码功能: 出租车模拟计费系统的实现 设计一个模拟的出租车计费系统,能显示里程和费用。 要求:(1)自行设定车速,根据计时转换为里程,里程显示方式为XXX,单位为km; (2)费用的计算及显

    2024年01月17日
    浏览(40)
  • 【C51】基于51单片机的出租车计价器设计

    随着我国经济的快速发展,出行选择乘坐出租车的人越来越多。与此同时电子信息技术的发展更新,更加准确、便捷、稳定的出租车计价收费系统随之出现。基于单片机的出租车计价系统的设计,不仅可以更加准确、稳定的反映计价情况,也能促进出租车行业健康稳定的发展

    2024年02月03日
    浏览(55)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包