阅读视图

发现新文章,点击刷新页面。

国家统计局新闻发言人:我国制造业规模有望连续16年保持全球第一

1月19日,国家统计局新闻发言人、总经济师、国民经济综合统计司司长付凌晖在国新办新闻发布会上表示,2025年工业生产呈现增长较快、结构向优、动能向新的特点。作为工业经济的主体,制造业产出规模持续扩大。2025年制造业增加值34.7万亿元,比上年增长6.1%,占GDP的比重稳定在25%左右,制造业规模有望连续16年保持全球第一,门类体系完整的优势更加明显。(证券时报)

国家统计局:推动CPI温和回升的有利因素在累积

1月19日,国家统计局局长康义在国新办新闻发布会上表示,近年来,我国价格水平总体偏低,低位运行。当前,推动CPI温和回升的有利因素在累积。从基本面来看,随着提振消费专项行动有力实施,特别是财政金融协同促内需一揽子政策等增量措施陆续推出,消费需求有望逐步扩大,物价稳定运行有基础。2025年12月份,CPI同比上涨0.8%,涨幅是2023年3月份以来最高。元旦假期食品消费增加,外出就餐、走亲访友、旅游等服务消费较为活跃,9天春节假期也在临近,有助于推动CPI季节性回升。根据目前掌握情况初步来看,1月份以来,鲜果、飞机票、旅游等商品和服务价格总体稳中有涨。从政策支撑来看,行业自律、产能治理成效仍将继续显现,今年将进一步加强重点行业产能调控,完善产品标准体系和质量,有利于价格回升。(证券时报)

恒指午间休盘跌0.99%,恒生科技指数跌1.15%

36氪获悉,恒指午间休盘跌0.99%,恒生科技指数跌1.15%;交通运输、石油石化、消费板块领涨,中原海能涨超6%,中国石油化工股份、彩星玩具涨超1%;医药生物、造纸包装、传媒板块跌幅居前,汇量科技跌超9%,艾美疫苗跌超7%,玖龙纸业跌超3%;南向资金净买入29.9亿港元。

深圳水贝金饰克价达到1200元

1月19日,国际金价和银价再创新高,金银的市场热度早已从交易盘面蔓延至线下。记者采访发现,19日深圳水贝市场的金饰克价达到1200元。“水贝会”小程序显示,金饰克价也同样达到1200元。而在今年1月1日,深圳水贝市场的金饰克价在1126元左右。与此对比的是,19日多个品牌金饰的克价已经超过1450元。(证券时报)

半日主力资金加仓电力设备股,抛售计算机股

主力资金早间净流入电力设备、基础化工、汽车、机械设备、有色金属等板块,净流出计算机、通信、传媒、电子、医药生物等板块。具体到个股来看,中国西电、特变电工、三花智控获净流入23.04亿元、14.58亿元、11.72亿元。净流出方面,岩山科技、金风科技、中际旭创遭抛售39.69亿元、35.3亿元、16.6亿元。(第一财经)

鸿蒙组件封装手把手教程:重复代码拜拜,效率UP UP

做鸿蒙ArkUI开发的兄弟姐妹们,是不是总被重复代码折磨?登录页的确认按钮、购物页的结算按钮,样式一模一样还要写两遍;图片加文字的布局,换个页面又得重新拼一遍——改样式时逐个文件找,维护起来头都大了!其实只要学会组件封装,把重复代码“打包”起来,下次直接拿过来用,效率直接翻倍,还方便团队协作。今天就照着华为官方文档,手把手教你三种核心封装方式,新手也能轻松拿捏!

一、先搞懂:组件封装到底好在哪?

简单说,封装就是把相同或相似的UI样式、布局、逻辑“装起来”,核心好处就三个:

  1. 少写重复代码:写一次能用N次,不用复制粘贴
  2. 维护超方便:要改样式/逻辑,只改封装组件,所有用到的地方自动同步
  3. 团队不吵架:统一组件风格,不用纠结“你写的按钮和我不一样”

鸿蒙里最常用的封装场景就三种:只复用样式、复用样式+布局+逻辑、批量管理多个组件,咱们一个个说清楚。

二、第一种:组件公共样式封装(只复用样式)

适用场景

如果只是多个组件要共用一套样式,比如所有确认按钮都是胶囊形、一样的大小和颜色,就用这种方式。比如登录页的“登录”按钮和购物页的“结算”按钮,功能不同但样式一致,直接封装样式就行。

核心思路

用系统提供的AttributeModifier接口,把公共样式写在一个类里,之后哪个组件要用,直接套用这个类就行。

手把手操作

第一步:封装公共样式类

先创建一个类,实现AttributeModifier接口,把按钮的默认态、按压态样式都写进去:

// 封装按钮的公共样式
export class MyButtonModifier implements AttributeModifier<ButtonAttribute> {
  // 默认是普通按钮,后续可修改
  private buttonType: ButtonType = ButtonType.Normal;

  // 设置按钮类型(比如胶囊形、普通形)
  type(type: ButtonType): MyButtonModifier {
    this.buttonType = type;
    return this;
  }

  // 按钮默认状态的样式
  applyNormalAttribute(instance: ButtonAttribute): void {
    instance.type(this.buttonType); // 按钮类型
    instance.width(200); // 宽度
    instance.height(50); // 高度
    instance.fontSize(20); // 字体大小
    instance.fontColor('#0A59F7'); // 字体颜色
    instance.backgroundColor('#0D000000'); // 背景色
  }

  // 按钮按压状态的样式
  applyPressedAttribute(instance: ButtonAttribute): void {
    instance.fontColor('#0A59F7');
    instance.backgroundColor('#26000000'); // 按压时背景色加深
  }
}

第二步:使用封装好的样式

在需要的页面里,创建样式实例,通过attributeModifier()方法应用到按钮上:

@Entry
@Component
struct AttributeStylePage {
  // 创建样式实例,设置为胶囊形按钮
  modifier = new MyButtonModifier().type(ButtonType.Capsule);

  build() {
    NavDestination() {
      Column() {
        // 直接套用封装好的样式
        Button('确认按钮')
          .attributeModifier(this.modifier)
      }
      .margin({ top: $r('app.float.margin_top') })
      .width('100%')
      .height('100%')
    }
    .title(getResourceString($r('app.string.common_style_extract'), this))
  }
}

小提醒

  1. 这种样式只能用在系统组件上(比如Button、Image),自定义组件暂时不支持
  2. 样式类可以跨文件导出,整个项目都能复用,还能灵活改参数(比如改按钮类型、颜色)

三、第二种:自定义组件封装(样式+布局+逻辑一起复用)

适用场景

如果不仅要复用样式,连布局和逻辑都要复用,比如商品卡片(图片+名称+价格固定布局)、个人信息项(图标+文字+箭头),就适合封装成自定义组件。而且还能让使用方灵活修改部分属性,比如图片大小、文字内容。

核心思路

@Component装饰器把组件“打包”,不变的部分(比如图片和文字的排列方式)写在内部,可变的部分(比如图片地址、大小)用参数暴露出去,让使用方按需配置。

手把手操作

咱们以“图片+文字”的组合组件为例:

第一步:先封装子组件的样式(可选)

分别给Image和Text组件写样式类,方便后续修改:

// 图片组件的样式封装
export class CustomImageModifier implements AttributeModifier<ImageAttribute> {
  private imageWidth: Length = 0;
  private imageHeight: Length = 0;

  // 初始化图片大小
  constructor(width: Length, height: Length) {
    this.imageWidth = width;
    this.imageHeight = height;
  }

  // 提供修改宽高的方法
  width(width: Length) {
    this.imageWidth = width;
    return this;
  }

  height(height: Length) {
    this.imageHeight = height;
    return this;
  }

  // 应用图片样式
  applyNormalAttribute(instance: ImageAttribute): void {
    instance.width(this.imageWidth);
    instance.height(this.imageHeight);
    instance.borderRadius($r('app.float.border_radius')); // 圆角
  }
}

// 文字组件的样式封装
export class CustomTextModifier implements AttributeModifier<TextAttribute> {
  applyNormalAttribute(instance: TextAttribute): void {
    instance.fontSize($r('app.float.font_size_l')); // 字体大小
  }
}

第二步:封装自定义组件

把图片和文字的布局、点击事件都封装进去,暴露可变参数:

@Component
export struct CustomImageText {
  // 图片样式(默认100x100)
  @Prop imageAttribute: AttributeModifier<ImageAttribute> = new CustomImageModifier(100, 100);
  // 文字样式(默认样式)
  @Prop textAttribute: AttributeModifier<TextAttribute> = new CustomTextModifier();
  // 图片资源(必须由使用方传入)
  @Prop imageSrc: PixelMap | ResourceStr | DrawableDescriptor;
  // 文字内容(必须由使用方传入)
  @Prop text: string;
  // 点击事件(可选,使用方按需传入)
  onClickEvent?: () => void;

  build() {
    // 固定布局:图片和文字纵向排列
    Column({ space: 12 }) {
      Image(this.imageSrc)
        .attributeModifier(this.imageAttribute)
      Text(this.text)
        .attributeModifier(this.textAttribute)
    }
    // 点击事件触发
    .onClick(() => {
      if (this.onClickEvent !== undefined) {
        this.onClickEvent();
      }
    })
  }
}

第三步:使用自定义组件

按需传入图片资源、文字、样式和点击事件:

@Component
struct CommonComponent {
  // 自定义图片大小为330x330
  imageAttribute: CustomImageModifier = new CustomImageModifier(330, 330);

  build() {
    NavDestination() {
      Column() {
        CustomImageText({
          imageAttribute: this.imageAttribute, // 传入自定义图片大小
          imageSrc: $r('app.media.image'), // 图片资源
          text: 'Scenery', // 文字内容
          onClickEvent: () => {
            // 点击组件显示提示
            this.getUIContext().getPromptAction().showToast({ message: 'Clicked' })
          }
        })
      }
      .margin({ top: $r('app.float.margin_top') })
      .width('100%')
      .height('100%')
    }
    .title(getResourceString($r('app.string.common'), this))
  }
}

四、第三种:组件工厂类封装(批量管理多个组件)

适用场景

如果项目里有很多零散组件(比如单选框、复选框、输入框),想统一管理,让业务团队通过组件名直接获取使用,就用这种方式。比如传入“Radio”拿单选框,传入“Checkbox”拿复选框,不用逐个导入。

核心思路

@Builder装饰器定义组件模板,再用wrapBuilder函数包装,存入一个Map(键是组件名,值是组件对象),最后导出这个“组件工厂”,使用方按名字取就行。

手把手操作

第一步:定义组件模板

@Builder写全局的组件模板(比如单选框、复选框):

// 单选框组件模板
@Builder
function myRadio() {
  Text($r('app.string.radio'))
    .width('100%')
    .fontColor($r('sys.color.mask_secondary'))
  // 男选项
  Row() {
    Radio({ value: '1', group: 'radioGroup' })
      .margin({ right: $r('app.float.margin_right') })
    Text('man')
  }
  .width('100%')
  // 女选项
  Row() {
    Radio({ value: '0', group: 'radioGroup' })
      .margin({ right: $r('app.float.margin_right') })
    Text('woman')
  }
  .width('100%')
}

// 复选框组件模板
@Builder
function myCheckBox() {
  Text($r('app.string.checkbox'))
    .width('100%')
    .fontColor($r('sys.color.mask_secondary'))
  // 全选
  Row() {
    CheckboxGroup({ group: 'checkboxGroup' })
      .checkboxShape(CheckBoxShape.ROUNDED_SQUARE)
    Text('all')
      .margin({ left: $r('app.float.margin_right') })
  }
  .width('100%')
  // 选项1
  Row() {
    Checkbox({ name: '1', group: 'checkboxGroup' })
      .shape(CheckBoxShape.ROUNDED_SQUARE)
      .margin({ right: $r('app.float.margin_right') })
    Text('text1')
  }
  .width('100%')
  // 选项2
  Row() {
    Checkbox({ name: '0', group: 'checkboxGroup' })
      .shape(CheckBoxShape.ROUNDED_SQUARE)
      .margin({ right: $r('app.float.margin_right') })
    Text('text2')
  }
  .width('100%')
}

第二步:创建组件工厂

把组件模板包装后存入Map,再导出工厂:

// 定义组件工厂Map,键是组件名,值是组件对象
let factoryMap: Map<string, object> = new Map();

// 把组件存入工厂(用wrapBuilder包装@Builder方法)
factoryMap.set('Radio', wrapBuilder(myRadio));
factoryMap.set('Checkbox', wrapBuilder(myCheckBox));

// 导出工厂,供外部使用
export { factoryMap };

第三步:使用工厂里的组件

导入工厂,按组件名获取并渲染:

// 导入组件工厂(路径要按实际项目调整)
import { factoryMap } from '../view/FactoryMap';

@Component
struct ComponentFactory {
  build() {
    NavDestination() {
      Column({ space: 12 }) {
        // 按名字获取单选框组件并渲染
        (factoryMap.get('Radio') as WrappedBuilder<[]>).builder();
        // 按名字获取复选框组件并渲染
        (factoryMap.get('Checkbox') as WrappedBuilder<[]>).builder();
      }
      .width('100%')
      .padding($r('app.float.padding'))
    }
    .title(getResourceString($r('app.string.factory'), this))
  }
}

小提醒

  1. 只有全局的@Builder方法才能用wrapBuilder包装
  2. 从工厂拿的组件,只能在structbuild方法里使用

五、封装后常见问题:直接抄作业就行!

1. 怎么调用子组件里的方法?

三种实用方法,按需选:

方法一:用Controller类(推荐)

定义一个控制器,子组件把方法“交”给控制器,父组件通过控制器调用:

// 定义控制器
export class Controller {
  action = () => {}; // 用来存子组件的方法
}

// 子组件
@Component
export struct ChildComponent {
  @State bgColor: ResourceColor = Color.White;
  controller: Controller | undefined = undefined;

  // 子组件的方法:切换背景色
  private switchColor = () => {
    this.bgColor = this.bgColor === Color.White ? Color.Red : Color.White;
  }

  // 组件初始化时,把方法赋值给控制器
  aboutToAppear(): void {
    if (this.controller) {
      this.controller.action = this.switchColor;
    }
  }

  build() {
    Column() {
      Text('Child Component')
    }.backgroundColor(this.bgColor).borderWidth(1)
  }
}

// 父组件
@Entry
@Component
struct Index {
  private childRef = new Controller(); // 创建控制器实例

  build() {
    Column() {
      // 把控制器传给子组件
      ChildComponent({ controller: this.childRef })
      Button('切换子组件颜色')
        .onClick(() => {
          this.childRef.action(); // 调用子组件方法
        })
        .margin({ top: 16 })
    }
    .width('100%')
    .alignItems(HorizontalAlign.Center)
  }
}

方法二:用@Watch监听

父组件改状态变量,子组件监听变量变化,触发方法:

// 子组件
@Component
export struct ChildComponent {
  @State bgColor: ResourceColor = Color.White;
  // 监听checkFlag变量变化
  @Link @Watch('switchColor') checkFlag: boolean;

  // 变量变化时触发的方法
  private switchColor() {
    this.bgColor = this.checkFlag ? Color.Red : Color.White;
  }

  build() {
    Column() {
      Text('Child Component')
    }.backgroundColor(this.bgColor).borderWidth(1)
  }
}

// 父组件
@Entry
@Component
struct Index {
  @State childCheckFlag: boolean = false;

  build() {
    Column() {
      ChildComponent({ checkFlag: this.childCheckFlag })
      Button('切换颜色')
        .onClick(() => {
          this.childCheckFlag = !this.childCheckFlag; // 改变量
        })
        .margin({ top: 16 })
    }
    .width('100%')
    .alignItems(HorizontalAlign.Center)
  }
}

方法三:用Emitter事件通信

子组件监听事件,父组件发送事件,触发子组件方法:

// 子组件
@Component
export struct ChildComponent {
  // 定义事件ID
  public static readonly EVENT_ID_SWITCH_COLOR = 'SWITCH_COLOR';
  @State bgColor: ResourceColor = Color.White;

  private switchColor = () => {
    this.bgColor = this.bgColor === Color.White ? Color.Red : Color.White;
  }

  // 组件初始化时监听事件
  aboutToAppear(): void {
    emitter.on(ChildComponent.EVENT_ID_SWITCH_COLOR, this.switchColor);
  }

  // 组件销毁时取消监听
  aboutToDisappear(): void {
    emitter.off(ChildComponent.EVENT_ID_SWITCH_COLOR, this.switchColor);
  }

  build() {
    Column() {
      Text('Child Component')
    }.backgroundColor(this.bgColor).borderWidth(1)
  }
}

// 父组件
@Entry
@Component
struct Index {
  build() {
    Column() {
      ChildComponent()
      Button('切换颜色')
        .onClick(() => {
          emitter.emit(ChildComponent.EVENT_ID_SWITCH_COLOR); // 发送事件
        })
        .margin({ top: 16 })
    }
    .width('100%')
    .alignItems(HorizontalAlign.Center)
  }
}

2. 怎么调用父组件里的方法?

超简单!子组件留个回调参数,父组件把自己的方法传进去:

// 子组件
@Component
export struct ChildComponent {
  call = () => {}; // 回调参数,用来存父组件的方法

  build() {
    Column() {
      Button('调用父组件方法')
        .onClick(() => {
          this.call(); // 触发父组件方法
        })
    }
  }
}

// 父组件
@Entry
@Component
struct Index {
  // 父组件的方法
  parentAction() {
    try {
      this.getUIContext().getPromptAction().showToast({ message: 'Parent Action' });
    } catch (error) {
      let err = error as BusinessError;
      hilog.warn(0x000, 'testTag', `showToast failed, code=${err.code}, message=${err.message}`);
    }
  }

  build() {
    Column() {
      // 把父组件方法传给子组件
      ChildComponent({ call: this.parentAction })
    }
    .width('100%')
    .alignItems(HorizontalAlign.Center)
  }
}

3. 怎么实现“插槽”(可变UI部分)?

@BuilderParam!子组件留个位置,父组件按需传入UI内容:

// 子组件
@Component
export struct ChildComponent {
  // 默认空UI
  @Builder
  customBuilder() {}

  // 暴露给父组件的UI参数
  @BuilderParam customBuilderParam: () => void = this.customBuilder;

  build() {
    Column() {
      Text('子组件固定内容')
      this.customBuilderParam(); // 父组件传入的可变UI
    }
  }
}

// 父组件
@Entry
@Component
struct Index {
  // 父组件定义的可变UI
  @Builder
  componentBuilder() {
    Text(`父组件传入的UI`)
  }

  build() {
    Column() {
      // 传入可变UI
      ChildComponent() {
        this.componentBuilder();
      }
    }
    .width('100%')
    .alignItems(HorizontalAlign.Center)
  }
}

4. 怎么传递组件数组,实现循环渲染?

先把组件包装成全局@Builder,再用wrapBuilder封装成数组,最后用ForEach循环:

// 1. 定义全局组件模板
@Builder
function itemBuilder(text: string) {
  Text(text)
    .width('100%')
    .padding(10)
    .borderWidth(1)
}

// 2. 封装成组件数组
const componentArray = [
  wrapBuilder(itemBuilder, '项目1'),
  wrapBuilder(itemBuilder, '项目2'),
  wrapBuilder(itemBuilder, '项目3')
];

// 3. 循环渲染
@Component
struct ForEachComponent {
  build() {
    Column() {
      ForEach(componentArray, (item) => {
        item.builder(); // 渲染每个组件
      })
    }
  }
}

总结

组件封装的核心就是“提取重复,暴露可变”:

  • 只复用样式:用AttributeModifier
  • 复用样式+布局+逻辑:用@Component写自定义组件
  • 批量管理组件:用组件工厂(Map+wrapBuilder)

掌握这三招,再也不用写重复代码,项目维护起来也省心。遇到调用方法、插槽这些问题,直接抄上面的作业就行~ 赶紧把你项目里的重复组件封装起来试试吧!

今年广州力争实现项目投资超3800亿元

1月19日,广州市第十六届人民代表大会第六次会议开幕,广州市市长孙志洋向大会作政府工作报告。报告总结了过去一年广州的发展成就,并对今年的各领域工作划出重点。报告提出,2026年要强化有效投资牵引带动。加强重点项目谋划储备和资源要素保障,实施工业投资跃升计划,推进“五个一批”项目全流程管理,推动851个市重点项目年度实现投资3800亿元以上。(南方+)

在AI时代下,技术人应该脱离再等等思维

为什么“再等等”是毒药?

你对自己说“再等等”,这听起来像很谨慎,但本质上是--延迟面对现实。

再等等的毒药海报.png

再等等都真正含义是:

让自己再多一个不发布的理由

不是你还差一点。

而是你给自己一个永远合理的缓冲区。


完善,永远没有终点

你永远可以说:

  • 功能还不够全

  • 体验还能再顺一点

  • 架构还能再稳一点

所以问题从来不是:


什么时候够好?

而是:

你有没有一个必须发布的时间点?

一个你必须接受的判断句

没有发布时间点的项目,本质上是不打算发布。

不是你不想,而是你在结构上允许自己一直逃避。


可售性优先原则(核心思想)

什么是“可售性”?

一句话:有人愿意为“当前状态”付钱,而不是为你脑海里的未来版本。

可售性优先原则海报.png

这里要忽略三件事:

1️⃣ 潜力

2️⃣ 愿景

3️⃣ 长期规划

这些东西都不重要。


可售性我们只关心一个问题

现在有没有愿意掏钱?

不是等你做完,不是等你优化,不是等你“再完善一点”。

而是现在。


可售性 vs 完整性

其实很多技术人就是分不清这个概念,我们用两个概念的承诺方式做对比。

完整性的承诺是:

1️⃣ 我会把它做好

2️⃣ 我会把边角补齐

3️⃣ 我会对所有的情况负责

它带来的感受是:

👉 安心。

可售性的承诺是:

1️⃣ 我现在就解决一个具体问题

2️⃣ 不完美,但有用

3️⃣ 今天就能交付

它带来的感受是:

👉 清醒。

完整vs可售对比插画.png


请记住,完整性让你安心,可售性让你清醒。一个让你舒服,一个让你面对现实。

耿乐再创业项目“青初于蓝”已完成两轮融资

36氪获悉,蓝城兄弟创始人耿乐近日对媒体透露,他的再创业项目“青初于蓝”已完成天使轮、A轮两轮融资,融资金额数千万元人民币。耿乐表示,青初于蓝这家公司要做的是“AI时代背景下的社交体验与效率变革的产品实践”,未来将继续在社交赛道深耕。他之前创立的蓝城兄弟于2020年在纳斯达克上市,后于2022年退市,耿乐本人出售股权后离开公司。

A股三大指数午间休盘涨跌不一,大连圣亚涨停

36氪获悉,A股三大指数午间休盘涨跌不一,沪指涨0.13%,深成指跌0.01%,创业板指跌0.64%;石油化工、餐饮旅游、发电设备板块领涨,大连圣亚涨停,恒力石化、东方电气涨超6%;互联网、教育、文化传媒板块跌幅居前,视觉中国跌停,壹网壹创跌超8%,豆神教育跌超2%。

敢不敢挑战用Cocos3.8复刻曾经很火的割绳子游戏?

在这里插入图片描述

引言

哈喽大家好,我是亿元程序员,最近有小伙伴私信笔者:

亿哥,不知道你有没有玩过上面那款16年前火遍全网的割绳子游戏。

我最近在做游戏时,需要做类似这个游戏里面的绳子效果,不知道怎么实现!

你最近不是在更新热门小游戏实战合集吗!

敢不敢挑战用Cocos3.8复刻一个?

好家伙,这款游戏笔者最熟悉不过了,那时候苹果4才刚出来没多久,这样的触屏物理游戏可以算得上人手一个。

关于割绳子的物理效果,我记得之前参与论坛投稿活动时,参考某个砍树游戏出过一期:

翻了一下,看到了一些扎心的评论:

你这是棍子吧

言归正传,本期带大家一起来看看,如何在Cocos游戏开发中,如何实现不像棍子的绳子效果,并且实战做一个曾经很火的割绳子游戏。

本文源工程可在文末获取,小伙伴们自行前往。

回顾一下

想要Cocos游戏开发中,实现一条带有物理效果的绳子,可以使用我们系统自带的距离关节Distance Joint

距离关节会将关节两端的刚体约束在一个最大范围内。超出该范围时,刚体的运动会互相影响。

来源于Cocos官方文档

既然如此,为什么之前做的绳子像棍子?

关节太少

因为之前做的demo,钩子和物品之间,仅仅使用了一个距离关节,所以很难模拟出来绳子柔软的效果。

那么不像棍子的绳子怎么做?

增加足够多的关节

首先制作一节5*20的绳子,属性组件如下:

理论上只要添加足够多的关节,就会越来越接近柔软的绳子效果,因此我们可以通过代码去控制生成足够多的关节,一节连一节。

效果如下,不过绳子看起来并不是连续的,关节因为重力效果,会被拉开一段距离。

这个问题怎么解决?

画线辅助

为了解决受重力效果导致的绳结断开的问题,我们可以通过Graphics组件逐点进行画线,代码如下。

效果如下,但是看起来还是有点问题,绳子连接处不太和谐,不像一根绳子,反而像哼哼哈嘿的双截棍:

因此我们需要把绳子画得更加平滑一点。

平滑曲线

想要让曲线更加平滑,通常可以采用二次贝塞尔曲线连接相邻点,首先我们先要把所有的点位收集起来:

核心的绘制方法quadraticCurveTo

这样看起来就合理一点了:

通过上述流程,我们就能成功实现一根不像棍子的绳子了。

接下来我们完成割绳子游戏的剩余部分。

割绳子游戏实战

有了上面的绳子基础,我们想要实现一个完整的割绳子游戏就易如反掌了。

1. 割绳子

既然是割绳子游戏,割当然是也是重要的一环,割绳子最主要判断手指移动的轨迹与实际上哪一段关节相交。

判断相交的核心方法如下:

2. 切割效果

为了增加游戏效果,我们可以在屏幕上画线时,增加一段拖尾效果。

效果如下:

3. 其他游戏元素

除去绳子相关的部分,剩下就是割绳子游戏的其他元素,包括:

  • 钩子: 主要起到固定绳子的作用,需要添加刚体组件,Type设置为Static

  • 甜甜圈: 需要添加碰撞组件、管理脚本以及刚体组件,需要开启Enabled Contact Listener,在对应的管理脚本进行处理碰撞事件。 如下碰撞到星星和目标点时进行处理。

  • 星星: 添加碰撞组件和管理脚本,增加一些简单的动画。 旋转和碰撞放大效果。

  • 目标点: 目标点和上面差不多,主要是用来判断是否到达目标点,以及添加一些到达动画。

  • 其他: 随着游戏关卡地不断增加,会有越来越多的其他游戏元素,笔者只实现了以上基础的部分,感兴趣的小伙伴们可以自行扩展。

4. 效果演示

在这里插入图片描述

结语

割绳子游戏真的是回忆杀,勾起笔者的无限回忆,等我退休了,一定要完完整整复刻一个,一边复刻一边玩。

小伙伴们,你们玩过这款游戏吗?那时候的你们进入游戏行业了吗?

本文实战完整源码已集成到亿元Cocos小游戏实战合集,内含体验链接。


我是"亿元程序员",一位有着8年游戏行业经验的主程。在游戏开发中,希望能给到您帮助, 也希望通过您能帮助到大家。

AD:笔者线上的小游戏《打螺丝闯关》《贪吃蛇掌机经典》《重力迷宫球》《填色之旅》《方块掌机经典》大家可以自行点击搜索体验。

实不相瞒,想要个爱心!请把该文章分享给你觉得有需要的其他小伙伴。谢谢!

推荐文章:

亿元Cocos小游戏实战合集

Cocos游戏如何接入安卓穿山甲广告变现?

你知道和不知道的微信小游戏常用API整理,赶紧收藏用起来~

Cocos游戏如何快速接入抖音小游戏广告变现?

如何在CocosCreator3.8中实现割绳子游戏效果

如何在CocosCreator3.8中实现动态切割模型?

Cocos游戏开发中的贴花效果

胡润发布中国人工智能50强,寒武纪以6300亿元价值居首

1月19日,胡润研究院发布的《2025胡润中国人工智能企业50强》显示,AI芯片企业寒武纪以6300亿元的价值位居榜首,比上年增长165%;国产GPU第一股摩尔线程排名第二,价值3100亿元;中国首批实现全流程国产化的高端GPU企业沐曦排名第三,价值2500亿元。(证券时报)

马蜂窝与梵净山达成战略合作

36氪获悉,近日,粤铜旅游行业联谊会暨2025年梵净山旅游年会在贵州铜仁举行,马蜂窝旅游集团创始人、CEO陈罡与梵净山文旅集团党委书记、董事长简庆刚正式签署战略合作协议。双方将围绕AI智能体服务平台建设、目的地深度玩法开发、全域旅游资源整合营销推广等方面展开全面合作,共同推动以梵净山为中心的铜仁文旅产业提质升级。

2025年新建商品房销售8.39万亿

1月19日,国家统计局公布《2025年全国房地产市场基本情况》和《2025年12月份70个大中城市商品住宅销售价格变动情况》,数据显示,2025年,新建商品房销售面积88101万平方米,比上年下降8.7%;新建商品房销售额83937亿元,下降12.6%;12月份,一线城市新建商品住宅销售价格同比下降1.7%,降幅比上月扩大0.5个百分点,二、三线城市新建商品住宅销售价格同比分别下降2.5%和3.7%;12月份,一线城市二手住宅销售价格同比下降7.0%,二、三线城市二手住宅销售价格同比均下降6.0%。市场继续呈现筑底趋势。

01 商品房库存持续下降

2025年,全国房地产开发投资82788亿元,比上年下降17.2%;其中,住宅投资63514亿元,下降16.3%。

在销售端,2025年新建商品房销售面积88101万平方米,比上年下降8.7%;其中住宅销售面积下降9.2%,至73299平方米。新建商品房销售额83937亿元,下降12.6%;其中住宅销售额下降13.0%。

图源:国家统计局

作为重要的未来预测性指标,2025年房屋新开工面积58770万平方米,下降20.4%。其中,住宅新开工面积42984万平方米,下降19.8%。这意味着未来可售的新房面积持续减少,这一点也成为楼市库存下降的主要原因之一。

2025年末,商品房待售面积76632万平方米,比上年末增长1.6%,比11月末回落1.0个百分点。其中,住宅待售面积同比增长2.8%,但比今年2月末的库存高点下降了2938万平方米,至约4亿平方米。

目前新房市场的库存面积和新开工面积总和只比年销售面积略高。

02 一线市场止跌回暖信号增强

在价格方面,根据统计局数据,12月份,一线城市新建商品住宅销售价格环比下降0.3%,降幅比上月收窄0.1个百分点。其中,上海上涨0.2%,北京、广州和深圳分别下降0.4%、0.6%和0.5%。

二线城市新建商品住宅销售价格环比下降0.4%,降幅扩大0.1个百分点。三线城市新建商品住宅销售价格环比下降0.4%,降幅与上月相同。

图源:国家统计局

二手房市场,12月份,一线城市二手住宅销售价格环比下降0.9%,降幅比上月收窄0.2个百分点。其中,北京、上海、广州和深圳分别下降1.3%、0.6%、1.0%和0.6%。二、三线城市二手住宅销售价格环比均下降0.7%,降幅均扩大0.1个百分点。

58安居客研究院院长张波分析认为,一线市场止跌回暖信号增强。一线城市新建商品住宅环比降幅收窄 0.1 个百分点,其中上海新房环比上涨 0.2%、同比大涨 4.8%,成为一线城市中唯一实现同比、环比双涨的城市,144㎡以上大户型的热销印证了改善型需求的坚实韧性。二手房市场同样呈现改善迹象,一线城市二手住宅环比降幅收窄 0.2 个百分点,上海二手房价环比降幅收窄至 0.6%,需求端观望情绪正在逐步消解。

值得关注的是,今年一月市场止跌信号已经显现。张波表示,近期全国面的政策出台较为密集,贷款利率下调、税费优惠等政策直接降低购房门槛,公积金贷款利率和商贷利率都已达到历史低点。从安居客线上数据来看,政策对用户的拉动作用明显,1月前两周,用户主动发起微聊(用户和经纪人线上文字沟通)对数周环比增长 1.8%,月同比大幅增长 8.6%。发起微聊的用户数周环比增长 1.3%,月同比增长 7.5%。用户发起留电意愿持续增强,留电用户数周环比增长 1.0%,月同比增长 7.1%,但整体来看,用户从线上浏览转向主动咨询的情况不断增多,尤其是多子女家庭、新市民等政策重点支持群体的咨询量增长更为突出,表明市场观望情绪正逐步缓解,买家议价空间收窄,市场定价趋于理性。

重构第三天,我把项目里 500 个 any 全部换成了具体的 Interface,然后项目崩了😭

0_GvAg7U_pHLYNUJOj.jpg

开始在重构旧项目的最近一个月,我每天打开项目代码,心情都像是在上坟😖。

这个项目是5年前的老代码,说是用了 TypeScript,但含 any 量高达 80%。

User 是 any,Response 是 any,连 window 也是 (window as any)。

每次写业务,我都得猜这个 res.data.list 到底是个数组,还是个 null,还是后端心情不好传回来的一个空字符串。

作为一个有代码洁癖的资深前端,我忍不了😡。

每周五下午,趁着业务需求的空窗期,决定搞个大扫除。

目标是:干掉核心模块里的所有 any,还 TypeScript 一个明确的类型。

过程是 - 爽是爽了

那个周末我是在多巴胺的海洋里度过的。

我对着接口文档,把那些面目可憎的 any 一个个替换成了极其优雅的 Interface。

Refactor 前:

// 屎山代码
function renderUser(user: any) {
    const name = user.info.name; // 这里的 info 可能是 undefined,但 TS 不报错
    const age = user.age.toFixed(2); // 这里的 age 可能是 string,TS 也不报错
    return `User: ${name}, Age: ${age}`;
}

Refactor 后:

// 优雅代码
interface UserInfo {
    name: string;
    avatar?: string;
}

interface User {
    id: number;
    info: UserInfo; // 必须存在
    age: number;    // 必须是数字
}

function renderUser(user: User) {
    // 此时 IDE 智能提示全出来了,简直丝滑
    return `User: ${user.info.name}, Age: ${user.age.toFixed(2)}`;
}

看着 VS Code 里那一个个红色的波浪线被我修好,看着 TS 编译通过的绿色对勾,我感觉自己就是代码界的上帝🥱。我甚至顺手把以前代码里那些丑陋的像 if (user && user.info) 防御性判断给删了—— 因为 Interface 定义里写了,info 是必选属性,不可能为空!

周一早上,我信心满满地提了 Merge Request,顺便把代码推上了测试环境。

我心想:兄弟们,以后你们调接口都有智能提示了😁。

事故发生了:P1 级白屏,我在群里被 @ 成了筛子

上午 10 点的时候,测试环境崩了。

image.png

image.png

上午 10 点半,技术总监冲到我工位:你动核心模块了?怎么列表页全白了?控制台全是报错!

我一愣:不可能啊,我本地编译全通过了,TS 类型检查也是完美的。

打开控制台一看,我傻眼了。满屏红字:

  • Uncaught TypeError: Cannot read properties of undefined (reading 'name')
  • Uncaught TypeError: user.age.toFixed is not a function

image.png

怎么会?

我明明定义了 interface User,里面写死了 info 必须存在,age 必须是 number 啊!

真相是,原来TypeScript 是最大的骗子😡

经过两个小时的狼狈排查,我终于明白了那个让所有 TS 新手都会摔坑的真理:

TypeScript 的类型,在运行时(Runtime)就是个屁!

后端的嘴,骗人的鬼

Interface 定义里,我信了后端的文档,写了 age: number。

但实际上,老数据里有几百条数据,age 存的是字符串 "18"😡。

以前用 any 的时候,JS 隐式转换还能跑(或者之前压根没调 toFixed)。

现在我为了规范,加了 .toFixed(2),因为 TS 告诉我它是数字。

结果运行时,浏览器拿着字符串 "18" 去调 toFixed,直接炸穿😥。

必选属性莫名其妙的消失

Interface 里我写了 info: UserInfo(必选)。

于是我自信地删掉了 if (user.info) 的判空逻辑。

结果后端返回的数据里,因为历史脏数据,真的有几个用户的 info 是 null😡。

TS 编译时很开心,浏览器运行时直接抛出 Cannot read properties of null。

我把编译时的类型安全,误当作了运行时的数据安全。

我以为我在写 Java,其实我还在写 JavaScript。TS 编译成 JS 后,那些漂亮的 Interface 全都被擦除得干干净净,裸奔的数据依然是那个烂样子。

反思反思反思:这 500 个 any,原来是护身符😥

看着回滚后的代码,那个丑陋的 any 重新回到了屏幕上,我竟然感到了一丝安全感。

这次事故给了我三个血淋淋的教训:

别太迷信文档,要信数据。

后端的 Swagger 文档写写而已,你真信了 Required,上线就得背锅。

TS 是协定,Zod 才是严格的。

如果你真的想保证数据类型安全,光写 Interface 没用(那只是给 IDE 看的)。你得上运行时校验库(比如 Zod 或 Runtypes)。

// 这才是真安全
const UserSchema = z.object({
    age: z.number(), // 运行时如果拿到 string,这里直接抛错,而不是等到业务逻辑里炸开
});

防御性编程永远不要删 !!!

不管 TS 告诉你这个字段多一定存在,只要数据源来自网络(API),老老实实写 Optional Chaining (user?.info?.name)。

最后的结局

那天下午,我默默地把自己写的那些 interface 改成了原样:

interface User {
    // 认怂了,全部改成可选 + 联合类型😒
    age?: number | string; 
    info?: UserInfo | null;
}

虽然代码里又要加回那些烦人的 if 判断,虽然类型提示没那么完美了,但至少——它不崩了😒。

如果你也想重构项目里的 any,听哥一句劝:

除非你上了 Zod 做运行时校验,否则那个丑陋的 any,可能是你项目里最后一道防线。

分享完毕🙌

谢谢大家.gif

❌