最近有位网友咨询关于放置类策略游戏的逻辑(这里的放置不是指挂机,而是自走),就是角色自动行走攻击的那种,网上找了一圈相关资源还挺少的,于是花了一个钟撸了一个最简单的demo,效果如下:
1.对手召唤剑士,向我方主城移动
2.我方点击“召唤剑士”按钮,召唤出我方剑士(我方剑士3点HP,敌方2点HP)
3.当对方的剑士被消灭后,我方的剑士会继续前进,直至攻击敌方主城
4.敌方主城HP归零后,游戏结束(敌方主城3点HP)
在开发前,首先需要注意的是,如果是个人开发者(个人主体)要上线此类微信小游戏,注册小程序后选择的分类建议选择“休闲-塔防”类,因为严格来说这种放置自走的游戏属于策略类,而策略类的一级分类是角色类,个人是禁止上线角色类的(另两个是捕鱼和棋牌),别辛辛苦苦开发的游戏上不了线哦…另外虽然设置了塔防类,但审核人员还是会根据游戏的实际内容修改你的分类的,如果他们觉得你是角色类,那逃不掉的,可以考虑申请软著上线字节等其他平台。
言归正传,看看代码:
首先我们新建项目,横屏,因为用到了碰撞体判断范围内是否有敌人,所以需要设置分组,这里设置了我方mine和敌方enemy两个分组,不同的分组会互相碰撞
然后场景的节点如下,我方主城和敌方主城挂载碰撞组件,敌方主城的TAG设置为1:
我方剑士和敌方剑士,做成预制体,用于动态生成,剑士的TAG设置为0,这样在碰撞后可以知道自己攻击的是敌方剑士还是主城
剑士的动画准备两个,一个是移动(一跳一跳),另一个是攻击(挥剑)
移动的动画,就是设置节点中三张图片(身体,手,武器)的position,Y坐标从0到10再到0即可,这个动画我们设置为循环播放
攻击的动画,就是设置武器图片的旋转,从0到-88度再到0,这个设置只播放一次(可以看到demo里剑士攻击的时候身体还在空中,是因为攻击动画中我忘记把body的position的Y设置为0了,这边设置一下就行)
节点都设置完毕了,代码的话总共有三份,一份是全局脚本,挂载在DemoManage上,控制游戏的开始与结束等,另两份是剑士的脚本(我方剑士和敌方剑士),分别挂载在上面两个预制体上
全局脚本:
import { _decorator, Collider2D, Component, Contact2DType, instantiate, Node, Prefab, resources, Tween, UITransform } from 'cc';
const { ccclass, property } = _decorator;
@ccclass('ScriptDemoManage')
export class ScriptDemoManage extends Component {
//定义我方主城的血量,3为被打三下就游戏结束
public myhomeHp = 3
//定义敌方主城的血量,3为被打三下就游戏结束
public enemyhomeHp = 3
start() {
//NodeSwordmanEnemy是敌方剑士,默认血量2点,这里在游戏开始时动态生成一个,如果有需要也可以随机生成多个
resources.load("prefab/NodeSwordmanEnemy", Prefab, (err, prefeb) => {
var nodeSwordmanEnemy = instantiate(prefeb);
//把这个剑士挂载到SpriteBG节点中
nodeSwordmanEnemy.parent = this.node.parent.getChildByPath('Canvas/SpriteBG')
nodeSwordmanEnemy.active = true
})
}
update(deltaTime: number) {
}
homeAttacked() {
//我方主城被攻击
this.myhomeHp--;
if(this.myhomeHp <= 0){
//我方主城血量为0,游戏结束
Tween.stopAll()
this.node.parent.getChildByPath('Canvas/NodeGameOver').active = true
}
}
enemyhomeAttacked() {
//敌方主城被攻击
this.enemyhomeHp--;
if(this.enemyhomeHp <= 0){
//敌方主城血量为0,游戏结束
Tween.stopAll()
this.node.parent.getChildByPath('Canvas/NodeGameOver').active = true
}
}
btnSendSwordmanClick(){
//玩家点击了“召唤剑士”按钮,动态生成一个我方剑士,嘎嘎乱杀,我方剑士默认血量为3点,在ScriptSwordman脚本中能找到
resources.load("prefab/NodeSwordman", Prefab, (err, prefeb) => {
var nodeSwordman = instantiate(prefeb);
nodeSwordman.parent = this.node.parent.getChildByPath('Canvas/SpriteBG')
nodeSwordman.active = true
})
}
}
我方剑士脚本:
import { _decorator, Collider2D, Component, Contact2DType, IPhysics2DContact, Node, tween, Vec3, Animation, PhysicsSystem2D, Tween } from 'cc';
import { ScriptSwordmanEnemy } from './ScriptSwordmanEnemy';
import { ScriptDemoManage } from './ScriptDemoManage';
const { ccclass, property } = _decorator;
@ccclass('ScriptSwordman')
export class ScriptSwordman extends Component {
//初始化我方剑士的血量为3点
public hp: number = 3;
start() {
//监听碰撞体事件,这里只监听onBeginContact,也就是第一次接触
let collider = this.getComponent(Collider2D);
if (collider) {
collider.on(Contact2DType.BEGIN_CONTACT, this.onBeginContact, this);
}
//开始移动,我方剑士往右边移动
this.startMoving()
}
update(deltaTime: number) {
}
startMoving() {
//匀速朝右边移动
let tweenDuration: number = 10.0;
tween(this.node)
.to(tweenDuration, { position: new Vec3(this.node.position.x + 1200, -90, 0) })
.start();
//显示动画,一跳一跳的,动画设置的是循环播放
const animationComponent = this.node.getComponent(Animation);
animationComponent.play('effectMove')
}
onBeginContact(selfCollider: Collider2D, otherCollider: Collider2D, contact: IPhysics2DContact | null) {
//只在两个碰撞体开始接触时被调用一次
let _this = this
//停止该节点的所有缓动,目的是让剑士停下来别走了,准备干架
Tween.stopAllByTarget(this.node);
//动画也停止,别一跳一跳了
const animationComponent = this.node.getComponent(Animation);
animationComponent.stop()
//判断碰撞体的tag,这个tag是在目标node的组件里,0表示剑士,1表示主城,如果后续有其他兵种,记得设置不同的tag方便区分
if (otherCollider.tag == 0) {
//定义一个回调函数,用来循环执行攻击指令
let callback = function () {
//如果对手已经挂了,停止攻击,继续往前走
if (!otherCollider.node) {
_this.unschedule(callback);
_this.startMoving()
return
}
//显示攻击动画
const animationComponent = _this.node.getComponent(Animation);
animationComponent.play('effectAttack')
//获得敌方节点挂载的脚本,以便调用BeAttacked扣除它的血量
const enemy = otherCollider.node.getComponent(ScriptSwordmanEnemy)
enemy.BeAttacked()
//判断血量为0,直接往前走,不用等一个循环了,但是上面的判断也需要因为可能存在并发,也就是两个剑士同时攻击一个目标
if (enemy.hp <= 0) {
_this.scheduleOnce(() => {
_this.startMoving()
}, 0.5)
return
}
}
//见面立即攻击一次,因为schedule不会立刻执行
callback()
//设置循环,2秒攻击一次
this.schedule(callback, 2);
}
else if (otherCollider.tag == 1) {
//tag==1表示攻击的是敌方主城
let callback = function () {
if (!otherCollider.node) {
_this.unschedule(callback);
return
}
//显示攻击动画
const animationComponent = _this.node.getComponent(Animation);
animationComponent.play('effectAttack')
//主城没有单独挂载脚本,是在DemoManage节点统一处理的,所以这边找到DemoManage的脚本组件触发enemyhomeAttacked方法
_this.node.parent.parent.parent.getChildByName('DemoManage').getComponent(ScriptDemoManage).enemyhomeAttacked()
}
callback()
this.schedule(callback, 2);
}
}
BeAttacked() {
//我方被攻击
this.hp--
if (this.hp <= 0) {
//没血了,0.2秒(挥剑动作正好碰到)后销毁当前节点
this.scheduleOnce(() => {
this.node.destroy()
}, 0.2)
}
}
}
敌方剑士脚本:
import { _decorator, Collider2D, Component, Contact2DType, IPhysics2DContact, Node, PhysicsSystem2D, tween, Vec3, Animation, Tween } from 'cc';
import { ScriptSwordman } from './ScriptSwordman';
import { ScriptDemoManage } from './ScriptDemoManage';
const { ccclass, property } = _decorator;
@ccclass('ScriptSwordmanEnemy')
export class ScriptSwordmanEnemy extends Component {
//初始化敌方剑士的血量为2点,比我方少一点
public hp: number = 2;
//脚本的逻辑和ScriptSwordman基本一致,只是换了目标,详细注释请阅读ScriptSwordman里面的注释
//也可以只用一份脚本,根据变量区分敌我,这边是偷懒了copy了一份,违反了DRY原则,管它呢
start() {
let collider = this.getComponent(Collider2D);
if (collider) {
collider.on(Contact2DType.BEGIN_CONTACT, this.onBeginContact, this);
}
this.startMoving()
}
update(deltaTime: number) {
}
startMoving() {
//匀速朝左边移动
let tweenDuration: number = 10.0;
tween(this.node)
.to(tweenDuration, { position: new Vec3(this.node.position.x - 1200, -90, 0) })
.start();
const animationComponent = this.node.getComponent(Animation);
animationComponent.play('effectMove')
}
onBeginContact(selfCollider: Collider2D, otherCollider: Collider2D, contact: IPhysics2DContact | null) {
// 只在两个碰撞体开始接触时被调用一次
let _this = this
Tween.stopAllByTarget(this.node);
const animationComponent = this.node.getComponent(Animation);
animationComponent.stop()
if (otherCollider.tag == 0) {
let callback = function () {
if (!otherCollider.node) {
_this.unschedule(callback);
_this.startMoving()
return
}
const animationComponent = _this.node.getComponent(Animation);
animationComponent.play('effectAttack')
const mine = otherCollider.node.getComponent(ScriptSwordman)
mine.BeAttacked()
if (mine.hp <= 0) {
_this.scheduleOnce(() => {
_this.startMoving()
}, 0.5)
return
}
}
callback()
this.schedule(callback, 2);
}
else if (otherCollider.tag == 1) {
let callback = function () {
if (!otherCollider.node) {
_this.unschedule(callback);
return
}
const animationComponent = _this.node.getComponent(Animation);
animationComponent.play('effectAttack')
_this.node.parent.parent.parent.getChildByName('DemoManage').getComponent(ScriptDemoManage).homeAttacked()
}
callback()
this.schedule(callback, 2);
}
}
BeAttacked() {
this.hp--
if (this.hp <= 0) {
this.scheduleOnce(() => {
this.node.destroy()
}, 0.2)
}
}
}
以上,素材用的均是kenney(https://kenney.nl/assets)的CC0资源,逻辑很简单仅供参考,需要此demo源码打包的可私信,随便写的有BUG请自行修复啊哈哈哈