预览效果:
技术要点:
- 主摄像机的视场轴需要设置为水平。
- 在场景下创建一个空节点用于挂载控制器脚本
图片已进行各概念的说明
在“collisionNodeArray”属性下,放置需要点击的星球节点,系统会自己绑定碰撞器。
也可自己提前绑定。
- 布局场景,星球围绕相机。参考如下:
注意相机的属性。可以根据自己的需要调整相机的z值。只要保证星球绕着相机布局则行。
- 关于摄像机旋转的上下限制。
可以通过设置
这两个参数进行调节,是个经验数值。
系统本身会进行基础的上下限制。这两个参数属于额外的限制。即顶部往下,底部往上。
控制器(planet_view_controller)代码:
直接拷贝到项目的一个空代码文件即可:
import { _decorator, Component, Node, Camera, Input, input, EventTouch, Vec2, Quat, Vec3, screen, tween, Tween, Collider, SphereCollider, geometry, PhysicsSystem, EventHandler } from "cc";
const { ccclass, property } = _decorator;
@ccclass("PlanetViewController")
export class PlanetViewController extends Component {
start() {
this.bindInputEvent();
this.setLimitEuler();
this.clickManagerStart();
}
//浏览功能区
@property({
displayName: "顶部额外限制角度",
})
private upLimitAngle = 0;
@property({
displayName: "底部额外限制角度",
})
private downLimitAngle = 0;
@property(Camera)
private centerCamera!: Camera;
private bindInputEvent() {
input.on(Input.EventType.TOUCH_START, this.onTouchStart, this);
input.on(Input.EventType.TOUCH_MOVE, this.onTouchMove, this);
input.on(Input.EventType.TOUCH_END, this.onTouchEnd, this);
}
private limitEdgeEuler = 0;
private setLimitEuler() {
this.limitEdgeEuler = (180 * Math.atan(Math.tan((this.centerCamera.fov * Math.PI) / 360) / this.centerCamera.camera.aspect)) / Math.PI;
}
private startLocation = new Vec2();
private onTouchStart(e: EventTouch) {
e.getLocation(this.startLocation);
Tween.stopAllByTarget(this.lastRotaionSpeed);
}
private lastRotaionSpeed = new Vec2();
private onTouchMove(e: EventTouch) {
e.getDelta(this.lastRotaionSpeed);
this.rotateCenterCamera(this.lastRotaionSpeed);
}
private clickLocation = new Vec2();
private onTouchEnd(e: EventTouch) {
e.getLocation(this.clickLocation);
const dis = Vec2.squaredDistance(this.startLocation, this.clickLocation);
if (dis <= 0.1) {
this.node.emit("click", this.clickLocation);
console.log("click");
return;
}
tween(this.lastRotaionSpeed)
.to(
0.5,
{
x: 0,
y: 0,
},
{
easing: "sineOut",
onUpdate: () => {
this.rotateCenterCamera(this.lastRotaionSpeed);
},
}
)
.start();
}
private curRotateResultEuler = new Vec3();
private rotateQuat = new Quat();
private lastRotationQuat = new Quat();
private rotateCenterCamera(volume: Vec2) {
Quat.fromAxisAngle(this.rotateQuat, Vec3.UP, (volume.x * 0.785) / screen.windowSize.width);
Quat.rotateX(this.rotateQuat, this.rotateQuat, (-volume.y * 0.785) / screen.windowSize.height);
this.lastRotationQuat.set(this.centerCamera.node.rotation);
Quat.multiply(this.lastRotationQuat, this.lastRotationQuat, this.rotateQuat);
this.lastRotationQuat.getEulerAngles(this.curRotateResultEuler);
this.centerCamera.node.rotate(this.rotateQuat);
const isOverUp = this.curRotateResultEuler.x < -this.limitEdgeEuler + this.upLimitAngle;
const isOverDown = this.curRotateResultEuler.x > this.limitEdgeEuler - this.downLimitAngle;
if (isOverUp || isOverDown) {
this.lastRotationQuat.set(this.centerCamera.node.rotation);
this.lastRotationQuat.getEulerAngles(this.curRotateResultEuler);
const { y, z } = this.curRotateResultEuler;
const x = isOverUp ? -this.limitEdgeEuler + this.upLimitAngle : this.limitEdgeEuler - this.downLimitAngle;
this.centerCamera.node.setRotationFromEuler(x, y, z);
}
const { x, y } = this.centerCamera.node.eulerAngles;
this.centerCamera.node.setRotationFromEuler(x, y, 0);
}
//点击检测功能区
@property([EventHandler])
private collisionEventHandlerArray: EventHandler[] = [];
@property([Node])
private collisionNodeArray: Node[] = [];
private clickManagerStart() {
this.setCollisionNodeCollider();
this.bindClickEvent();
}
private setCollisionNodeCollider() {
this.collisionNodeArray.forEach((node) => {
let collider = node.getComponent(Collider);
if (!collider) {
collider = node.addComponent(SphereCollider);
}
collider.isTrigger = true;
});
}
private clickRay = new geometry.Ray();
private bindClickEvent() {
this.node.on("click", ({ x, y }: Vec2) => {
if (this.collisionNodeArray.length === 0) return;
this.centerCamera.screenPointToRay(x, y, this.clickRay);
PhysicsSystem.instance.raycast(this.clickRay);
if (PhysicsSystem.instance.raycastResults.length) {
const firstColliderNode = PhysicsSystem.instance.raycastResults[0].collider.node;
this.collisionEventHandlerArray.forEach((handler) => handler.emit([firstColliderNode]));
}
});
}
}