引言
本文很长,建议收藏食用。
课程来源:
游戏开发教程 | 零基础也可以用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吧。
-
下好后是这样的:
-
下载时要往下滑,滑到最底下找到废弃的版本。
新建项目
-
在右下角新建项目
-
记得修改项目名字,叫Taxi或者什么,随意。
-
新建完成后,我们就得到了一个空的项目
导入资源
- 上这个网站把资源下载下来。
资源下载:https://github.com/cocos-creator/tutorial-taxi-game - 找到这个文件夹:
- 打开,把里面的东西全部拖到引擎内的资源管理器下的asserts文件夹下。
- 等待资源导入,观察控制台,无报错就算成功。
搭建场景
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) {
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节点上。
- 删掉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) {
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小车的路径,随意摆放一下。注意复制的时候需要重新绑定一下节点喔~
游戏逻辑
1. 游戏逻辑梳理
根据游戏逻辑,可以整理出游戏中包含的几个大类:界面(UI)、音频资源、特效、车(玩家车、AI车)、乘客、关卡(地图)。
- 点击开始界面游戏开始,此时切换到游戏界面,并播放背景音乐
- 点按屏幕任意处小车加速,松开刹车并根据当前车速播放刹车特效和刹车音效
- 到达指定地点接送乘客,播放乘客动画并更新相对应界面订单进度
- 第一次接乘客的时候,触发关卡内A小车运作
- 送乘客时播放金币奖励特效和音效
- 到达终点或者出车祸游戏结束,进入结算界面,出车祸不予以金币奖励
- 结算界面点击领取后更新自身拥有金币数量并回到主界面
对于游戏的几个大类,我们应当对其设置不同的管理类: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 = 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
在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);
}
}
-
把相机摆放在小车上面,并调整一个合适的角度。
-
这里就随便调调,合适就行。
-
运行看看:
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);
}
}
- 运行,点下屏幕,此时小车已经可以向前运动起来啦!
继续改写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;
}
}
- 运行看看有无打印
让小车可以弯道行驶
- 改写到达站点的函数,判断它要弯道行驶时,求初始旋转角度originRotation、最终旋转角度targetRotation以及中心点centerPoint。
- centerPoint实际上就是取点A到点B的中心点,也就是去A的x和B的z,也可能是B的x和A的z,这个情况需要自己去画图分析。
- 下图介绍了顺时针时,该取谁的x和谁的z
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;
- 其中要运用到矩阵计算公式中的旋转公式,来计算小车的最终坐标。
- 编辑一下路线,注意转弯的节点必须要精确。设置从A点转弯到B点时,需要两个点刚好位于角度为90度的圆弧上,否则将无法识别。
- 通过上面的代码也可以理解这一点。
- 下面这个示意图也许不标准,但大家理解代码并在编辑器里尝试一下就可以解决这个小问题。
- 如果都能识别上,小车的道路应该可以以“直-弯-直”行驶,也可以行驶更长的路径(只要你一直正确地设置下去)
- 我设置了一个如下所示的路径:
- 运行一下:
- 但我数学不好啊,我哪会矩阵乘法,用矩阵算旋转太麻烦了,就没个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;
}
- 运行一下看看小车能否加速和刹车了
完整的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模型加入场景中,并更改名字
文章来源:https://www.toymoban.com/news/detail-433354.html -
新建CustomerManager脚本并挂在customerManger节点上
文章来源地址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出租车小游戏的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!