版本:2.4.4
之前好几个游戏有用过镜头跟随,现在再总结一下。
实现效果:
一 实现需求
目的就是在人物移动的时候,一直让人物处于视口屏幕中央显示。
例如人物在地图上向右移动200像素,那么人物就会显示在屏幕右边,这时为了让人物居中,就得将地图向左移动200像素,因为人物节点在地图节点下,
右移200和左移200相互抵消,所以人物相当于没动,仍然会保持屏幕居中显示了。
二 视口、地图、人物
视口就是游戏中显示地图的窗口,如果是满屏幕显示,那么视口就是Canvas屏幕节点,例如Canvas节点高宽1280x720。
地图节点就是放地图map的节点,例如地图高宽是2000x2000。
人物节点就是控制的小人,一般是个骨骼动画。
三 摄像机代码
摄像机GameCamera在初始化时传入视口、地图、人物。在人物移动时调用updatePosition刷新摄像机位置。
/** * 游戏摄像机 * @author chenkai 2022.8.29 */ export class GameCamera { /**地图节点 */ private mapNode: cc.Node; /**人物节点 */ private roleNode: cc.Node; /**地图x轴最大移动距离 */ private xRange: number; /**地图y轴最大移动距离 */ private yRange: number; /**上一次人物X位置 */ private lastRoleX: number; /**上一次人物Y位置 */ private lastRoleY: number; /** * 构造函数 * @param viewPortNode 视口节点 例如屏幕大小720x1280 * @param mapNode 地图节点 例如2000x2000 * @param roleNode 人物节点 */ public constructor(viewPortNode: cc.Node, mapNode: cc.Node, roleNode: cc.Node) { //保存节点 this.mapNode = mapNode; this.roleNode = roleNode; //计算x,y轴最大移动距离 if (this.mapNode.width > viewPortNode.width) { this.xRange = (this.mapNode.width - viewPortNode.width) / 2; } else { this.xRange = 0; } if (this.mapNode.height > viewPortNode.height) { this.yRange = (this.mapNode.height - viewPortNode.height) / 2; } else { this.yRange = 0; } //保存人物位置 this.lastRoleX = roleNode.x; this.lastRoleY = roleNode.y; console.log("摄像头最大移动距离:", this.xRange, this.yRange); } /**刷新位置 */ public updatePosition() { //人物未移动,则不需要更新位置 if (this.lastRoleX == this.roleNode.x && this.lastRoleY == this.roleNode.y) { return; } this.lastRoleX = this.roleNode.x; this.lastRoleY = this.roleNode.y //人物和地图中点距离 let distX = this.roleNode.x; let distY = this.roleNode.y; //地图根据距离反向移动,这样人物就能一直处于视口中间 this.mapNode.x = -distX; this.mapNode.y = -distY; //地图边缘检测 if (this.mapNode.x > this.xRange) { this.mapNode.x = this.xRange; console.log("摄像头超过右边界"); } else if (this.mapNode.x < -this.xRange) { this.mapNode.x = -this.xRange; console.log("摄像头超过左边界"); } if (this.mapNode.y > this.yRange) { this.mapNode.y = this.yRange; console.log("摄像头超过上边界"); } else if (this.mapNode.y < -this.yRange) { this.mapNode.y = -this.yRange; console.log("摄像头超过下边界"); } } }
四 主场景代码
用cc.SystemEvent监听键盘上下左右按键,用于移动人物,人物移动后刷新摄像机位置updatePosition让人物居中。
const { ccclass, property } = cc._decorator; /** * 摄像机跟随Demo * @author chenkai 2022.8.29 */ @ccclass export default class MainScene extends cc.Component { @property({ type: cc.Node, tooltip: "地图节点" }) mapNode: cc.Node = null; @property({ type: cc.Node, tooltip: "人物节点" }) roleNode: cc.Node = null; @property({ type: cc.Node, tooltip: "视口" }) viewPort: cc.Node = null; /**摄像机 */ private gameCamera: GameCamera; /**按键缓存 */ private keyCache = {}; /**人物移动速度 */ private roleSpeed: number = 10; onLoad() { this.gameCamera = new GameCamera(this.viewPort, this.mapNode, this.roleNode); cc.systemEvent.on(cc.SystemEvent.EventType.KEY_DOWN, this.onKeyDown, this); cc.systemEvent.on(cc.SystemEvent.EventType.KEY_UP, this.onKeyUp, this); } update(dt) { this.updateRoleMove(); this.updateCamera(); } /**刷新人物移动 */ private updateRoleMove() { //根据按键移动 if (this.keyCache[cc.macro.KEY.up]) { this.roleNode.y += this.roleSpeed; } if (this.keyCache[cc.macro.KEY.down]) { this.roleNode.y -= this.roleSpeed; } if (this.keyCache[cc.macro.KEY.left]) { this.roleNode.x -= this.roleSpeed; } if (this.keyCache[cc.macro.KEY.right]) { this.roleNode.x += this.roleSpeed; } //边缘检测 if (this.roleNode.x + this.roleNode.width / 2 > this.mapNode.width / 2) { this.roleNode.x = this.mapNode.width / 2 - this.roleNode.width / 2; console.log("人物超过地图右边缘"); } else if (this.roleNode.x - this.roleNode.width / 2 < -this.mapNode.width / 2) { this.roleNode.x = -this.mapNode.width / 2 + this.roleNode.width / 2; console.log("人物超过地图左边缘"); } if (this.roleNode.y + this.roleNode.height / 2 > this.mapNode.height / 2) { this.roleNode.y = this.mapNode.height / 2 - this.roleNode.height / 2; console.log("人物超过地图上边缘"); } else if (this.roleNode.y - this.roleNode.height / 2 < -this.mapNode.height / 2) { this.roleNode.y = -this.mapNode.height / 2 + this.roleNode.height / 2; console.log("人物超过地图下边缘"); } } private updateCamera() { this.gameCamera.updatePosition(); } /**按下键*/ public onKeyDown(event) { switch (event.keyCode) { case cc.macro.KEY.up: this.keyCache[cc.macro.KEY.up] = true; console.log("按下Up"); break; case cc.macro.KEY.down: this.keyCache[cc.macro.KEY.down] = true; console.log("按下Down"); break; case cc.macro.KEY.left: this.keyCache[cc.macro.KEY.left] = true; console.log("按下Left"); break; case cc.macro.KEY.right: this.keyCache[cc.macro.KEY.right] = true; console.log("按下Right"); break; } } /**松开按键*/ public onKeyUp(event) { switch (event.keyCode) { case cc.macro.KEY.up: this.keyCache[cc.macro.KEY.up] = false; console.log("松开Up"); break; case cc.macro.KEY.down: this.keyCache[cc.macro.KEY.down] = false; console.log("松开Down"); break; case cc.macro.KEY.left: this.keyCache[cc.macro.KEY.left] = false; console.log("松开Left"); break; case cc.macro.KEY.right: this.keyCache[cc.macro.KEY.right] = false; console.log("松开Right"); break; } } } /**方向 */ export enum Dir { /**上 */ Up = 0, /**下 */ Down = 1, /**左 */ Left = 2, /**右 */ Right = 3 }