first commit
This commit is contained in:
133
assets/scripts/BallPhysics.ts
Normal file
133
assets/scripts/BallPhysics.ts
Normal file
@@ -0,0 +1,133 @@
|
||||
// assets/scripts/BallPhysics.ts
|
||||
import {
|
||||
_decorator,
|
||||
Component,
|
||||
Node,
|
||||
RigidBody,
|
||||
Vec3,
|
||||
math,
|
||||
SphereCollider,
|
||||
Contact2DType,
|
||||
MeshCollider,
|
||||
ICollisionEvent,
|
||||
ITriggerEvent,
|
||||
} from "cc";
|
||||
const { ccclass, property } = _decorator;
|
||||
|
||||
@ccclass("BallPhysics")
|
||||
export class BallPhysics extends Component {
|
||||
@property({ tooltip: "马格努斯系数(越大弧线越明显)" }) magnusK = 0.1;
|
||||
@property({ tooltip: "空气阻力" }) dragK = 0.001;
|
||||
@property({ tooltip: "风力(m/s^2)" }) windStrength = 0;
|
||||
@property({ tooltip: "风向" }) windDir: Vec3 = new Vec3(1, 0, 0);
|
||||
|
||||
@property({ tooltip: "自动重置高度阈值(低于视为无效球)" }) resetY = -2;
|
||||
@property(Node) spawnPoint!: Node;
|
||||
|
||||
@property(Node) ball!: Node;
|
||||
|
||||
private rb!: RigidBody;
|
||||
private tmpF = new Vec3();
|
||||
private v = new Vec3();
|
||||
private w = new Vec3();
|
||||
private wind = new Vec3();
|
||||
private pos = new Vec3();
|
||||
|
||||
// ⚽ 保存 spin(KickInput 设置进来)
|
||||
private spin = new Vec3();
|
||||
|
||||
onLoad() {
|
||||
this.rb = this.ball.getComponent(RigidBody)!;
|
||||
const col = this.ball.getComponent(MeshCollider);
|
||||
if (col) {
|
||||
console.log(22223333, col);
|
||||
col.on("onTriggerEnter", this.onTriggerEnter, this);
|
||||
col.on("onTriggerStay", this.onTriggerStay, this);
|
||||
col.on("onCollisionEnter", this.onCollisionEnter, this);
|
||||
}
|
||||
this.wind.set(this.windDir).normalize();
|
||||
}
|
||||
|
||||
private onTriggerStay(event: ITriggerEvent) {
|
||||
console.log(event.type, event);
|
||||
}
|
||||
|
||||
onTriggerEnter(event: ICollisionEvent) {
|
||||
console.log("⚽ 触发器进入:", event.otherCollider.node.name);
|
||||
}
|
||||
|
||||
onCollisionEnter(event: ICollisionEvent) {
|
||||
console.log("⚽ 碰撞进入:", event.otherCollider.node.name);
|
||||
}
|
||||
|
||||
init() {
|
||||
this.rb = this.node.getComponent(RigidBody)!;
|
||||
this.wind.set(this.windDir).normalize();
|
||||
this.pos = this.node.worldPosition.clone();
|
||||
}
|
||||
|
||||
setWind(direction: Vec3, strength: number) {
|
||||
this.wind.set(direction).normalize();
|
||||
this.windStrength = strength;
|
||||
}
|
||||
|
||||
resetBall() {
|
||||
if (!this.spawnPoint) return;
|
||||
this.rb.clearState();
|
||||
this.node.worldPosition = this.pos; //this.spawnPoint.worldPosition.clone();
|
||||
this.rb.setLinearVelocity(new Vec3());
|
||||
this.rb.setAngularVelocity(new Vec3());
|
||||
}
|
||||
|
||||
/** 设置自旋(由 KickInput 调用) */
|
||||
public setSpin(spin: Vec3) {
|
||||
this.spin.set(spin);
|
||||
}
|
||||
|
||||
/** 发射足球 */
|
||||
shootBall(dir: Vec3, speed: number) {
|
||||
this.rb.clearState();
|
||||
this.rb.setLinearVelocity(dir.clone().multiplyScalar(speed));
|
||||
this.rb.setAngularVelocity(this.spin); // 应用横向旋转
|
||||
}
|
||||
|
||||
update(dt: number) {
|
||||
if (!this.rb) return;
|
||||
|
||||
this.rb.getLinearVelocity(this.v);
|
||||
|
||||
// 1. 空气阻力(与预测保持一致: -k * v * |v|)
|
||||
const speed = this.v.length();
|
||||
if (speed > 0.01) {
|
||||
const drag = this.v.clone().multiplyScalar(-this.dragK * speed);
|
||||
this.rb.applyForce(drag);
|
||||
}
|
||||
|
||||
// 2. 马格努斯力: F = k * (spin × v)
|
||||
if (!this.spin.equals(Vec3.ZERO) && speed > 0.01) {
|
||||
const magnus = new Vec3();
|
||||
Vec3.cross(magnus, this.spin, this.v);
|
||||
magnus.multiplyScalar(this.magnusK);
|
||||
this.rb.applyForce(magnus);
|
||||
}
|
||||
|
||||
// if (!this.spin.equals(Vec3.ZERO) && speed > 0.01) {
|
||||
// const magnus = new Vec3();
|
||||
// const vNorm = this.v.clone().normalize();
|
||||
// Vec3.cross(magnus, this.spin, vNorm);
|
||||
// magnus.multiplyScalar(this.magnusK * Math.sqrt(speed));
|
||||
// this.rb.applyForce(magnus);
|
||||
// }
|
||||
|
||||
// 3. 风
|
||||
if (this.windStrength > 0.0001) {
|
||||
this.tmpF.set(this.wind).multiplyScalar(this.windStrength);
|
||||
this.rb.applyForce(this.tmpF);
|
||||
}
|
||||
|
||||
// 4. 掉出场地自动重置
|
||||
if (this.node.worldPosition.y < this.resetY && this.spawnPoint) {
|
||||
this.resetBall();
|
||||
}
|
||||
}
|
||||
}
|
||||
9
assets/scripts/BallPhysics.ts.meta
Normal file
9
assets/scripts/BallPhysics.ts.meta
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "3aece382-c6ed-4971-82a0-cf40507368f7",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
32
assets/scripts/CameraRig.ts
Normal file
32
assets/scripts/CameraRig.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
// assets/scripts/CameraRig.ts
|
||||
import { _decorator, Component, Node, Vec3, math } from "cc";
|
||||
const { ccclass, property } = _decorator;
|
||||
|
||||
@ccclass("CameraRig")
|
||||
export class CameraRig extends Component {
|
||||
@property(Node) ball!: Node;
|
||||
@property({ tooltip: "跟随平滑(0~1,越小越平滑)" }) smooth = 0.1;
|
||||
@property(Vec3) offset = new Vec3(0, 2.5, -5);
|
||||
|
||||
private _mode: "static" | "follow" = "static";
|
||||
|
||||
setModeFollow() {
|
||||
this._mode = "follow";
|
||||
}
|
||||
setModeStatic() {
|
||||
this._mode = "static";
|
||||
}
|
||||
|
||||
lateUpdate(dt: number) {
|
||||
if (this._mode !== "follow" || !this.ball) return;
|
||||
const target = this.ball.worldPosition.clone().add(this.offset);
|
||||
const p = this.node.worldPosition.clone();
|
||||
const lerp = new Vec3(
|
||||
math.lerp(p.x, target.x, 1 - this.smooth),
|
||||
math.lerp(p.y, target.y, 1 - this.smooth),
|
||||
math.lerp(p.z, target.z, 1 - this.smooth)
|
||||
);
|
||||
this.node.setWorldPosition(lerp);
|
||||
this.node.lookAt(this.ball.worldPosition);
|
||||
}
|
||||
}
|
||||
9
assets/scripts/CameraRig.ts.meta
Normal file
9
assets/scripts/CameraRig.ts.meta
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "ffb4877f-b15c-46cd-b252-dedbe7540235",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
58
assets/scripts/GameManager.ts
Normal file
58
assets/scripts/GameManager.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
// assets/scripts/GameManager.ts
|
||||
import { _decorator, Component, Node, Label, Vec3 } from "cc";
|
||||
import { BallPhysics } from "./BallPhysics";
|
||||
// import { CameraRig } from "./CameraRig";
|
||||
const { ccclass, property } = _decorator;
|
||||
|
||||
@ccclass("GameManager")
|
||||
export class GameManager extends Component {
|
||||
@property(Node) ball!: Node;
|
||||
// @property(Node) cameraRig!: Node;
|
||||
// @property(Label) scoreLabel!: Label;
|
||||
// @property(Label) windLabel!: Label;
|
||||
|
||||
private score = 0;
|
||||
private ballPhys!: BallPhysics;
|
||||
// private cam!: CameraRig;
|
||||
|
||||
onLoad() {
|
||||
this.ballPhys = this.ball.getComponent(BallPhysics)!;
|
||||
this.ballPhys.init();
|
||||
// this.cam = this.cameraRig.getComponent(CameraRig)!;
|
||||
|
||||
// 监听进球
|
||||
this.node.on("GOAL", this.onGoal, this);
|
||||
|
||||
this.newRound();
|
||||
}
|
||||
|
||||
onGoal() {
|
||||
this.score += 1;
|
||||
this.updateScore();
|
||||
this.scheduleOnce(() => {
|
||||
this.newRound();
|
||||
}, 1.0);
|
||||
}
|
||||
|
||||
private updateScore() {
|
||||
// if (this.scoreLabel) this.scoreLabel.string = `${this.score}`;
|
||||
}
|
||||
|
||||
private newRound() {
|
||||
// 重置球与镜头
|
||||
this.ballPhys.resetBall();
|
||||
// this.cam.setModeStatic();
|
||||
|
||||
// 随机风
|
||||
// const dir = new Vec3(Math.random() - 0.5, 0, Math.random() - 0.5);
|
||||
// const strength = 4 * Math.random(); // 0~4
|
||||
// this.ballPhys.setWind(dir, strength);
|
||||
// if (this.windLabel) {
|
||||
// const a = (Math.atan2(dir.x, dir.z) * 180) / Math.PI; // 粗略角度显示
|
||||
// this.windLabel.string = `Wind ${strength.toFixed(1)} @ ${a.toFixed(0)}°`;
|
||||
// }
|
||||
|
||||
// 几百毫秒后把镜头切为跟随(等玩家射门后也可在 KickInput 里触发)
|
||||
// this.scheduleOnce(() => this.cam.setModeFollow(), 0.6);
|
||||
}
|
||||
}
|
||||
9
assets/scripts/GameManager.ts.meta
Normal file
9
assets/scripts/GameManager.ts.meta
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "81883817-9f1d-4d28-8109-f62a0dc23285",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
109
assets/scripts/GameMrg.ts
Normal file
109
assets/scripts/GameMrg.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
import {
|
||||
_decorator,
|
||||
Component,
|
||||
Vec3,
|
||||
RigidBody,
|
||||
input,
|
||||
Input,
|
||||
EventTouch,
|
||||
} from "cc";
|
||||
import { TrajectoryPreview } from "./TrajectoryPreview";
|
||||
const { ccclass, property } = _decorator;
|
||||
|
||||
@ccclass("GameMrg")
|
||||
export class GameMrg extends Component {
|
||||
@property(RigidBody)
|
||||
rigidBody: RigidBody = null!;
|
||||
|
||||
@property(TrajectoryPreview)
|
||||
trajectory: TrajectoryPreview = null!;
|
||||
|
||||
@property
|
||||
maxPower: number = 25;
|
||||
|
||||
private startPos: Vec3 = new Vec3();
|
||||
private endPos: Vec3 = new Vec3();
|
||||
private isDragging: boolean = false;
|
||||
|
||||
start() {
|
||||
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);
|
||||
input.on(Input.EventType.TOUCH_CANCEL, this.onTouchEnd, this);
|
||||
}
|
||||
|
||||
onDestroy() {
|
||||
input.off(Input.EventType.TOUCH_START, this.onTouchStart, this);
|
||||
input.off(Input.EventType.TOUCH_MOVE, this.onTouchMove, this);
|
||||
input.off(Input.EventType.TOUCH_END, this.onTouchEnd, this);
|
||||
input.off(Input.EventType.TOUCH_CANCEL, this.onTouchEnd, this);
|
||||
}
|
||||
|
||||
onTouchStart(e: EventTouch) {
|
||||
this.startPos.set(e.getLocation().x, e.getLocation().y, 0);
|
||||
this.isDragging = true;
|
||||
}
|
||||
|
||||
onTouchMove(e: EventTouch) {
|
||||
if (!this.isDragging) return;
|
||||
this.endPos.set(e.getLocation().x, e.getLocation().y, 0);
|
||||
const dragVec = new Vec3(
|
||||
this.startPos.x - this.endPos.x,
|
||||
this.startPos.y - this.endPos.y,
|
||||
0
|
||||
);
|
||||
if (dragVec.length() < 10) {
|
||||
this.trajectory.hideTrajectory();
|
||||
return;
|
||||
}
|
||||
|
||||
// 主方向(始终向前 + 抬升)
|
||||
const forward = new Vec3(0, 0.5, 1).normalize();
|
||||
|
||||
// 力度
|
||||
const power = Math.min(dragVec.y * 0.05 + 10, this.maxPower);
|
||||
|
||||
// 左右弧线
|
||||
const sideForce = dragVec.x * 0.02;
|
||||
|
||||
// 显示轨迹
|
||||
this.trajectory.showTrajectory(
|
||||
// this.rigidBody.node.worldPosition,
|
||||
this.startPos.clone(),
|
||||
forward,
|
||||
power,
|
||||
sideForce
|
||||
);
|
||||
}
|
||||
|
||||
onTouchEnd(e: EventTouch) {
|
||||
if (!this.isDragging) return;
|
||||
this.isDragging = false;
|
||||
this.trajectory.hideTrajectory();
|
||||
|
||||
this.endPos.set(e.getLocation().x, e.getLocation().y, 0);
|
||||
|
||||
const dragVec = new Vec3(
|
||||
this.startPos.x - this.endPos.x,
|
||||
this.startPos.y - this.endPos.y,
|
||||
0
|
||||
);
|
||||
|
||||
if (dragVec.length() < 10) return;
|
||||
|
||||
// 主方向
|
||||
const forward = new Vec3(0, 0.5, 1).normalize();
|
||||
|
||||
// 力度
|
||||
const power = Math.min(dragVec.y * 0.05 + 10, this.maxPower);
|
||||
|
||||
// 左右弧线
|
||||
const sideForce = dragVec.x * 0.02;
|
||||
|
||||
// 发射冲量
|
||||
const impulse = forward.multiplyScalar(power);
|
||||
impulse.x += sideForce;
|
||||
|
||||
this.rigidBody.applyImpulse(impulse);
|
||||
}
|
||||
}
|
||||
9
assets/scripts/GameMrg.ts.meta
Normal file
9
assets/scripts/GameMrg.ts.meta
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "cb6a4184-4c18-405f-8e18-f0d5829517cd",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
19
assets/scripts/GoalDetector.ts
Normal file
19
assets/scripts/GoalDetector.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
// assets/scripts/GoalDetector.ts
|
||||
import { _decorator, Component, ITriggerEvent } from "cc";
|
||||
const { ccclass, property } = _decorator;
|
||||
|
||||
@ccclass("GoalDetector")
|
||||
export class GoalDetector extends Component {
|
||||
@property({ tooltip: "GameManager 节点(接收进球事件)" }) gameManagerPath =
|
||||
"";
|
||||
|
||||
onTriggerEnter(event: ITriggerEvent) {
|
||||
const other = event.otherCollider?.node;
|
||||
if (!other) return;
|
||||
if (other.name.includes("Ball")) {
|
||||
// 简单通知
|
||||
const gm = this.node.scene.getChildByPath(this.gameManagerPath);
|
||||
gm?.emit("GOAL");
|
||||
}
|
||||
}
|
||||
}
|
||||
9
assets/scripts/GoalDetector.ts.meta
Normal file
9
assets/scripts/GoalDetector.ts.meta
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "85ff5590-99dd-47f5-8422-0702fc260fb3",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
45
assets/scripts/GoalkeeperAI.ts
Normal file
45
assets/scripts/GoalkeeperAI.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
// assets/scripts/GoalkeeperAI.ts
|
||||
import { _decorator, Component, Node, RigidBody, Vec3, math } from "cc";
|
||||
const { ccclass, property } = _decorator;
|
||||
|
||||
@ccclass("GoalkeeperAI")
|
||||
export class GoalkeeperAI extends Component {
|
||||
@property(Node) ball!: Node;
|
||||
@property({ tooltip: "球门线Z(世界坐标)" }) goalLineZ = 10;
|
||||
@property({ tooltip: "守门员水平活动范围X" }) rangeX = 3.5;
|
||||
@property({ tooltip: "移动速度(m/s)" }) speed = 6;
|
||||
@property({ tooltip: "反应延迟(秒)" }) reactDelay = 0.15;
|
||||
|
||||
private targetX = 0;
|
||||
private tAcc = 0;
|
||||
private tmp = new Vec3();
|
||||
|
||||
update(dt: number) {
|
||||
if (!this.ball) return;
|
||||
|
||||
// 简化预测:用当前速度线性外推到球门线的交点
|
||||
const rb = this.ball.getComponent(RigidBody);
|
||||
if (!rb) return;
|
||||
rb.getLinearVelocity(this.tmp);
|
||||
const v = this.tmp.clone();
|
||||
const bp = this.ball.worldPosition.clone();
|
||||
|
||||
if (Math.abs(v.z) < 0.1) return; // 几乎不朝球门方向飞
|
||||
|
||||
const timeToLine = (this.goalLineZ - bp.z) / v.z; // z 轴朝向球门
|
||||
if (timeToLine < 0) return; // 已过线或背离
|
||||
|
||||
this.tAcc += dt;
|
||||
if (this.tAcc >= this.reactDelay) {
|
||||
const predictX = bp.x + v.x * timeToLine;
|
||||
this.targetX = math.clamp(predictX, -this.rangeX, this.rangeX);
|
||||
this.tAcc = 0; // 下次再延迟
|
||||
}
|
||||
|
||||
// 移动守门员
|
||||
const p = this.node.worldPosition;
|
||||
const dx = this.targetX - p.x;
|
||||
const step = math.clamp(dx, -this.speed * dt, this.speed * dt);
|
||||
this.node.setWorldPosition(p.x + step, p.y, p.z);
|
||||
}
|
||||
}
|
||||
9
assets/scripts/GoalkeeperAI.ts.meta
Normal file
9
assets/scripts/GoalkeeperAI.ts.meta
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "30f1b3d4-4345-4002-a1ae-d9a745976704",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
172
assets/scripts/KickInput.ts
Normal file
172
assets/scripts/KickInput.ts
Normal file
@@ -0,0 +1,172 @@
|
||||
import {
|
||||
_decorator,
|
||||
Component,
|
||||
Node,
|
||||
EventTouch,
|
||||
Input,
|
||||
input,
|
||||
Vec2,
|
||||
Vec3,
|
||||
RigidBody,
|
||||
math,
|
||||
Graphics,
|
||||
MeshRenderer,
|
||||
geometry,
|
||||
} from "cc";
|
||||
import { TrajectoryPreview } from "./TrajectoryPreview";
|
||||
import { Preview } from "./Preview";
|
||||
import { BallPhysics } from "./BallPhysics";
|
||||
const { ccclass, property } = _decorator;
|
||||
|
||||
@ccclass("KickInput")
|
||||
export class KickInput extends Component {
|
||||
@property(Node) ball!: Node;
|
||||
// @property(Node) goal!: Node;
|
||||
|
||||
@property(Node) cameraRig!: Node;
|
||||
|
||||
// 力度与角度控制
|
||||
@property({ tooltip: "最大初速度(m/s)" }) maxSpeed = 20;
|
||||
@property({ tooltip: "最小仰角(度)" }) minPitchDeg = 8;
|
||||
@property({ tooltip: "最大仰角(度)" }) maxPitchDeg = 18;
|
||||
|
||||
// 自旋与弧线
|
||||
@property({ tooltip: "自旋强度系数" }) spinFactor = 20;
|
||||
|
||||
@property(Preview) preview: Preview = null!;
|
||||
|
||||
private startPos = new Vec2();
|
||||
private dragging = false;
|
||||
private lastPos = new Vec2();
|
||||
private trail: Vec2[] = [];
|
||||
|
||||
private ballPhys: BallPhysics = null;
|
||||
|
||||
protected onLoad(): void {
|
||||
this.ballPhys = this.ball.getComponent(BallPhysics)!;
|
||||
}
|
||||
|
||||
onEnable() {
|
||||
input.on(Input.EventType.TOUCH_START, this.onStart, this);
|
||||
input.on(Input.EventType.TOUCH_MOVE, this.onMove, this);
|
||||
input.on(Input.EventType.TOUCH_END, this.onEnd, this);
|
||||
input.on(Input.EventType.TOUCH_CANCEL, this.onEnd, this);
|
||||
}
|
||||
onDisable() {
|
||||
input.off(Input.EventType.TOUCH_START, this.onStart, this);
|
||||
input.off(Input.EventType.TOUCH_MOVE, this.onMove, this);
|
||||
input.off(Input.EventType.TOUCH_END, this.onEnd, this);
|
||||
input.off(Input.EventType.TOUCH_CANCEL, this.onEnd, this);
|
||||
}
|
||||
|
||||
private onStart(e: EventTouch) {
|
||||
this.dragging = true;
|
||||
e.getLocation(this.startPos);
|
||||
e.getLocation(this.lastPos);
|
||||
this.trail.length = 0;
|
||||
this.trail.push(this.startPos.clone());
|
||||
}
|
||||
|
||||
private onMove(e: EventTouch) {
|
||||
if (!this.dragging) return;
|
||||
const p = e.getLocation();
|
||||
this.trail.push(p.clone());
|
||||
this.lastPos.set(p);
|
||||
|
||||
// 1) 拖拽向量
|
||||
const drag = this.lastPos.clone().subtract(this.startPos);
|
||||
const dragLen = drag.length();
|
||||
|
||||
// 2) 速度(映射拖拽长度)
|
||||
const speed = math.clamp(dragLen / 8, 5, this.maxSpeed);
|
||||
// const speed = 12;
|
||||
|
||||
// 3) 仰角 pitch(映射拖拽的纵向)
|
||||
const t = math.clamp(drag.y / 300, 0, 1);
|
||||
const pitchDeg = math.lerp(this.minPitchDeg, this.maxPitchDeg, t);
|
||||
|
||||
// 4) 偏航角 yaw(映射拖拽的横向)
|
||||
const yawDeg = math.clamp(-drag.x / 8, -45, 45);
|
||||
|
||||
// 5) 得到方向向量
|
||||
const dir = this.dirFromAngles(yawDeg, pitchDeg);
|
||||
|
||||
// 6) 自旋(用轨迹估计)
|
||||
const spin = this.estimateSpin(this.trail, drag).multiplyScalar(
|
||||
this.spinFactor
|
||||
);
|
||||
|
||||
this.preview.showTrajectory(this.ball.worldPosition, dir, speed, spin);
|
||||
}
|
||||
|
||||
private onEnd() {
|
||||
if (!this.dragging) return;
|
||||
this.dragging = false;
|
||||
|
||||
this.preview.clear();
|
||||
|
||||
// 拖拽向量
|
||||
const drag = this.lastPos.clone().subtract(this.startPos);
|
||||
const dragLen = drag.length();
|
||||
|
||||
if (!this.ball || !this.ballPhys) return;
|
||||
|
||||
// 1) 计算初速度
|
||||
const speed = math.clamp(dragLen / 8, 5, this.maxSpeed);
|
||||
// const speed = 12;
|
||||
// 2) 仰角 pitch
|
||||
const t = math.clamp(drag.y / 300, 0, 1);
|
||||
const pitchDeg = math.lerp(this.minPitchDeg, this.maxPitchDeg, t);
|
||||
|
||||
// 3) 偏航 yaw
|
||||
const yawDeg = math.clamp(-drag.x / 8, -45, 45);
|
||||
|
||||
// 4) 方向向量
|
||||
const dir = this.dirFromAngles(yawDeg, pitchDeg);
|
||||
|
||||
// 5) 自旋
|
||||
const spin = this.estimateSpin(this.trail, drag).multiplyScalar(
|
||||
this.spinFactor
|
||||
);
|
||||
this.ballPhys.setSpin(spin);
|
||||
console.log(`t: ${t}, pitchDeg:${pitchDeg}`);
|
||||
// 6) 发射
|
||||
this.ballPhys.shootBall(dir, speed);
|
||||
}
|
||||
|
||||
private dirFromAngles(yawDeg: number, pitchDeg: number) {
|
||||
const yaw = math.toRadian(yawDeg);
|
||||
const pitch = math.toRadian(pitchDeg);
|
||||
// 水平前方Z轴,右手坐标:根据你场景轴向可能需调整
|
||||
const x = Math.sin(yaw) * Math.cos(pitch);
|
||||
const y = Math.sin(pitch);
|
||||
const z = Math.cos(yaw) * Math.cos(pitch);
|
||||
return new Vec3(x, y, z).normalize();
|
||||
}
|
||||
|
||||
// 简单估计自旋方向:用拖拽路径的“弯曲方向”
|
||||
private estimateSpin(path: Vec2[], drag: Vec2): Vec3 {
|
||||
if (path.length < 4) return new Vec3(0, 0, 0);
|
||||
// 取开始、中点、终点,计算平面曲率符号,映射为Y轴为主或Z轴自旋
|
||||
const a = path[0],
|
||||
b = path[Math.floor(path.length / 2)],
|
||||
c = path[path.length - 1];
|
||||
// 2D 叉积 z 分量(曲率方向)
|
||||
const ab = new Vec2(b.x - a.x, b.y - a.y);
|
||||
const bc = new Vec2(c.x - b.x, c.y - b.y);
|
||||
const cross = ab.x * bc.y - ab.y * bc.x; // >0 逆时针弯曲
|
||||
|
||||
// 横向弯 -> 绕Y轴的自旋(产生左右弧线)
|
||||
|
||||
const spinY = math.clamp(drag.x / 200, -3.5, 3.5);
|
||||
return new Vec3(0, spinY, 0); // 只绕Y轴,产生左右弧度
|
||||
|
||||
// const spinY = math.clamp(cross / 20000, -1.5, 1.5) * 2.0; // 横向弧度提前
|
||||
|
||||
// // 竖向弯 -> 绕X轴/或Z轴的自旋(下坠/挑球),这里简单给点Z
|
||||
// const dy = c.y - a.y;
|
||||
// const spinZ = math.clamp(dy / 1500, -1.0, 1.0);
|
||||
|
||||
// return new Vec3(0, spinY, spinZ);
|
||||
}
|
||||
}
|
||||
9
assets/scripts/KickInput.ts.meta
Normal file
9
assets/scripts/KickInput.ts.meta
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "4e90f5ed-3b95-4d1d-90b7-3b9eb5d8c130",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
82
assets/scripts/Preview.ts
Normal file
82
assets/scripts/Preview.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
// assets/scripts/Preview.ts
|
||||
import { _decorator, Component, Node, Prefab, instantiate, Vec3 } from "cc";
|
||||
import { BallPhysics } from "./BallPhysics";
|
||||
const { ccclass, property } = _decorator;
|
||||
|
||||
@ccclass("Preview")
|
||||
export class Preview extends Component {
|
||||
@property(Prefab) pointPrefab: Prefab = null!;
|
||||
@property(Node) goal: Node = null!;
|
||||
|
||||
@property({ tooltip: "轨迹点数量" }) maxPoints = 30;
|
||||
@property({ tooltip: "步长时间(秒)" }) timeStep = 0.05;
|
||||
|
||||
@property(BallPhysics) ballPhys: BallPhysics = null!;
|
||||
|
||||
private points: Node[] = [];
|
||||
|
||||
clear() {
|
||||
for (const p of this.points) p.destroy();
|
||||
this.points.length = 0;
|
||||
}
|
||||
|
||||
/** 显示预测轨迹 */
|
||||
showTrajectory(startPos: Vec3, dir: Vec3, speed: number, spin: Vec3) {
|
||||
this.clear();
|
||||
if (!this.pointPrefab || !this.ballPhys) return;
|
||||
|
||||
const goalZ = this.goal?.worldPosition?.z;
|
||||
|
||||
// 初始状态
|
||||
let pos = startPos.clone();
|
||||
let v = dir.clone().multiplyScalar(speed);
|
||||
let w = spin.clone(); // 自旋
|
||||
|
||||
// 迭代预测
|
||||
// let age = 0;
|
||||
for (let i = 0; i < this.maxPoints; i++) {
|
||||
// age += this.timeStep;
|
||||
// 保存点
|
||||
const p = instantiate(this.pointPrefab);
|
||||
p.setWorldPosition(pos);
|
||||
p.setParent(this.node);
|
||||
this.points.push(p);
|
||||
|
||||
// === 模拟 BallPhysics.update(dt) ===
|
||||
// const dt = this.timeStep;
|
||||
const dt = this.timeStep * 0.5;
|
||||
|
||||
// 空气阻力
|
||||
const speedNow = v.length();
|
||||
if (speedNow > 0.01) {
|
||||
const drag = v.clone().multiplyScalar(-this.ballPhys.dragK * speedNow);
|
||||
v.add(drag.multiplyScalar(dt));
|
||||
}
|
||||
|
||||
// 马格努斯效应:F = k * (ω × v)
|
||||
if (w.lengthSqr() > 0.0001 && v.lengthSqr() > 0.0001) {
|
||||
// const tFactor = 1.0 / (1.0 + age * 2.0); // age 是飞行时间,越久衰减越快
|
||||
// const magnus = w
|
||||
// .clone()
|
||||
// .cross(v)
|
||||
// .multiplyScalar(this.ballPhys.magnusK * tFactor);
|
||||
// v.add(magnus.multiplyScalar(dt));
|
||||
const magnus = w.clone().cross(v).multiplyScalar(this.ballPhys.magnusK);
|
||||
v.add(magnus.multiplyScalar(dt));
|
||||
}
|
||||
|
||||
// 重力
|
||||
v.y += -9.8 * dt;
|
||||
|
||||
// 移动
|
||||
pos = pos.add(v.clone().multiplyScalar(dt));
|
||||
|
||||
if (
|
||||
(startPos.z > goalZ && pos.z <= goalZ) ||
|
||||
(startPos.z < goalZ && pos.z >= goalZ)
|
||||
) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
9
assets/scripts/Preview.ts.meta
Normal file
9
assets/scripts/Preview.ts.meta
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "274524cf-5678-498a-9486-122ab0771b04",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
63
assets/scripts/TrajectoryPreview.ts
Normal file
63
assets/scripts/TrajectoryPreview.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import { _decorator, Component, Node, Prefab, instantiate, Vec3 } from "cc";
|
||||
const { ccclass, property } = _decorator;
|
||||
|
||||
@ccclass("TrajectoryPreview")
|
||||
export class TrajectoryPreview extends Component {
|
||||
@property(Prefab)
|
||||
pointPrefab: Prefab = null!;
|
||||
|
||||
@property
|
||||
maxPoints: number = 20;
|
||||
|
||||
@property
|
||||
timeStep: number = 0.1;
|
||||
|
||||
@property
|
||||
gravity: Vec3 = new Vec3(0, -9.8, 0);
|
||||
|
||||
private points: Node[] = [];
|
||||
|
||||
onLoad() {
|
||||
for (let i = 0; i < this.maxPoints; i++) {
|
||||
const p = instantiate(this.pointPrefab);
|
||||
p.active = false;
|
||||
this.node.addChild(p); // 确保挂在场景下的空节点
|
||||
this.points.push(p);
|
||||
}
|
||||
}
|
||||
|
||||
showTrajectory(
|
||||
startPos: Vec3,
|
||||
forward: Vec3,
|
||||
power: number,
|
||||
sideForce: number
|
||||
) {
|
||||
const velocity = forward.multiplyScalar(power);
|
||||
velocity.x += sideForce;
|
||||
|
||||
const ballRadius = 0.01; // ⚽ 球半径
|
||||
const groundY = 0; // 🟩 地面高度
|
||||
|
||||
for (let i = 0; i < this.maxPoints; i++) {
|
||||
const t = i * this.timeStep;
|
||||
|
||||
const pos = new Vec3(
|
||||
startPos.x + velocity.x * t,
|
||||
startPos.y + velocity.y * t + 0.5 * this.gravity.y * t * t,
|
||||
startPos.z + velocity.z * t
|
||||
);
|
||||
|
||||
// ⭐ 限制最低不低于地面
|
||||
if (pos.y < groundY + ballRadius * 0.5) {
|
||||
pos.y = groundY + ballRadius * 0.5;
|
||||
}
|
||||
|
||||
this.points[i].setWorldPosition(pos);
|
||||
this.points[i].active = true;
|
||||
}
|
||||
}
|
||||
|
||||
hideTrajectory() {
|
||||
this.points.forEach((p) => (p.active = false));
|
||||
}
|
||||
}
|
||||
9
assets/scripts/TrajectoryPreview.ts.meta
Normal file
9
assets/scripts/TrajectoryPreview.ts.meta
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "4210fa5e-4d43-4bef-bd48-c7949dccf074",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
Reference in New Issue
Block a user