全套源码丨Cocos Creator 轻松玩转 3D 策略对战游戏,创意拉满!
又一款 Cocos Demo Team 的作品《》上线了!《疯狂打群架》是一款 3D 策略对战小游戏,具有丰富的关卡、兵种等元素,项目完整开源,含工程源码、美术资源、策划文档,支持 Cocos Creator 3.3.2,点击文末【阅读原文】即可跳转下载地址。
游戏开始前,玩家首先通过「战力表」的指针悬停去获得不同等级的战力加成。积累的金币可以用来购买土地和士兵,合成同等战力的士兵能提升士兵等级,合理的排兵布阵将会增强整体战力。战斗过程中还可以使用火球攻击,给我方队伍开个外挂。
接下来根据游戏玩法,来看看《疯狂打群架》的技术实现亮点。
3D 对象的移动
在游戏中,玩家可以长按拖拽士兵将其移动,进行不同格子之间的放置、交换或合并。那么,在开发中要怎么去实现通过触发屏幕操作 3D 对象呢?
1、事件监听
首先需要对屏幕的触摸范围进行事件监听,监听触摸的开始、移动、结束三种状态。
2、射线检测
射线检测是判断玩家是否触摸到 3D 对象的关键。先用相机构造一条从相机原点到屏幕触摸点发射出的射线:
const outRay = new ray();
cameraCom.screenPointToRay(pos.x, pos.y, outRay);
然后通过射线检测,检测与 3D 格子是否有碰撞:
let isCheck = PhysicsSystem.instance.raycastClosest(this._outRay1, Constants.COLLIDER_GROUP.PLANE, 50, false);
寻找敌方士兵
当游戏开始后,我方士兵会朝敌方士兵移动,移动到攻击距离之内后便会向敌方发起攻击。这里就需要对我方与敌方的相隔距离进行判断:
//攻击方与被攻击方的距离是否小于攻击距离
if (offset.length() - this.target.radius - this.radius <= this._attackRange) {
this._direction.set(offset);
this._direction.normalize().negative();
this._forward.set(this._direction);
this.node.forward = this._forward;
this._moveTime = 0;
//可以攻击
if (this._coolTime <= 0) {
//发起攻击
this._attack();
} else {
this._coolTime -= deltaTime;
}
} else if (this._isAttacking) {
//正在攻击,不要停
this._attackAniTime += deltaTime;
if (this._attackAniTime > 2) {
this._isAttacking = false;
}
} else {
//接着移动
...
}
远程武器攻击
游戏中的士兵分为近战兵和远程兵。远程兵通过远程投掷武器进行攻击,武器与士兵是分开存在的。那么游戏是怎样将这两者完美的结合起来的呢?
1、投掷时机
动画事件。我们可以采用给角色的动画添加事件的形式来决定武器投掷的时间)
事件监听
//加载士兵对应模型
ResourceUtil.getFighterModel(this.fighterInfo.type, (err: any, pf: Prefab)=>{
if (err) {
console.error('get fighter model failed!: ', err);
return;
}
this._nodeModel = PoolManager.instance.getNode(pf, this.ndModelParent);
this._fighterModel = this._nodeModel.getComponent(FighterModel) as FighterModel;
this._fighterModel.show(this);
//注册监听事件
this._fighterModel.setEffectListener(this._triggerEffect, this);
});
2、武器飞行
武器投掷出去后,根据与被攻击方之间的距离计算出飞行所需的总时长和武器飞行的最大高度,飞行路径为一个半圆。同时结合余弦曲线图正好可以满足 -pi/2 到 pi/2 的余弦值。
//百分比
let percent = Number((this.currentTime / this.totalTime).toFixed(2));
//目标高度
let height = this.maxHeight * Math.cos(percent * Math.PI - Math.PI / 2);
//设置目标位置
this._targetPos.set(this.posStart.x + this.posOffset.x * percent, this.posStart.y + height, this.posStart.z + this.posOffset.z * percent);
//设置武器位置
this.node.setWorldPosition(this._targetPos);
火球技能攻击
游戏中还加入了一个额外的「火球」攻击技能。玩家通过手动触发,从天而降的火球将对敌方造成一定的伤害。使用时会先对比敌方远程和近战士兵人数,哪一类士兵较多,炸弹的降落点就会设置在哪一类的中央,目的是给予敌方最大打击。
/**
* 使用火球技能
*/
private _useFireBall () {
let sumClose = 0;//近战士兵数量
let sumLong = 0;//远程士兵数量
let closePosX = 0, closePosY = 0;//近战士兵x、y的值
let longPosX = 0, longPosY = 0;//远程士兵x、y的值
for (let team in this.dictFighter) {...
}
//哪种兵种多就打谁,炸弹设置在敌人中心点
if (sumClose > 0) {
this._ballPos.set(closePosX / sumClose, 0, closePosY / sumClose);
} else if (sumLong > 0) {
this._ballPos.set(longPosX / sumLong, 0, longPosY / sumLong);
}
//火球伤害面积
let disSqr = Constants.FIRE_BALL_DISTANCE * Constants.FIRE_BALL_DISTANCE;
this.effectGroup.playFireBallEffect(this._ballPos, ()=>{...
});
}
摄像机跟随
战斗中每隔0.5秒计算所有士兵的区域边界,然后对摄像机的位置进行动态适配(follow.ts)。此处使用 lerp 方法让相机位置和视角平滑过渡:
Vec3.lerp(this._nextPos, this.node.position, this._targetPos, 1 * deltaTime);
this.node.position = this._nextPos;
if (this._targetHeight > 0) {
let height = lerp(this.camera.fov, this._targetHeight, 1 * deltaTime);
this.camera.fov = height;
}
box2d
box2d 相信大家再熟悉不过了,这是 2D 物理游戏开发中最常用的一个碰撞引擎。我们要如何脱离已有的组件使用 box2d 呢?
1、物理世界-b2World
简单理解就是 box2d 世界,是所有 box2d 物体所依托的大环境:
实例化物理世界
//@ts-ignore 设置重力
const gravity = box2d.b2Vec2.ZERO;
//@ts-ignore 物理世界
const world = new box2d.b2World(gravity);
2、刚体-b2Body
所有物理世界中的刚体(也称为物体)都需要通过 b2BodyDef 去定义,然后设置基本的类型和状态。通过 CreateBody 将定义的刚体添加到物理世界中。
//@ts-ignore
const bodyDef = new box2d.b2BodyDef();
//@ts-ignore 动态刚体
bodyDef.type = box2d.b2BodyType.b2_dynamicBody;
//不允许休眠
bodyDef.allowSleep = false;
//禁止刚体旋转
bodyDef.fixedRotation = true;
//将刚体添加到物理世界
this._b2Body = world.CreateBody(this._bodyDef);
3、材质-b2FixtureDef
记录物体的表面信息:密度,摩擦力,弹力。
var fixDef = new b2FixtureDef();
fixDef.density = 1.0; // desity 密度,如果密度为0或者null,该物体则为一个静止对象
fixDef.friction = 0.5; //摩擦力(0~1)
fixDef.restitution = 0.2;// 弹性(0~1)
4、形状-b2Shape
在 box2d 中,支持的刚体形状大概有 b2CircleShape 圆外形、b2EdgeChainDef 边缘图形、b2MassData 质量运算器、b2PolygonShape 凸多边形等。
//@ts-ignore 标准圆,参数为半径,后续需要做转换
const circle = new box2d.b2CircleShape(radius / Constants.PTM_RATIO);
//@ts-ignore
//将形状指定给材质
fixDef.shape = circle;
//为刚体设置材质
this._fixture = this._b2Body.CreateFixture(fixtureDef, 0);
5、位置和角度
通过以上4步,我们已经创建好了一个圆形的刚体。刚体的类型是动态的,也就是说他的位置和角度会发生变化,我们可以通过代码去控制:
/**
* 设置士兵节点和对应刚体的世界坐标
* @param pos
*/
public setWorldPosition (pos: Vec3) {
let posBody = {x: pos.x / Constants.PTM_RATIO, y: pos.z / Constants.PTM_RATIO};
if (this._b2Body) {
this._b2Body.SetPosition(posBody);
}
this.node.setPosition(pos);
}
/**
* 获取刚体的世界坐标
* @returns
*/
private _getWorldPosition () {
let out = {x: 0, y: 0};
if (this._b2Body) {
let pos = this._b2Body.GetPosition();
out.x = pos.x * Constants.PTM_RATIO;
out.y = pos.y * Constants.PTM_RATIO;
}
return out;
}
一般情况下刚体应该和我们场景中的某个节点是处于同步状态的,所以我们需要通过刚体的属性对指定节点进行刷新:
update (deltaTime: number) {
// Your update function goes here.
//每帧去同步数据过来
if (this._b2Body && this.enableSync) {
let pos = this._getWorldPosition();
let posNode = this.node.position;
//同步士兵节点位置
if (pos.x !== posNode.x || pos.y !== posNode.z) {
this.node.setPosition(pos.x, 0, pos.y);
}
}
}
以上是《疯狂打群架》项目中部分技术亮点,希望能给大家带来帮助!点击文末【阅读原文】至 Cocos Store 或前往 限时 ¥9.9 抢购,下载完整源码。
Cocos Store 圣诞惊喜来啦!
12月23日-12月25日
官方源码任选2款99元
扫描下方二维码进入专区↓↓↓
参与抽奖100%赢好礼
百元插件源码大礼包和新款周边
等你来拿↓↓↓
自研源码 list
2D 消除《天天消一消》
https://store.cocos.com/app/detail/2905
往期精彩