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

阅读: 评论:0

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

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

引言

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

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

简介:
资源下载:
适合学习人群:本教程假定你对编程有一定的了解,ts,js 学习过其中之一。
如果不曾了解过,可以参考 js 教程()或者 ts 教程(/)
下载引擎:/
关注官方微信公众号(CocosEngine),不定期推送福利活动、新手教程还有好玩的活动噢!

本文作者的bb:
  项目已上传至:
  引擎版本为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吧。

  • 下好后是这样的:

  • 下载时要往下滑,滑到最底下找到废弃的版本。

新建项目

  • 在右下角新建项目

  • 记得修改项目名字,叫Taxi或者什么,随意。

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

导入资源

  1. 上这个网站把资源下载下来。
    资源下载:
  2. 找到这个文件夹:
  3. 打开,把里面的东西全部拖到引擎内的资源管理器下的asserts文件夹下。
  4. 等待资源导入,观察控制台,无报错就算成功。

搭建场景

1. 观察资源

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

2. 查看草图

策划给我们画的草图如下:

其中从start到end一共有4个建筑物,分别对应的是接客、送客、接客、送客操作。

3. 搭建路面

  • 新建一个地图节点(空节点),并添加路面管理节点(空节点),并将路面资源拖入路面管理节点。
  • 放入后如下所示

4. 搭建路线

  • 如图所示,路线只需要直线路面和十字路口即可完成搭建。
  • 在添加直线路面后,通过修改scale属性里的z值来修改长度。图中3段直线路面的z值分别是15、10、30.

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

  • 给十字路口部分补充上一点道路,没有细调。
  • (我做的时候:能看就行。)

5. 放置标记

  • 在map下新建空节点sign,用于放置标记。
  • 放置start标记,如下所示:
  • 同理放置end标记,移动到终点位置
  • 放置两个向前引导的标记
  • 到这里整个的路线已经出来了。

6. 放置建筑物

  • 定义黄色的建筑物是接客建筑物,蓝灰色建筑物为送客建筑物。
  • 新建building节点用于管理建筑物。
  • 放置完成大致为如下样式,不必细调位置。

7. 放置装饰物

  • 在map下新建decoration节点用于管理装饰物。
  • 按自己喜好放置就好。
  • 我因为懒,一样随便放了一点。

保存地图

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

  • 在asserts文件夹下新建一个prefab文件夹,再在prefab下建一个map文件夹,将层级管理器里的map101直接拖到这个文件夹里,保存这个预制。

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

录入路径

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

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

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

  • 同时,我们将小车模型资源放上去。

  • 保存玩家可用的小车为预制。(将taxi01改名为car101)

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

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

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

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

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

  • 左侧的意思为:每条线的障碍物小车的产生频率、产生的延迟时间、车速、车的种类等。

  • 新建一个脚本,用枚举的方式来定义这些点的属性和路面样式。

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) {pe != ROAD_POINT_TYPE.END}})nextStation: Node = null;@property({ type: ROAD_MOVE_TYPE })moveType = ROAD_MOVE_TYPE.LINE;@property({visible: function (this: RoadPoint) {pe != ROAD_POINT_TYPE.END && veType === ROAD_MOVE_TYPE.CURVE}})clockwise = true;//默认顺时针@property({type: Vec3,visible: function (this: RoadPoint) {pe === ROAD_POINT_TYPE.GREETING || pe === ROAD_POINT_TYPE.GOODBYE}})direction = new Vec3(1, 0, 0);//接送客的方向,默认右边@property({visible: function (this: RoadPoint) {pe === ROAD_POINT_TYPE.AISTART}})interval = 3;//AI产出间隔@property({visible: function (this: RoadPoint) {pe === ROAD_POINT_TYPE.AISTART}})delayTime = 0;@property({visible: function (this: RoadPoint) {pe === ROAD_POINT_TYPE.AISTART}})speed = 0.05;//小车速度@property({visible: function (this: RoadPoint) {pe === ROAD_POINT_TYPE.AISTART}})/**如果要产出不同的车,用逗号分开。如"201,202" */cars = '201';//当前路径产出的小车的类型
  • 将这个脚本挂载在point节点上。
  • 删掉point上的car,我们的路线上并不需要这个。
  • 将point复制几份,分别放在路上不同的位置。
  • 其中包含一个start点-接客点-送客点-接客点-送客点-end点
  • 在组件中配置每个点的信息,一第二个点(第一个接客点)为例:
  • 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) {pe != ROAD_POINT_TYPE.END}})nextStation: Node = null;@property({ type: ROAD_MOVE_TYPE })moveType = ROAD_MOVE_TYPE.LINE;@property({visible: function (this: RoadPoint) {pe != ROAD_POINT_TYPE.END && veType === ROAD_MOVE_TYPE.BEND}})clockwise = true;//默认顺时针@property({type: Vec3,visible: function (this: RoadPoint) {pe === ROAD_POINT_TYPE.GREETING || pe === ROAD_POINT_TYPE.GOODBYE}})direction = new Vec3(1, 0, 0);//接送客的方向,默认右边@property({visible: function (this: RoadPoint) {pe === ROAD_POINT_TYPE.AISTART}})interval = 3;//AI产出间隔@property({visible: function (this: RoadPoint) {pe === ROAD_POINT_TYPE.AISTART}})delayTime = 0;@property({visible: function (this: RoadPoint) {pe === ROAD_POINT_TYPE.AISTART}})speed = 0.05;//小车速度@property({visible: function (this: RoadPoint) {pe === ROAD_POINT_TYPE.AISTART}})/**如果要产出不同的车,用逗号分开。如"201,202" */cars = '201';//当前路径产出的小车的类型
}
  • 搭建完了玩家的小车路径,还要搭建几条AI小车的路径。这个先随意搭建两条就好,后面需要用上。
    在这里path02和path03都是AI小车的路径,随意摆放一下。注意复制的时候需要重新绑定一下节点喔~

游戏逻辑

1. 游戏逻辑梳理

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

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

对于游戏的几个大类,我们应当对其设置不同的管理类:
MapManager CarManager CustomerManager AudioManager EffectManager UIManager
这些管理部分,还要统一被一个流程控制的控制类来控制流程,来负责管理这几个类:GameCtrl
这些管理部分又可以衍生出自身要管理的部分,大致如下所示:

在本游戏中,我们采用事件的方式播放音效和特效。

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

  • 新建一个关于游戏运行的文件夹game,并把之前的RoadPoint拖动到该文件夹下。
  • 新建GameCtrl、CarManager、MapManager的脚本。
  • 新建一个scenes文件夹,并新建一个场景:Game(下图中的GameScene不用理会)
  • 选中Game场景进行编辑
  • 新建Canvas节点
  • 将GameCtrl脚本挂在Canvas上

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;}

  • 在层级管理器中添加map101预制(之前做好的那个预制)
  • 新建GameMap脚本,并挂在map101上。
  • 在这里点击应用
  • 同理,新建Car脚本,挂在小车预制上,应用一下。
  • 新建消息监听器脚本

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,规定它们的起始点。
  • 应用一下,因为路径都是跟随地图的。

在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 = de.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) {de.setWorldPosition(entry.worldPosition);}
}
  • 在carManager节点上绑定一下MainCar

在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() {setMap();setCars(this.mapManager.currPath);}
}
  • 把相机摆放在小车上面,并调整一个合适的角度。

  • 这里就随便调调,合适就行。

  • 运行看看:

4. 小车的运动方向

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

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

    public _currRoadPoint: RoadPoint = null;private _pointA = new Vec3();private _pointB = new Vec3();/**根据这个点设置自己的位置 */public setEntry(entry: Node) {de.setWorldPosition(entry.worldPosition);this._currRoadPoint = Component(RoadPoint);if (!this._currRoadPoint) {console.warn("There is no RoadPoint in ", entry.name);return;}this._pointA.set(entry.worldPosition);this._pointB.set(this._Station.worldPosition);//计算朝向const z = this._pointB.z - this._pointA.z;if (z !== 0) {//排除干扰因素if (z < 0) {de.eulerAngles = new Vec3();} else {//翻180度de.eulerAngles = new Vec3(0, 180, 0);}} else {const x = this._pointB.x - this._pointA.x;if (x > 0) {de.eulerAngles = new Vec3(0, 270, 0);} else {de.eulerAngles = new Vec3(0, 90, 0);}}}

注册运动事件

  • GameCtrl.ts里添加
    public start() {(Node.EventType.TOUCH_START, this._touchStart, this);(Node.EventType.TOUCH_END, this._touchEnd, this);}private _touchStart() {lMoving();}private _touchEnd() {lMoving(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.de.worldPosition);//朝向哪就往哪运动switch (this._veType) {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;}de.setWorldPosition(this._offset);}}
  • 运行,点下屏幕,此时小车已经可以向前运动起来啦!

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

  • 在外部添加一个中间变量
const _tempVec = new Vec3();
  • 改写update
    public update(dt: number) {if (this._isMoving) {this._offset.de.worldPosition);//朝向哪就往哪运动switch (this._veType) {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;}de.setWorldPosition(this._offset);Vec3.subtract(_tempVec, this._pointB, this._offset);if (_tempVec.length() <= 0.01) {this._arrivalStation();}}}
  • 添加到站函数,更新当前点和下一个点。
    private _arrivalStation() {console.log(&#");this._pointA.set(this._pointB);this._currRoadPoint = this._Component(RoadPoint);if (this._Station) {this._pointB.set(this._Station.worldPosition)} else {this._isMoving = false;this._currRoadPoint = null;}}
  • 运行看看有无打印

让小车可以弯道行驶

  • 改写到达站点的函数,判断它要弯道行驶时,求初始旋转角度originRotation、最终旋转角度targetRotation以及中心点centerPoint。
  • centerPoint实际上就是取点A到点B的中心点,也就是去A的x和B的z,也可能是B的x和A的z,这个情况需要自己去画图分析。
  • 下图介绍了顺时针时,该取谁的x和谁的z
    private _arrivalStation() {console.log(&#");this._pointA.set(this._pointB);this._currRoadPoint = this._Component(RoadPoint);if (this._Station) {this._pointB.set(this._Station.worldPosition)if (this._veType === RoadPoint.RoadMoveType.BEND) {if (this._currRoadPoint.clockwise) {//判断顺时针或逆时针this._originRotation = this._de.eulerAngles.y);this._targetRotation = this._originRotation - 90;//求顺时针centerPointif ((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._de.eulerAngles.y);this._targetRotation = this._originRotation + 90;//求逆时针centerPointif ((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._veType) {case RoadPoint.RoadMoveType.BEND:const offsetRotation = this._targetRotation - this._originRotation;const currRotation = this._de.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);de.eulerAngles = _tempVec;const sin = Math.sin(nextStation * Math.PI / 180);//通过角度求出弧度const cos = s(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;
  • 其中要运用到矩阵计算公式中的旋转公式,来计算小车的最终坐标。
  • 编辑一下路线,注意转弯的节点必须要精确。设置从A点转弯到B点时,需要两个点刚好位于角度为90度的圆弧上,否则将无法识别。
  • 通过上面的代码也可以理解这一点。
  • 下面这个示意图也许不标准,但大家理解代码并在编辑器里尝试一下就可以解决这个小问题。
  • 如果都能识别上,小车的道路应该可以以“直-弯-直”行驶,也可以行驶更长的路径(只要你一直正确地设置下去)
  • 我设置了一个如下所示的路径:
  • 运行一下:
  • 但我数学不好啊,我哪会矩阵乘法,用矩阵算旋转太麻烦了,就没个API吗?
  • 当然有!一行代码就能代替上面注释掉的这么多行,真香啊
// const sin = Math.sin(nextStation * Math.PI / 180);//通过角度求出弧度
// const cos = s(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
ateY(this._offset, this._pointA, this._centerPoint, nextStation * Math.PI / 180);
  • 运行一下,也是一样的可以转得很丝滑!

小车加速度

  • 定义一个加速度,并将之前设置的匀速运动改为0,在设置一个最大限速
    @propertymaxSpeed = 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;}
  • 运行一下看看小车能否加速和刹车了

完整的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 {@propertymaxSpeed = 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.de.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._veType) {case RoadPoint.RoadMoveType.BEND:const offsetRotation = this._targetRotation - this._originRotation;const currRotation = this._de.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);de.eulerAngles = _tempVec;//绕轴旋转的ateY(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;}de.setWorldPosition(this._offset);Vec3.subtract(_tempVec, this._pointB, this._offset);if (_tempVec.length() <= 0.01) {this._arrivalStation();}}}/**根据这个点设置自己的位置 */public setEntry(entry: Node) {de.setWorldPosition(entry.worldPosition);this._currRoadPoint = Component(RoadPoint);if (!this._currRoadPoint) {console.warn("There is no RoadPoint in ", entry.name);return;}this._pointA.set(entry.worldPosition);this._pointB.set(this._Station.worldPosition);//计算朝向const z = this._pointB.z - this._pointA.z;if (z !== 0) {//排除干扰因素if (z < 0) {de.eulerAngles = new Vec3();} else {//翻180度de.eulerAngles = new Vec3(0, 180, 0);}} else {const x = this._pointB.x - this._pointA.x;if (x > 0) {de.eulerAngles = new Vec3(0, 270, 0);} else {de.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(&#");this._pointA.set(this._pointB);this._currRoadPoint = this._Component(RoadPoint);if (this._Station) {this._pointB.set(this._Station.worldPosition)if (this._veType === RoadPoint.RoadMoveType.BEND) {if (this._currRoadPoint.clockwise) {//判断顺时针或逆时针this._originRotation = this._de.eulerAngles.y);this._targetRotation = this._originRotation - 90;//求顺时针centerPointif ((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._de.eulerAngles.y);this._targetRotation = this._originRotation + 90;//求逆时针centerPointif ((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模型加入场景中,并更改名字

  • 新建CustomerManager脚本并挂在customerManger节点上

改写Car.ts

  • 在Car.ts里派发接客和送客的事件
  • 并且定义一个变量,判断现在是否在订单中。在订单中,也就是乘客正在走动,车是不能动的。
    /**接客 */private _greetingCustomer() {this._isInOrder = true;CustomEventListener.dispatchEvent(EventName.GREETING, de.worldPosition, this._currRoadPoint.direction);}/**送客 */private _takingCustomer() {this._isInOrder = true;CustomEventListener.dispatchEvent(EventName.GOODBYE, de.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 {@propertymaxSpeed = 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 {(EventName.FINISHEDWALK, this.finishedWalk, this);}public update(dt: number) {if (!this._isMoving || this._isInOrder) return;this._offset.de.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._veType) {case RoadPoint.RoadMoveType.BEND:const offsetRotation = this._targetRotation - this._originRotation;const currRotation = this._de.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);de.eulerAngles = _tempVec;//绕轴旋转的ateY(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;}de.setWorldPosition(this._offset);Vec3.subtract(_tempVec, this._pointB, this._offset);if (_tempVec.length() <= 0.01) {this._arrivalStation();}}/**根据这个点设置自己的位置 */public setEntry(entry: Node, isMain = false) {de.setWorldPosition(entry.worldPosition);this._currRoadPoint = Component(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._Station.worldPosition);//计算朝向const z = this._pointB.z - this._pointA.z;if (z !== 0) {//排除干扰因素if (z < 0) {de.eulerAngles = new Vec3();} else {//翻180度de.eulerAngles = new Vec3(0, 180, 0);}} else {const x = this._pointB.x - this._pointA.x;if (x > 0) {de.eulerAngles = new Vec3(0, 270, 0);} else {de.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(&#");this._pointA.set(this._pointB);this._currRoadPoint = this._Component(RoadPoint);if (this._Station) {this._pointB.set(this._Station.worldPosition)//假如是玩家小车并且是接客点或者送客点if (this._isMainCar) {if (this._pe === RoadPoint.RoadPointType.GREETING) {this._greetingCustomer();}else if (this._pe === RoadPoint.RoadPointType.GOODBYE) {this._takingCustomer();}}if (this._veType === RoadPoint.RoadMoveType.BEND) {if (this._currRoadPoint.clockwise) {//判断顺时针或逆时针this._originRotation = this._de.eulerAngles.y);this._targetRotation = this._originRotation - 90;//求顺时针centerPointif ((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._de.eulerAngles.y);this._targetRotation = this._originRotation + 90;//求逆时针centerPointif ((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, de.worldPosition, this._currRoadPoint.direction);}/**送客 */private _takingCustomer() {this._isInOrder = true;CustomEventListener.dispatchEvent(EventName.GOODBYE, de.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[] = [];@propertywalkTime = 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 {(EventName.GREETING, this._greetingCustomer, this);(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._Component(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._Component(AnimationComponent);animComp.play('walk');}
}
  • 运行一下

本文发布于:2024-01-31 19:34:01,感谢您对本站的认可!

本文链接:https://www.4u4v.net/it/170670084330855.html

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。

留言与评论(共有 0 条评论)
   
验证码:

Copyright ©2019-2022 Comsenz Inc.Powered by ©

网站地图1 网站地图2 网站地图3 网站地图4 网站地图5 网站地图6 网站地图7 网站地图8 网站地图9 网站地图10 网站地图11 网站地图12 网站地图13 网站地图14 网站地图15 网站地图16 网站地图17 网站地图18 网站地图19 网站地图20 网站地图21 网站地图22/a> 网站地图23