不能传效果图,可以先看效果:星际应用中心 | 科幻游戏&工具空间站
最近爆款的打螺丝/拧螺丝 休闲小游戏,凭借极简操作、解压反馈和闯关玩法,成为通勤摸鱼神器。今天就用Cocos Creator 3.8+,从零实现一套完整的打螺丝游戏,涵盖物理交互、拖拽拧动、关卡逻辑、音效反馈全流程,新手也能跟着复刻上线。
核心玩法 :点击/拖拽螺丝,将松动的螺丝拧入对应孔位,固定木板完成闯关;拧动到位触发特效+音效,关卡递进提升难度。 技术栈:Cocos Creator 3.8.x、TypeScript、2D物理引擎、触摸/鼠标事件、Tween动画
一、项目初始化与基础配置
1.1 新建项目
-
打开Cocos Dashboard,创建2D项目 ,命名为ScrewGame,引擎版本选择3.8.0及以上
-
清空默认场景,新建Main场景作为游戏主场景
1.2 资源准备(极简素材,新手友好)
无需复杂美术,准备基础素材即可快速开发:
-
螺丝素材:螺丝头部、螺丝杆(2张图,区分未拧入/拧入状态)
-
孔位素材:螺丝孔底图、孔位边框
-
木板素材:不同形状木板(遮挡孔位、增加闯关难度)
-
音效素材:拧螺丝音效、成功固定音效、闯关音效
将素材导入Cocos资源管理器,分类存放至texture、audio、prefab文件夹。
1.3 场景布局搭建
-
创建Canvas节点,适配分辨率设为720x1280(竖屏适配手机)
-
新建节点分层:Bg(背景)、GameLayer(游戏核心层)、UILayer(UI层)
-
GameLayer下创建Board(木板)、ScrewGroup(螺丝组)、HoleGroup(孔位组)
-
UILayer下创建关卡文本、进度条、重新开始按钮
二、核心模块开发(重点)
2.1 螺丝节点与组件配置
螺丝是游戏核心交互对象,需配置物理组件+触摸事件:
-
新建Sprite节点,命名Screw,挂载螺丝头部图片
-
添加核心组件:
-
RigidBody2D :类型设为Kinematic(运动学刚体,避免物理漂移)
-
CircleCollider2D :勾选Enabled,半径适配螺丝大小,开启IsTrigger(触发检测)
-
自定义脚本Screw.ts,挂载到螺丝节点
-
2.2 螺丝孔位模块(Hole)
孔位是螺丝的目标点位,负责定位和碰撞判定:
-
新建Sprite节点,命名Hole,挂载孔位图片
-
添加CircleCollider2D,设为Trigger,用于检测螺丝是否对准
-
制作孔位预制体,方便批量生成关卡
2.3 木板模块(Board)
木板用于增加游戏策略性,部分孔位被木板遮挡,需按顺序拧螺丝:
-
新建Sprite节点,挂载木板图片
-
添加PolygonCollider2D(多边形碰撞体),贴合木板形状
-
添加RigidBody2D,类型设为Static,避免移动
三、核心脚本逻辑实现(TypeScript)
3.1 螺丝控制脚本 Screw.ts
实现触摸拖拽、拧动动画、碰撞判定、固定逻辑,完整代码如下:
import { _decorator, Component, Node, input, Input, EventTouch, Vec3, tween, RigidBody2D, Collider2D, Contact2DType, AudioSource, assert } from 'cc';
const { ccclass, property } = _decorator;
@ccclass('Screw')
export class Screw extends Component {
// 拧动音效
@property(AudioSource)
screwAudio: AudioSource = null!;
// 固定成功音效
@property(AudioSource)
fixAudio: AudioSource = null!;
// 螺丝目标孔位
targetHole: Node = null!;
// 是否已经固定
isFixed: boolean = false;
// 初始位置
private startPos: Vec3 = Vec3.ZERO;
// 拧动旋转角度
private readonly rotateAngle: number = 360;
onLoad() {
this.startPos = this.node.position.clone();
// 注册触摸事件
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);
// 注册碰撞触发
let collider = this.getComponent(Collider2D)!;
collider.on(Contact2DType.TRIGGER_ENTER, this.onTriggerEnter, this);
}
// 触摸开始:选中螺丝
onTouchStart(event: EventTouch) {
if (this.isFixed) return;
let touchPos = event.getUILocation();
let worldPos = this.node.getParent()!.getComponent(UITransform)!.convertToNodeSpaceAR(new Vec3(touchPos.x, touchPos.y));
// 判断是否点击到螺丝
if (Vec3.distance(this.node.position, worldPos) < 50) {
this.node.scale = new Vec3(1.1, 1.1, 1.1);
}
}
// 触摸移动:拖拽螺丝
onTouchMove(event: EventTouch) {
if (this.isFixed) return;
let touchPos = event.getUILocation();
let nodePos = this.node.getParent()!.getComponent(UITransform)!.convertToNodeSpaceAR(new Vec3(touchPos.x, touchPos.y));
this.node.setPosition(nodePos);
}
// 触摸结束:判定是否拧入孔位
onTouchEnd() {
if (this.isFixed || !this.targetHole) {
// 未对准,回归原位
tween(this.node).to(0.2, { position: this.startPos, scale: Vec3.ONE }).start();
return;
}
this.fixScrew();
}
// 碰撞检测:对准孔位
onTriggerEnter(otherCollider: Collider2D) {
if (otherCollider.node.name.includes('Hole') && !this.isFixed) {
this.targetHole = otherCollider.node;
}
}
// 固定螺丝:拧动动画+音效
fixScrew() {
this.isFixed = true;
this.targetHole.active = false;
// 播放拧动音效
this.screwAudio.play();
// 拧动旋转+缩放动画
tween(this.node)
.to(0.3, { eulerAngles: new Vec3(0, 0, this.rotateAngle), scale: Vec3.ONE })
.call(() => {
this.fixAudio.play();
// 通知游戏管理器更新进度
GameManager.instance.checkLevelComplete();
})
.start();
}
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);
}
}
3.2 游戏管理器 GameManager.ts
全局控制关卡进度、螺丝计数、闯关判定、重启游戏,采用单例模式:
import { _decorator, Component, Node, Label, find, warn } from 'cc';
const { ccclass, property } = _decorator;
@ccclass('GameManager')
export class GameManager extends Component {
// 单例实例
static instance: GameManager = null!;
// 当前关卡螺丝总数
@property
totalScrew: number = 0;
// 已固定螺丝数
fixedScrew: number = 0;
// 关卡文本
@property(Label)
levelLabel: Label = null!;
// 通关提示节点
@property(Node)
completeNode: Node = null!;
onLoad() {
// 单例初始化
if (GameManager.instance) {
this.destroy();
return;
}
GameManager.instance = this;
this.completeNode.active = false;
}
// 初始化关卡数据
initLevel(level: number, screwCount: number) {
this.levelLabel.string = `第${level}关`;
this.totalScrew = screwCount;
this.fixedScrew = 0;
this.completeNode.active = false;
}
// 螺丝固定后更新计数,判定通关
checkLevelComplete() {
this.fixedScrew++;
if (this.fixedScrew >= this.totalScrew) {
this.levelComplete();
}
}
// 通关逻辑
levelComplete() {
this.completeNode.active = true;
warn("关卡通关!");
}
// 重新开始游戏
restartGame() {
find("GameLayer")!.children.forEach(child => {
if (child.name.includes("Screw")) {
child.getComponent(Screw)!.resetScrew();
}
});
this.initLevel(1, 5);
}
}
3.3 孔位脚本 Hole.ts
import { _decorator, Component, Node } from 'cc';
const { ccclass, property } = _decorator;
@ccclass('Hole')
export class Hole extends Component {
// 孔位是否被占用
isUsed: boolean = false;
}
四、进阶优化与效果增强
4.1 解压反馈升级
-
动画优化 :螺丝拧入时添加弹性缩放+旋转,增强手感
-
音效分层:拧动过程循环音效,固定成功触发短促音效
-
视觉反馈:固定后螺丝变色,添加粒子特效
4.2 关卡难度递进
-
增加螺丝数量、木板遮挡层数
-
加入顺序拧螺丝逻辑(必须先拧指定螺丝,否则木板无法固定)
-
添加限时闯关模式,提升可玩性
4.3 性能优化
-
螺丝、孔位使用预制体复用,减少节点创建销毁
-
关闭多余物理碰撞,仅保留触发检测
-
音频对象复用,避免频繁创建AudioSource
五、打包发布与适配
-
平台适配 :Canvas适配模式选择SHOW_ALL,兼容手机横竖屏
-
微信小游戏发布:
-
构建平台选择微信小游戏,填写AppID
-
压缩资源、开启代码混淆,减少包体积
-
使用微信开发者工具预览、上传发布
-
-
Web发布:构建为Web平台,部署至静态服务器,嵌入网页引流
六、常见问题排坑
-
螺丝拖拽漂移:将RigidBody2D设为Kinematic,关闭重力
-
碰撞检测失效:检查Collider2D是否开启Trigger,层级是否正确
-
音效不播放:音频格式改为mp3/ogg,AudioSource挂载到节点
-
微信内无法交互:开启触摸事件兼容,适配微信浏览器环境
七、总结与拓展
这款打螺丝游戏核心是物理交互+极简操作,通过Cocos的2D物理、事件系统和动画系统,快速实现解压玩法。在此基础上,还能拓展:
-
加入拆螺丝反向玩法
-
新增螺丝刀道具,升级拧螺丝效率