Files
cocos/assets/scripts/gameplay/LineGridRenderer.ts
刘宇飞 d393302388 Complete Cocos Creator port with level bundles, themes, and tooling.
Adds level prefabs, theme assets, audio, extensions, and deployment scripts for the Unity WebGL migration.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-16 15:30:58 +08:00

207 lines
6.5 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import {
_decorator, Color, Component, Graphics, Layers, Node, UITransform, Vec3, view,
} from 'cc';
import { CELL_PIXEL, CAMERA_ORTHO_HALF, DESIGN_HEIGHT } from '../core/GridConstants';
import { cellToWorldCenter, getHalfCellSize } from '../core/GridCoords';
import { EventManager, EventType } from '../core/EventManager';
import { GameManager } from '../manager/GameManager';
import { GridSnapHelper } from '../level/GridSnapHelper';
const { ccclass } = _decorator;
const UI_LAYER = Layers.Enum.UI_2D;
/** 对齐 Unity LineGridRenderer导航按钮切换的满屏等距辅助网格 */
@ccclass('LineGridRenderer')
export class LineGridRenderer extends Component {
static instance: LineGridRenderer | null = null;
/** 以中心向外的格子半径(菱形个数 ≈ (2r+1)²) */
radius = 24;
cellSize = CELL_PIXEL;
private graphics: Graphics | null = null;
private visible = false;
static ensure(parent: Node): LineGridRenderer {
LineGridRenderer.purgeDuplicates(parent);
let comp = LineGridRenderer.instance?.isValid ? LineGridRenderer.instance : null;
if (!comp) {
const found = LineGridRenderer.findUnder(parent);
comp = found?.getComponent(LineGridRenderer) ?? null;
}
if (!comp) {
const node = new Node('LineGrid');
node.parent = parent;
comp = node.addComponent(LineGridRenderer);
} else if (comp.node.parent !== parent) {
comp.node.parent = parent;
}
comp.setupRuntime();
comp.sendToBack();
return comp;
}
/** 移除误挂在关卡内的重复 LineGrid会导致叠线无法关闭 */
private static purgeDuplicates(entrance: Node) {
const keep = LineGridRenderer.instance?.isValid
? LineGridRenderer.instance.node
: LineGridRenderer.findUnder(entrance);
const walk = (node: Node) => {
for (let i = node.children.length - 1; i >= 0; i--) {
const ch = node.children[i];
if (ch.name === 'LineGrid' && ch !== keep) {
ch.destroy();
continue;
}
walk(ch);
}
};
walk(entrance);
}
private static findUnder(root: Node): Node | null {
if (root.name === 'LineGrid') return root;
for (const ch of root.children) {
const hit = LineGridRenderer.findUnder(ch);
if (hit) return hit;
}
return null;
}
onLoad() {
if (LineGridRenderer.instance && LineGridRenderer.instance !== this) {
this.node.destroy();
return;
}
LineGridRenderer.instance = this;
this.setupRuntime();
EventManager.register(EventType.LevelInit, this.onLevelInit);
}
onDestroy() {
EventManager.remove(EventType.LevelInit, this.onLevelInit);
if (LineGridRenderer.instance === this) LineGridRenderer.instance = null;
}
isGridVisible(): boolean {
return this.visible;
}
private setupRuntime() {
this.node.layer = UI_LAYER;
if (!this.node.getComponent(UITransform)) {
this.node.addComponent(UITransform).setContentSize(1, 1);
}
const g = this.getComponent(Graphics) ?? this.addComponent(Graphics);
this.graphics = g;
g.lineWidth = 1.4;
g.strokeColor = new Color(0, 0, 0, 255);
if (!this.visible) {
g.clear();
this.node.active = false;
}
}
private onLevelInit = () => {
this.purgeEditorGrid();
this.syncLevelOffset();
this.updateGridRadius();
if (this.visible) {
this.rebuild();
this.node.active = true;
this.sendToBack();
} else {
this.graphics?.clear();
this.node.active = false;
}
};
toggleGridVisibility() {
this.setupRuntime();
this.visible = !this.visible;
if (this.visible) {
this.purgeEditorGrid();
this.syncLevelOffset();
this.updateGridRadius();
this.rebuild();
this.node.active = true;
this.sendToBack();
} else {
this.graphics?.clear();
this.node.active = false;
}
}
/** 移除编辑器 GridSnapHelper 遗留的蓝色/灰色参考格 */
private purgeEditorGrid() {
const entrance = this.node.parent;
if (entrance?.isValid) {
GridSnapHelper.purgeRuntimeGrids(entrance);
}
const scene = this.node.scene;
if (scene?.isValid) {
GridSnapHelper.purgeRuntimeGrids(scene);
}
}
/** 保持在 MainLevelEntrance 最底层,不遮挡关卡内砖块/角色 */
private sendToBack() {
const parent = this.node.parent;
if (parent?.isValid) {
this.node.setSiblingIndex(0);
}
}
/** 与关卡根节点对齐,网格覆盖当前可见砖块区域 */
private syncLevelOffset() {
const parent = this.node.parent;
if (!parent) return;
const levelRoot = parent.children.find(
(c) => c?.isValid && c !== this.node && (c.name ?? '').startsWith('Level'),
);
if (levelRoot?.isValid) {
const p = levelRoot.position;
this.node.setPosition(p.x, p.y, 0);
} else {
this.node.setPosition(0, 0, 0);
}
}
private updateGridRadius() {
const b = GameManager.instance?.getCurLevel()?.boundary;
const vs = view.getVisibleSize();
const halfH = CAMERA_ORTHO_HALF;
const halfW = halfH * (vs.width / Math.max(vs.height, DESIGN_HEIGHT));
const cover = Math.ceil(Math.max(halfW, halfH) / (CELL_PIXEL * 0.35)) + 6;
if (b) {
this.radius = Math.max(cover, b.x + 8, b.y + 8);
} else {
this.radius = cover;
}
}
private rebuild() {
const g = this.graphics;
if (!g) return;
g.clear();
const { halfW, halfH } = getHalfCellSize();
const r = this.radius;
for (let x = -r; x <= r; x++) {
for (let y = -r; y <= r; y++) {
const c = cellToWorldCenter(new Vec3(x, y, 0));
g.moveTo(c.x, c.y + halfH);
g.lineTo(c.x + halfW, c.y);
g.lineTo(c.x, c.y - halfH);
g.lineTo(c.x - halfW, c.y);
g.close();
}
}
g.stroke();
}
}