先看效果
上面视频来自近期的爆款游戏向僵尸开炮
,本章就来实现下视频中的制导激光的效果。
激光射线的实现原理
激光射线的实现基于几个关键的几何和物理概念,主要包括向量的计算和图形的渲染。以下是构建激光射线的几个基本步骤:
1. 计算方向向量
激光射线的方向由起点(如防御塔)和终点(如僵尸)之间的向量决定。这个向量可以通过终点坐标减去起点坐标得到,表示为 𝑣⃗=𝑏⃗−𝑎⃗v=b−a,其中 𝑎⃗a 是起点坐标,𝑏⃗b 是终点坐标。
2. 向量归一化
为了仅获取方向而不考虑距离,需要对方向向量进行归一化处理。归一化后的向量具有单位长度,可通过 𝑢⃗=𝑣⃗∣𝑣⃗∣u=∣v∣v 计算得出,其中 ∣𝑣⃗∣∣v∣ 是向量的模长。
3. 计算角度
激光需要旋转到指向目标的正确角度。这个角度可以通过计算方向向量的反正切得到,即 𝜃=atan2(𝑣𝑦,𝑣𝑥)θ=atan2(vy,vx),其中 𝑣𝑦vy 和 𝑣𝑥vx 分别是向量的 y 和 x 分量。
整体实现思路
-
激光和敌人的组件化设计:
- 敌人管理 :创建一个
EnemyManager
类,负责管理所有敌人的生成、位置更新及状态管理。 - 激光管理 :设计一个
LaserManager
类,控制激光的生成、方向调整、状态更新及与敌人的交互。
- 敌人管理 :创建一个
-
基于向量的激光指向系统:
- 使用向量计算来确定从激光发射装置到当前目标的直线路径。
- 动态更新激光的长度和角度,以保证激光始终指向最近的敌人。
-
游戏主控制逻辑:
- 在
Main
类中实现游戏的主循环,定期检查和更新敌人的位置及激光的状态。 - 使用事件和回调函数来处理游戏中的交互,如敌人被消灭时的逻辑处理。
- 在
详细实现过程
先准备一张可以应用九宫格的激光图块
和一个用于展示激光打到小怪后的效果图块
将二者做成预制体
预制体做成这样的好处:
- 只需要设置整个预制节点的朝向,无需确定
light
节点的朝向 - 激光射线
light
节点只要设置动态设置宽度(为何不是高度,因为激光原图是水平朝向,这里旋转了90°) - 红点
pointEnd
节点只要动态设置高度
在预制体上挂上组件Laser
,代码如下:
typescript
import { _decorator, Component, math, Node, tween, UITransform, v3, Vec2, Vec3 } from 'cc';
import { Enemy } from './Enemy';
const { ccclass, property } = _decorator;
@ccclass('Laser')
export class Laser extends Component {
@property(Node)
pointStart: Node = null;
@property(Node)
pointEnd: Node = null;
@property(Node)
light: Node = null;
/** 激光射线长度 */
private _laserLength: number = 0;
private _lightTransform: UITransform = null;
/** 攻击对象 */
private _target: Node = null;
public get target() {
return this._target;
}
/** 攻击对象死亡回调 */
private _attackFinishCallback: Function = null;
public setTarget(target: Node, attackFinishCallback: Function) {
this._target = target;
this._attackFinishCallback = attackFinishCallback;
this.schedule(this.attack, 0.3);
this.attack();
}
onLoad() {
this.pointStart.active = false;
this._lightTransform = this.light.getComponent(UITransform);
this.lightAction();
this.pointEndAction();
}
/** 激光射线闪烁动画 */
lightAction () {
tween(this.light)
.sequence(
tween().to(0.5, { scale: v3(1.0, 1.5, 1)}),
tween().to(0.5, { scale: v3(1.0, 1, 1)}),
)
.repeatForever()
.start();
}
/** 攻击点闪烁动画 */
pointEndAction () {
tween(this.pointEnd)
.sequence(
tween().to(0.3, { scale: v3(1.0, 1.0, 1.0)}),
tween().to(0.3, { scale: v3(0.5, 0.5, 0.5)}),
)
.repeatForever()
.start();
}
/** 设置激光长度 */
setupSprite(p1: Vec3, p2: Vec3): void {
// 计算 p1 和 p2 之间的向量
let direction = p2.clone().subtract(p1);
// 计算两点之间的距离
let length = direction.length();
this._laserLength = length;
this._lightTransform.width = this._laserLength;
this.node.getComponent(UITransform).width = length;
// 设置攻击点的位置,这里只要设置高度就行
this.pointEnd.setPosition(v3(0, this._laserLength, 0));
}
/** 攻击 */
attack() {
if (!this._target) return;
const hp = this._target.getComponent(Enemy).beAttacked(50);
// 死亡后,从数组从删除
if (hp <= 0) {
this._attackFinishCallback && this._attackFinishCallback(this.target);
this.unschedule(this.attack);
this._target = null;
}
}
}
组件Laser
中主要实现激光射线的展示和攻击效果
接着添加小怪预制体
在小怪预制体挂上组件Enemy
:
typescript
import { _decorator, Color, Component, Node, Skeleton, sp, tween } from 'cc';
const { ccclass, property } = _decorator;
@ccclass('Enemy')
export class Enemy extends Component {
@property(Node)
target: Node = null;
private _hp: number = 100;
/** 骨骼 */
private _skeleton: sp.Skeleton = null;
private _isAttacked: boolean = false;
start() {
this._skeleton = this.target.getComponent(sp.Skeleton);
}
/** 颜色闪烁动画 */
colorAction() {
tween(this._skeleton)
.to(0.5, { color: new Color(255, 0, 0, 255) })
.to(0.5, { color: new Color(255, 255, 255, 255) })
.start();
}
/** 被攻击扣血 */
public beAttacked(damage: number): number {
this._hp -= damage;
// 被攻击,变红
if (!this._isAttacked) {
this._isAttacked = true;
this.colorAction();
}
if (this._hp <= 0) {
this.node.destroy();
}
return this._hp;
}
}
Enemy
组件中主要实现了小怪节点被攻击时的闪烁动画和死亡后节点的销毁.
接下来,通过EnemyManager
来控制小怪节点的管理
typescript
import { _decorator, Component, Node, Prefab, instantiate, Vec3, tween, math, Vec2, view, Size } from 'cc';
const { ccclass, property } = _decorator;
@ccclass('EnemyManager')
export class EnemyManager extends Component {
/** 敌人预制体 */
@property(Prefab)
enemyPrefab: Prefab = null;
/** 敌人数组 */
private _enemyArray: Node[] = [];
/** 屏幕尺寸 */
private _screenSize: Size = null;
start() {
this.schedule(this.createEnemies, 0.8);
this._screenSize = view.getVisibleSize();
}
/** 创建敌人 */
createEnemies() {
// 每次随机生成 2 到 4 个敌人
const enemyNums = math.randomRangeInt(2, 5);
const screenW = this._screenSize.width;
const screenH = this._screenSize.height;
for (let i = 0; i < enemyNums; i++) {
let newPos;
let overlap;
// 检测和之前生成的小怪位置是否有重叠,重叠就重新生成
do {
newPos = new Vec3(math.randomRange(-screenW/2 + 100, screenW/2 - 100), this._screenSize.height/2, 0);
overlap = this._enemyArray.some(enemy => {
return math.Vec3.distance(enemy.position, newPos) < 70;
});
} while (overlap);
let enemy = instantiate(this.enemyPrefab);
enemy.parent = this.node;
enemy.setPosition(newPos);
this._enemyArray.push(enemy);
this.moveEnemy(enemy);
}
}
/** 敌人移动 */
private moveEnemy(enemy: Node) {
tween(enemy)
.to(5, { position: new Vec3(enemy.position.x, -this._screenSize.height/2 - 100, 0) })
.start();
}
/** 获取距离指定点最近的敌人 */
getClosestEnemy(startPoint: Vec3): Node {
let minDistance = Number.MAX_VALUE;
let closestEnemy = null;
this._enemyArray.forEach(enemy => {
let enemyWorldPosition = enemy.worldPosition; // 确保使用世界坐标
let distance = math.Vec3.distance(enemyWorldPosition, startPoint);
if (distance < minDistance) {
minDistance = distance;
closestEnemy = enemy;
}
});
return closestEnemy;
}
/** 从数组中删除敌人,主要用于小怪死亡后节点的销毁 */
removeEnemy(enemy: Node) {
const index = this._enemyArray.indexOf(enemy);
if (index > -1) {
this._enemyArray.splice(index, 1);
}
enemy.destroy();
}
}
通过组件LaserManager
来管理激光射线的发射和更新
typescript
import { _decorator, Component, Node, Prefab, instantiate, Vec3, UITransform } from 'cc';
import { Laser } from './Laser';
import { EnemyManager } from './EnemyManager';
const { ccclass, property } = _decorator;
@ccclass('LaserManager')
export class LaserManager extends Component {
/** 激光射线预制体 */
@property(Prefab)
laserPrefab: Prefab = null;
/** 激光射线节点 */
private _laserNode: Node = null;
/** 激光射线组件 */
private _laser: Laser = null;
/** 敌人管理 */
private _enemyManager: EnemyManager = null;
protected onLoad(): void {
this._enemyManager = this.node.getComponent(EnemyManager);
}
/** 发射激光射线 */
fireLaser(startPoint: Vec3, endPoint: Vec3, target: Node) {
// 激光射线不存在,创建激光射线
if (!this._laserNode) {
this._laserNode = instantiate(this.laserPrefab);
this._laserNode.parent = this.node;
this._laserNode.getComponent(UITransform).priority = 2;
this._laser = this._laserNode.getComponent(Laser);
this._laserNode.setWorldPosition(startPoint);
}
// 求出单位向量
const direction = endPoint.clone().subtract(startPoint).normalize();
// 计算两点形成线段的角度,转换为角度制
let angle = Math.atan2(direction.y, direction.x) * (180 / Math.PI);
this._laserNode.setRotationFromEuler(new Vec3(0, 0, angle - 90));
this._laser.setupSprite(startPoint, endPoint);
if (this._laser.target) return;
this._laser.setTarget(target, (enemyNode) => {
// 从数组中删除已死亡的小怪
this._enemyManager.removeEnemy(enemyNode);
});
}
/** 更新激光射线的 位置,长度和角度 */
updateLaserPosition(startPoint: Vec3, endPoint: Vec3) {
if (this._laser) {
this._laser.setupSprite(startPoint, endPoint);
}
}
}
在通过Main
组件来控制整个流程:
typescript
import { _decorator, Component, Node, Prefab, UITransform, v3, Vec3 } from 'cc';
import { EnemyManager } from './EnemyManager';
import { LaserManager } from './LaserManager';
const { ccclass, property } = _decorator;
@ccclass('Main')
export class Main extends Component {
@property(Node)
gun: Node = null;
/** 启动节点 */
private _startPointNode: Node = null;
/** 启动点位置 */
private _startPoint: Vec3 = new Vec3();
/** 敌人管理器 */
private enemyManager: EnemyManager = null;
/** 激光管理器 */
private laserManager: LaserManager = null;
protected onLoad(): void {
this._startPointNode = this.gun.getChildByName('point');
this._startPoint = this._startPointNode.worldPosition;
this.enemyManager = this.node.getComponent(EnemyManager);
this.laserManager = this.node.getComponent(LaserManager);
}
update(dt: number): void {
// 获取最近的敌人
let closestEnemy = this.enemyManager.getClosestEnemy(this._startPoint);
// 发射激光
if (closestEnemy) {
const enemyPos = closestEnemy.worldPosition;
this.laserManager.fireLaser(this._startPoint, enemyPos, closestEnemy);
}
}
}
在场景中设置背景bg
,添加激光发射器gun
和脚本控制script
节点,并在script
节点上挂载Main
、EnemyManager
和LaserManager
。
这样就可以直接运行查看效果:
有兴趣的查看代码:
gitee:gitee.com/dony1122/ga...