Cocos Creator 3.8箭头小游戏的实现

这日子一天天的,干饭都吃不到热热乎的,也不知道咋想的,这游戏都出来多久了。

算了,折腾就折腾吧。这游戏最讨厌的就是关卡编辑器了,实现起来倒是不太麻烦。

按照俺们写游戏的习惯,中心点肯定是0,0,然后根据阈值向外衍生,一堆放格,取中心点,再来个箭头,当然不要超出屏幕尺寸。

导出的数据就是这样的,肯定不可能就一条箭头线段

至于demo吗,弄一条就行了,装装样子吗。

1.场景上挂一个节点,挂一个脚本。

几个必要的属性,没必要挂到属性面板上 type Point = { x: number, y: number };

复制代码
// 箭头绘制组件
private _Graphics: Graphics = null!;
// 当前箭头路径点数组,0号=箭头头部,末尾=尾部
private _ArrowPath: Point[] = [];
// 移动开关
private _StartMove: boolean = false;
// 时间累加器:用于固定子步更新,原名叫_frameRate,容易误解
private _accumTime: number = 0;
// 可视窗口大小
private _WinSize: math.Size = null!;
// 移动开始时原始路径,用于碰撞回退,做深拷贝
private _OriginPath: Point[] = [];
// 每一次逻辑更新的固定时间片(秒),0.01 = 10ms
private readonly FIXED_DELTA = 0.01;
// 移动速度
private readonly MOVE_SPEED = 10;

这里的组件用到是Graphics,一些常用的属性

复制代码
/**
    * @en
    * Graphics component.
    *
    * @zh
    * 自定义图形类。
    */
export class GraphicsComponent extends UIRenderer {
    /**
        * @en
        * Current line width.
        *
        * @zh
        * 当前线条宽度。
        */
    get lineWidth(): number;
    set lineWidth(value: number);
    /**
        * @en
        * Determines how two connecting segments (of lines, arcs or curves) with non-zero lengths in a shape are joined together.
        *
        * @zh
        * 用来设置2个长度不为0的相连部分(线段,圆弧,曲线)如何连接在一起的属性。
        */
    get lineJoin(): __private._cocos_2d_assembler_graphics_types__LineJoin;
    set lineJoin(value: __private._cocos_2d_assembler_graphics_types__LineJoin);
    /**
        * @en
        * Determines how the end points of every line are drawn.
        *
        * @zh
        * 指定如何绘制每一条线段末端。
        */
    get lineCap(): __private._cocos_2d_assembler_graphics_types__LineCap;
    set lineCap(value: __private._cocos_2d_assembler_graphics_types__LineCap);
    /**
        * @en
        * Brush stroke color.
        *
        * @zh
        * 笔触的颜色。
        */
    get strokeColor(): Readonly<math.Color>;
    set strokeColor(value: Readonly<math.Color>);
    /**
        * @en
        * Fill paint color.
        *
        * @zh
        * 填充绘画的颜色。
        */
    get fillColor(): Readonly<math.Color>;
    set fillColor(value: Readonly<math.Color>);
    /**
        * @en
        * Move path start point to (x,y).
        *
        * @zh
        * 移动路径起点到坐标(x, y)。
        *
        * @param x @en The x-axis coordinate of the target position.
        *          @zh 目标位置的 X 轴坐标。
        * @param y @en The y-axis coordinate of the target position.
        *          @zh 目标位置的 y 轴坐标。
        */
    moveTo(x: number, y: number): void;
    /**
        * @en
        * Adds a straight line to the path.
        *
        * @zh
        * 绘制直线路径。
        *
        * @param x @en The x-axis coordinate of the target position.
        *          @zh 目标位置的 x 轴坐标。
        * @param y @en The x-axis coordinate of the target position.
        *          @zh 目标位置的 y 轴坐标。
        */
    lineTo(x: number, y: number): void;
    /**
        * @en
        * Adds a cubic Bézier curve to the path.
        *
        * @zh
        * 绘制三次贝赛尔曲线路径。
        *
        * @param c1x @en The x-axis coordinate of the first control point.
        *            @zh 第一个控制点的 x 轴坐标。
        * @param c1y @en The y-axis coordinate of the first control point.
        *            @zh 第一个控制点的 y 轴坐标。
        * @param c2x @en The x-axis coordinate of the second control point.
        *            @zh 第二个控制点的 x 轴坐标。
        * @param c2y @en The y-axis coordinate of the second control point.
        *            @zh 第二个控制点的 y 轴坐标。
        * @param x @en The x-axis coordinate of the last control point.
        *          @zh 最后一个控制点的 x 轴坐标。
        * @param y @en The y-axis coordinate of the last control point.
        *          @zh 最后一个控制点的 y 轴坐标。
        */
    bezierCurveTo(c1x: number, c1y: number, c2x: number, c2y: number, x: number, y: number): void;
    /**
        * @en
        * Adds a quadratic Bézier curve to the path.
        *
        * @zh
        * 绘制二次贝赛尔曲线路径。
        *
        * @param cx @en The x-axis coordinate of the starting control point.
        *           @zh 起始控制点的 x 轴坐标。
        * @param cy @en The y-axis coordinate of the starting control point.
        *           @zh 起始控制点的 y 轴坐标。
        * @param x @en The x-axis coordinates of the endpoint control point.
        *          @zh 终点控制点的 x 轴坐标。
        * @param y @en The y-axis coordinates of the endpoint control point.
        *          @zh 终点控制点的 x 轴坐标。
        */
    quadraticCurveTo(cx: number, cy: number, x: number, y: number): void;
    /**
        * @en
        * Adds an arc to the path which is centered at (cx, cy) position with radius r starting at startAngle
        * and ending at endAngle going in the given direction by counterclockwise (defaulting to false).
        *
        * @zh
        * 绘制圆弧路径。圆弧路径的圆心在 (cx, cy) 位置,半径为 r ,根据 counterclockwise (默认为false)指定的方向从 startAngle 开始绘制,到 endAngle 结束。
        *
        * @param cx @en The coordinate x-axis of the central control point.
        *           @zh 中心控制点的坐标 x 轴。
        * @param cy @en The coordinate y-axis of the central control point.
        *           @zh 中心控制点的坐标 y 轴。
        * @param r @en Angle in Radian.
        *          @zh 圆弧弧度。
        * @param startAngle @en The starting angle in radian, measured clockwise from the positive x-axis.
        *                   @zh 弧度起点,从正 x 轴顺时针方向测量。
        * @param endAngle @en The ending angle in radian, measured clockwise from the positive x-axis.
        *                 @zh 弧度终点,从正 x 轴顺时针方向测量。
        * @param counterclockwise @en If true, draws counterclockwise between the two angles. Default is clockwise.
        *                         @zh 如果为真,在两个角度之间逆时针绘制。默认顺时针。
        */
    arc(cx: number, cy: number, r: number, startAngle: number, endAngle: number, counterclockwise: boolean): void;
    /**
        * @en
        * Adds an ellipse to the path.
        *
        * @zh
        * 绘制椭圆路径。
        *
        * @param cx @en The x-axis coordinates of the center point.
        *           @zh 中心点的 x 轴坐标。
        * @param cy @en The y-axis coordinates of the center point.
        *           @zh 中心点的 y 轴坐标。
        * @param rx @en The radius of the x-axis of the ellipse.
        *           @zh 椭圆 x 轴半径。
        * @param ry @en The radius of the y-axis of the ellipse.
        *           @zh 椭圆 y 轴半径。
        */
    ellipse(cx: number, cy: number, rx: number, ry: number): void;
    /**
        * @en
        * Adds a circle to the path.
        *
        * @zh
        * 绘制圆形路径。
        *
        * @param cx @en The x-axis coordinates of the center point.
        *           @zh 中心点的 x 轴坐标。
        * @param cy @en The y-axis coordinates of the center point.
        *           @zh 中心点的 y 轴坐标。
        * @param r @en Radius.
        *          @zh 圆半径。
        */
    circle(cx: number, cy: number, r: number): void;
    /**
        * @en
        * Adds a rectangle to the path.
        *
        * @zh
        * 绘制矩形路径。
        *
        * @param x @en The x-axis coordinate of the top left point of the rectangle.
        *          @zh 矩形起始 x 轴坐标。
        * @param y @en The y-axis coordinate of the top left point of the rectangle.
        *          @zh 矩形起始 y 轴坐标。
        * @param w @en The width of the rectangle.
        *          @zh 矩形宽度。
        * @param h @en The height of the rectangle.
        *          @zh 矩形高度。
        */
    rect(x: number, y: number, w: number, h: number): void;
    /**
        * @en
        * Adds a round corner rectangle to the path.
        *
        * @zh
        * 绘制圆角矩形路径。
        *
        * @param x @en The x-axis coordinate of the top left point of the rectangle.
        *          @zh 矩形起始 x 轴坐标。
        * @param y @en The y-axis coordinate of the top left point of the rectangle.
        *          @zh 矩形起始 y 轴坐标。
        * @param w @en The width of the rectangle.
        *          @zh 矩形宽度。
        * @param h @en The height of the rectangle.
        *          @zh 矩形高度。
        * @param r @en Radius of rectangular rounded corners.
        *          @zh 矩形圆角半径。
        */
    roundRect(x: number, y: number, w: number, h: number, r: number): void;
    /**
        * @en
        * Draws a filled rectangle.
        *
        * @zh
        * 绘制填充矩形。
        *
        * @param x @en The x-axis coordinate of the top left point of the rectangle.
        *          @zh 矩形起始 x 轴坐标。
        * @param y @en The y-axis coordinate of the top left point of the rectangle.
        *          @zh 矩形起始 y 轴坐标。
        * @param w @en The width of the rectangle.
        *          @zh 矩形宽度。
        * @param h @en The height of the rectangle.
        *          @zh 矩形高度。
        */
    fillRect(x: number, y: number, w: number, h: number): void;
    /**
        * @en
        * Erasing any previously drawn content.
        *
        * @zh
        * 擦除之前绘制的所有内容的方法。
        */
    clear(): void;
    /**
        * @en
        * Causes the point of the pen to move back to the start of the current path.
        * It tries to add a straight line from the current point to the start.
        *
        * @zh
        * 将笔点返回到当前路径起始点的。它尝试从当前点到起始点绘制一条直线。
        */
    close(): void;
    /**
        * @en
        * Strokes the current or given path with the current stroke style.
        *
        * @zh
        * 根据当前的画线样式,绘制当前或已经存在的路径。
        */
    stroke(): void;
    /**
        * @en
        * Fills the current or given path with the current fill style.
        *
        * @zh
        * 根据当前的画线样式,填充当前或已经存在的路径。
        */
    fill(): void;
}

这里在start() {}中写启动脚本了,正常情况下,肯定是传值进来的

复制代码
start() {
    const data = {
        id: 1,
        color: [255, 68, 68, 255],
        lineWidth: 10,
        arrowPath: [
            { "x": 270, "y": -270 },
            { "x": 225, "y": -270 },
            { "x": 180, "y": -270 },
            ...忽略...
        ],
    };
    this._Graphics = this.node.addComponent(Graphics);
    this._Graphics.lineWidth = data.lineWidth;
    this._Graphics.lineCap = Graphics.LineCap.BUTT;
    this._WinSize = view.getVisibleSize();
    this._Graphics.strokeColor = new math.Color(...data.color);
    this._Graphics.fillColor = new math.Color(...data.color);
    // 初始化移动开关等数据
    this._StartMove = false;
    this._accumTime = 0;
    this._ArrowPath.length = 0;
    this._OriginPath.length = 0;

    // 拷贝路径,保存原始数据
    for (const p of data.arrowPath) {
        const copy = { x: p.x, y: p.y };
        this._ArrowPath.push(copy);
        this._OriginPath.push({ x: p.x, y: p.y });
    }

    this._drawArrow();
    //使用input需要范围检测的,毕竟多次调用这个脚本,this.node监听不现实
    input.on(Input.EventType.TOUCH_END, this._onTouchEnd, this);
}

2.有数据了,下面就是绘制这条箭头线段了

复制代码
/**
    * 绘制箭头折线 折线和箭头三角分开绘制,防止路径互相干扰
    */
private _drawArrow() {
    if (this._ArrowPath.length < 2) return;
    const pts = this._ArrowPath;
    this._Graphics.clear();
    // 1.绘制折线路径,只构建不提交
    this._Graphics.moveTo(pts[0].x, pts[0].y);
    for (let i = 1; i < pts.length; i++) {
        this._Graphics.lineTo(pts[i].x, pts[i].y);
    }
    // 只执行一次stroke,绘制出来折线路径
    this._Graphics.stroke();
    // 2.绘制头部箭头三角形 给三个点,闭合填充
    const headP = pts[0];
    const nextP = pts[1];
    const dir = this._getDirection(nextP.x, nextP.y, headP.x, headP.y);
    if (!(dir.x === 0 && dir.y === 0)) {
        const arrowSize = this._Graphics.lineWidth;
        const perpX = -dir.y;
        const perpY = dir.x;
        this._Graphics.moveTo(
            headP.x + dir.x * arrowSize,
            headP.y + dir.y * arrowSize
        );
        this._Graphics.lineTo(
            headP.x - dir.x * arrowSize + perpX * arrowSize,
            headP.y - dir.y * arrowSize + perpY * arrowSize
        );
        this._Graphics.lineTo(
            headP.x - dir.x * arrowSize - perpX * arrowSize,
            headP.y - dir.y * arrowSize - perpY * arrowSize
        );
        this._Graphics.close();
        this._Graphics.fill();
    }
}

3.绘制箭头肯定也需要有个方向,sqrt(x*x + y*y)不知道从哪个版本开始支持这个hypot(x, y)了

复制代码
/**
    * 获取从 pointA → pointB 的归一化方向向量,支持任意斜线
    * @param ax pointA x
    * @param ay pointA y
    * @param bx pointB x
    * @param by pointB y
    * @returns 归一化 {x,y}; 距离为0返回零向量
    */
private _getDirection(ax: number, ay: number, bx: number, by: number): { x: number; y: number } {
    const dx = bx - ax;
    const dy = by - ay;
    const len = Math.hypot(dx, dy);
    if (len < 0.0001) {
        return { x: 0, y: 0 };
    }
    return { x: dx / len, y: dy / len };
}

4.线段的移动,最好弄一个开关来控制,开关的控制记得加上范围检测

复制代码
private _onTouchEnd(event: EventTouch) {
    if (this._StartMove) return;
    const uiPos = event.getUILocation();
    // 触摸点转换到本节点本地坐标系(与 _ArrowPath 同一坐标系)
    const localPos = this.node.getComponent(UITransform).convertToNodeSpaceAR(new Vec3(uiPos.x, uiPos.y, 0));
    // TODO:判断点击点是否在箭头路径范围内 记录本次移动的原始位置,用于碰撞后回退
    this._StartMove = true;
}

5.CC中每帧调用的周期函数,就俩一个update一个lateUpdate,帧率还不稳,不想u3d那样有个FixedUpdate,尽量乔正,比如你写背景移动啊,什么的也可以这样用。

复制代码
update(deltaTime: number) {
    if (!this._StartMove) return;
    this._accumTime += deltaTime;
    // 固定子步:减法保留剩余时间,不会丢时间片
    while (this._accumTime >= this.FIXED_DELTA) {
        this._ArrowMove(this.MOVE_SPEED);
        this._accumTime -= this.FIXED_DELTA;
    }
}

6.然后就是移动了,好了,逻辑基本上就这样,动手强的可以看一下,那个...就去下载demo吧

复制代码
/**
    * 箭头移动逻辑
    * @param speed 移动步长
    */
private _ArrowMove(speed: number) {
    if (this._ArrowPath.length < 2) {
        this._StartMove = false;
        return;
    }
    if (this._ArrowPath.length === 2) {
        const p0 = this._ArrowPath[0];
        const p1 = this._ArrowPath[1];
        const dir = this._getDirection(p1.x, p1.y, p0.x, p0.y);
        if (dir.x === 0 && dir.y === 0) {
            this._drawArrow();
            return;
        }

        p0.x += dir.x * speed;
        p0.y += dir.y * speed;
        p1.x += dir.x * speed;
        p1.y += dir.y * speed;
        // TODO 对撞其他箭头线段,则回退原位置
        this._drawArrow();
        //TODO:判断是否飞出屏幕,若飞出则移除
        return;
    }

    // 折线阶段:头部延伸,尾部追赶并pop拐点
    // 头部向前
    const headDir = this._getDirection(
        this._ArrowPath[1].x, this._ArrowPath[1].y,
        this._ArrowPath[0].x, this._ArrowPath[0].y
    );
    this._ArrowPath[0].x += headDir.x * speed;
    this._ArrowPath[0].y += headDir.y * speed;

    // 尾部向前追赶
    const lastIdx = this._ArrowPath.length - 1;
    const tailDir = this._getDirection(
        this._ArrowPath[lastIdx].x, this._ArrowPath[lastIdx].y,
        this._ArrowPath[lastIdx - 1].x, this._ArrowPath[lastIdx - 1].y
    );
    this._ArrowPath[lastIdx].x += tailDir.x * speed;
    this._ArrowPath[lastIdx].y += tailDir.y * speed;

    // 距离足够就移除尾部拐点
    const dist = math.Vec2.distance(
        new math.Vec2(this._ArrowPath[lastIdx].x, this._ArrowPath[lastIdx].y),
        new math.Vec2(this._ArrowPath[lastIdx - 1].x, this._ArrowPath[lastIdx - 1].y)
    );
    if (dist < speed) {
        this._ArrowPath.pop();
    }
    // TODO 对撞其他箭头线段,则回退原位置
    this._drawArrow();
}
相关推荐
oden1 个月前
一个不会游戏开发的人,用 AI 把一款小游戏做到上线了
ai编程·cocos creator·游戏开发
陶锅煮新茄子3 个月前
[cc-2d-kit]CocosCreator2D游戏开发工具包🚀
cocos creator
wgslucky3 个月前
cocos creator构建window程序
cocos creator·cocos2dx·window平台游戏
烛阴4 个月前
用 MCP 调教 AI 代理:让 Cocos Creator 3.8.8 核心逻辑一键全自动生成
typescript·cocos creator
烛阴4 个月前
Cocos Creator 3.x 装饰器实战:让你的代码优雅 10 倍
typescript·cocos creator
winlife_4 个月前
把 Cocos Creator 编辑器接入 AI:Funplay MCP for Cocos 介绍
人工智能·编辑器·ai编程·cocos creator·游戏开发·claude·mcp
LcGero5 个月前
TypeScript 快速上手:泛型与工具类型
typescript·cocos creator·游戏开发
LcGero5 个月前
Cocos Creator 3.x 高维护性打字机对话系统设计与实现
cocos creator·打字机
LcGero5 个月前
Cocos Creator 三端接入穿山甲 SDK
sdk·cocos creator·穿山甲