游戏制作、关卡设计、活动策划 | Godot GDS(GDScript)
游戏制作、关卡设计、活动策划 | Godot GDS(GDScript)
综合
发行商 - 出渠道推广游戏,拿走游戏纯利润30%分成。 GDS混淆插件 - https://github.com/cherriesandmochi/gdmaim 3D = Three-dimensional 命名: Main - 主节点,用Node。 Camera3D - 摄像头,2D用Camera2D。 WorldEnvironment - 光照。 Game - 3D层,用Node3D。 Ground - 地面,用StaticBody3D。 Player - 玩家角色,用CharacterBody3D。脚本名为Player.cs Door - 门窗,用AnimatableBody3D。 HUD - Heads-up display,用Node2D。 Game2D - 或GameTwo,2D层,用Node2D。 模型朝向约定 地图、地形等都约定+X为东、-X为西。 Godot IDE前视图的入眼方向为Z轴,即Vector3.ModelFront(出眼-Z则用Vector3.Forward),故模型资源也应该用这个入眼朝向;LookAt(...)尾参为true则会使模型的面部朝向目标。 旋转顺序 - Godot组合时默认为Euler YXZ,分解时则倒序。 重要组件 - 组件化最外层节点:独有MoveAndSlide()的CharacterBody3D和都有MoveAndCollide()的StaticBody3D、RigidBody3D Godot 4.3+ 允许CollisionShape3D等碰撞体处于间接(孙)节点 - https://github.com/godotengine/godot/pull/77937 默认重力数值 - float gravity = ProjectSettings.GetSetting("physics/3d/default_gravity").AsSingle(); SpringArm3D(弹簧臂) - 作用:摄像头通常跟随在角色身后一段距离,当关门之后它将无法穿透门板(具有碰撞体)拍到角色,故应将摄像头包裹在弹簧臂内,其会自动缩距(发射线检测碰撞体),直至拍到角色。 用法:SpringArm3D节点的中心点即发射起点,浅蓝色的射线未碰撞时则将所有子节点(摄像头等/方位由SpringArm3D管理)临时弹出至射线终点,一旦遇到碰撞体,就将子节点全部回缩至射线最近的碰撞点。 排除角色自身碰撞体 - springArm3D.AddExcludedObject(this.GetRid()); 注意 - 加了SpringArm3D后,编辑器和运行时的摄像头位置可能会不一致,以后者为准。 视角: 第一人称 - 摄像头在眼睛位置,摄像头组件直接放角色节点内即可;第三人称 - 摄像头在身后或头顶,且跟随自身旋转,摄像头要放入SpringArm3D内来实现;俯视角 - 在空中跟随而已,并不会随着角色旋转。 第三人称视角 - 左右移动鼠标,角色同步移动,我们只能看到角色的背面 左右移动鼠标,角色静止不同,我们可以看到角色的全方向 注意 - 第三人称视角实现时,似乎都是旋转CharacterBody3D内的Mesh体,是否标准做法? https://www.bilibili.com/video/BV1Eu4y1i7is/ https://www.bilibili.com/video/BV1Ny4y1q71z/
强记(冗余)
GDS变量无法引用函数,只能用Callable包装下或self.call(funcVar);首选不会重复搜索函数表的前者。 GDS Lambda写法:button_pressed.connect(func(): print("hi!")) GDS不支持括号强转:print((int)1) # 语法报错 非类型化数组不能as转换至带类型的,必须新构造后再转:Array([], TYPE_INT, "", [0, 1]) as Array[int] # obj.is_typed()=true Variant即动态类型,永不为null,若实值判null要用: n.Get("blend_shapes/browDownLeft").VariantType != Variant.Type.Nil ResourceLoader.Load(...)不识别绝对路径(C:/x.txt这种),路径前缀必须为 res://、user:// 或 uid:// ,未填写则自动补 res:// ,或用外部文件专用类 Image.LoadFromFile(@"D:\x\x.jpg")。 路径规范化:var da=DirAccess.open(pDir); da.change_dir("../x/"); da.get_current_dir(true)+"/x.txt" 下载文件: var hr = HTTPRequest.new() add_child(hr) hr.request("https://congci.com/.well-known/static/images/core/logo.svg") var r=(await hr.request_completed)[3] print(r.get_string_from_utf8())
最佳实践
CharacterBody3D转向时应整体操作,不能只转换其节点内部的Mesh体,若需要跟随(摄像头等)可将子节点放外部,并结合RemoteTransform3D使用。 即时存档比手动存档更流畅。
工作流
建模: Godot直接支持.glb(首选)、.gltf,若已安装blender并填写编辑器设置filesystem/import/blender/blender3_path=C:/Program Files/Blender Foundation/Blender 4.0/,则会自动将.blend转为.gltf。 Blender出模型和设置碰撞区域(网格名加后缀-convcol)、导航网格NavigationMesh。(基于网格名后缀自动原样创建 用法视频) 非网格的Blender Empty Objects(仅*.dae?)带上-colonly后缀,也会识别为各种XxxShape3D。 [大地模型]Blender - File->New->General->将Cube改名为Cube-convcol,并修改Scale的X/Y为5,Z改为0.1,若灯光过亮和勾上闭眼。 或直接从Blender导出Godot识别的*.escn文件格式 - https://github.com/godotengine/godot-blender-exporter 建模和编程: 模型导入 - Godot支持“场景”和“AnimationLibrary”两种,导入AnimationLibrary后文件后缀不会变,但只会保留动画,忽略其他资源。 场景用法:直接拖拽至3D视图中 或者 右键“实例化子场景”,当前不建议用“新建继承”,资源文件全权由设计人员修改;若做了继承,部分资源(动画等)就能被其他节点勾选上了。 AnimationLibrary用法:AnimationPlayer->动画->管理动画->加载库;若为glb场景节点还应将其自身勾选至AnimationPlayer.root_node。 升级网格表面弹框 - “仅升级”指每次打开项目都会生成至内存中,不写入磁盘;“重启并升级”指写盘式升级,无法回退或降低版本。 语法: int to string - str(123) 等待信号完成: var file = await ToSignal(fd, FileDialog.SignalName.FileSelected); GD.Print(file[0]); // ToSignal(...)返回值即该信号的入参数组。 转换\u前缀至实际字符("弥补String方法不足".c_unescape()): C#可直接用 System.Text.RegularExpressions.Regex.Unescape(r); 而GDS则: var s = GDScript.new() # CSharpScript未测。 s.source_code = "static func f():\n return '%s'" % "\\u4E2D" s.reload() print(s.call("f")) 编程: Godot快捷键: 2D视图 - Ctrl+F1 3D视图 - Ctrl+F2 3D视图默认视角 - Ctrl+Shift+W 重开场景 3D视图前视图 - 数字键盘1 或 添加快捷键值 Alt+1 性能 - GD.Load载入后资源即驻留在当前Node对象,等当前节点被Free()后,该资源也就自动销毁。 常用 - GetTree().Paused = true; quit(); 除canvasItem.SetVisible(false)外优先用x.Hide() 获取分组节点 - GetTree().GetNodesInGroup("name"); 或 GetTree().GetFirstNodeInGroup("name"); 根视口Viewport(即含Autoload节点的Window) - GetTree().Root; 根节点(永在单例节点之后/using System.Linq;) - GetTree().Root.GetChildren().Last(); 按名遍历子节点(性能没“场景唯一节点”GetNode("%Name")高/window.FindChild永为null) - var firstByName = (Node3D)GetTree().Root.GetChildren().Last().FindChild("name"); 跨层级分组 - node.AddToGroup("name"); 或能序列化的 node.AddToGroup("name", true); 节点自身存取元数据 - node.SetMeta("x", new Variant()); node.GetMeta("x"); CanvasItem面板支持鼠标调整文字颜色,或代码调整(btn.Modulate会影响子节点颜色值):btn.SelfModulate = Colors.Red; 捕获鼠标 - Input.MouseMode = Input.MouseModeEnum.Captured; 代码映射输入键值 - Action同名则覆盖 var key = new InputEventKey(); key.Keycode = Key.W; InputMap.AddAction("new_key"); InputMap.ActionAddEvent("new_key", key); 攻击判定 - 近战: CollisionShape3D - 动画第0帧至伸出武器帧设为disabled,之后设disabled=off,收武器至动画结束重新设为disabled。 射线检测 - 攻击时发射一连串射线进行碰撞检测。 摔死: public override void _Process(double delta) { if (Velocity.Y < -20) { GD.Print("摔死"); GetTree().ReloadCurrentScene(); } } 核心: 3D物体由朝向(Basis)+原点(Origin/中心点)来定位(属性名为Transform),默认值为Transform3D.Identity;原点默认值为(0,0,0),远离父节点的原点时,其会变为实际距离值;朝向默认值为Basis.Identity,数值范围只有0、1、-1三个。 获取模型宽高和深度 - AABB即axis aligned bounding box (轴对齐-边界盒) 原点y值朝上正数,向下负数;当前朝向乘上即将移往的方向并归一化,就能得到该帧的移动偏移量,加负号则翻转方向。 移动碰撞模拟性检查 - CharacterBody3D.test_move(...) 四方向移动,最简未优化写法 - _PhysicsProcess(double delta): var v2 = Input.GetVector("ui_left", "ui_right", "ui_up", "ui_down"); // 获取输入量,但无物体定位信息。 Velocity = (Transform.Basis * new Vector3(v2.X, 0, v2.Y)).Normalized() * Speed; // 乘当前物体的空间位置Basis得出基于此定位的移动量,未处理旋转和跳落。 MoveAndSlide(); // 加负号则移动方向互换:... - new Vector3(v2.X, 0, v2.Y) ... 说明 - 官方移动模板的Godot.Mathf.MoveToward(...)作用仅仅是让物体平滑回落至Vector3.Zero偏移量。 绕轴旋转:局部变换;全局转换 node3D.RotationDegrees可设置绝对角度,node3D.RotateY(angle)则为相对角度。 360°度数(deg/RotationDegrees属性)=6.28...弧度(rad/Rotation属性/PI * 2/Mathf.Tau) //RotateY(-0.5f); // 绕Y轴(上下轴=左右)旋转;若不想用负号,可采用下行直观的视角旋转 RotateObjectLocal(Vector3.Down, 0.5f); // 绕Y轴=左转Up和右转Down(即RotateY负数值) 朝向目标: LookAt(targetNode.Transform.Origin + direction); // LookingAt比较平滑: //Transform = Transform.InterpolateWith(Transform.LookingAt(targetNode.Transform.Origin + direction), (float)delta * Speed); FPS自由视角: 弧度方式 - 第一人称时该脚本放玩家节点,第三人称(Third-person POV)时放SpringArm3D节点(用上SpringArm3D的Y轴) 第三人称跟随SpringArm3D方向移动:var dir = (Transform.Basis * new Vector3(inputDir.X, 0, inputDir.Y).Rotated(Vector3.Up, sa.Rotation.Y)).Normalized(); public override void _Input(InputEvent @event) { if (@event is InputEventMouseMotion mouseMotion) { // Rotation和RotationDegrees属性赋值均支持数值超出后循环映射,或 Mathf.Wrap(x, 0, 9); // 场景文件用弧度存储的Rotation值,因精度因素可能每次转换至RotationDegrees的度数均有出入,故统一用弧度才能精确比对。 var v2 = new Vector2(Rotation.X, Rotation.Y); // 转动前的旋转值 v2 += new Vector2(mouseMotion.Relative.Y, -mouseMotion.Relative.X) * 0.004f; var rad = Mathf.DegToRad(50); var v2x = Mathf.Clamp(v2.X, -rad, rad); // 允许的斜身弧度 //Rotation = new Vector3(v2x, v2.Y, Rotation.Z); // 取代Rotation - 必须先陀螺式(左右)、再车轮式(上下)的旋转顺序 var t3d = Transform; t3d.Basis = Basis.Identity; Transform = t3d; RotateObjectLocal(Vector3.Up, v2.Y); RotateObjectLocal(Vector3.Right, v2x); } } 变量方式 - https://docs.godotengine.org/zh-cn/4.x/tutorials/3d/using_transforms.html#setting-information _rotationY = Mathf.Clamp(_rotationY, -200 * 0.005f, 200 * 0.005f);// 允许的斜身弧度 RotateObjectLocal(Vector3.Up, -_rotationX); // 应为负数;非鼠标移动方式改旋转值不会同步该字段! 播放音频: var asw = new AudioStreamWav(); asw.Format = AudioStreamWav.FormatEnum.Format16Bits; asw.MixRate = 48000; // 必须跟原wav一致,否则影响速度。 asw.Data = bytes; // FileAccess.GetBuffer(...); audioStreamPlayer.Stream = asw; // 非运行时wav文件可换用:GD.Load(trackPath); audioStreamPlayer.Play(); var vsp = (VideoStreamPlayer)GetNode("VideoStreamPlayer"); var vst = new VideoStreamTheora(); vst.File = "x.ogv"; // 仅支持ogv格式 vsp.Stream = vst; vsp.Play(); // vsp.Expand为true则限定视频尺寸。 3D中显示2D内容: var mesh = (MeshInstance3D)GetNode("MeshInstance3D"); var sv = (SubViewport)GetNode("SubViewport"); // 子节点放2D内容 var sm = new StandardMaterial3D(); sm!.AlbedoTexture = sv.GetTexture(); mesh.MaterialOverride = sm;
素材、模型、示例
示例: Godot官方 - https://github.com/godotengine/godot-demo-projects 学习机构示例 - https://github.com/gdquest-demos 资源: 双击资源导入时或重导时均可以在IDE中指定资源的根类型,便于附加具体类型(CharacterBody3D等)的脚本。 地板、场所: 海滩(*.glb) - https://sketchfab.com/3d-models/beach-lowpoly-0425667a812247cabedaa60594bd11b1 两座小山 - https://sketchfab.com/3d-models/elephants-foot-tonalea-arizona-312feac08b344045ba6460864271f03e 卡通城堡 - https://sketchfab.com/3d-models/low-poly-castle-in-ruins-b4beaf78a6784847b4e63da79351a86b 山路台阶 - https://sketchfab.com/3d-models/fort-tryon-park-staircase-6d19979112b541aea02f9b68a66fbf7d 小家带院(*.glb、*.blend) - https://github.com/godotengine/godot-demo-projects/tree/master/3d/physical_light_camera_units 山河高楼(*.glb) - https://github.com/godotengine/godot-demo-projects/tree/master/3d/truck_town 人物动画(*.glb) - https://github.com/godotengine/godot-demo-projects/tree/master/3d/platformer/player
单机
整体: 玩家操作游戏人物打小怪、打大Boss后进入下一关。 《Palworld》买断式游戏可允许玩家自建服务器,分担游戏厂商网络服务压力。 常规做法: 玩家出生后应处于idle动画姿态,移动时应面向前方,并切换至walk动画。 发射物首选射线,或者用RigidBody3D构建,通过ApplyForce施力射出。
其他
当你使用 MyNode.new(value) 创建一个新实例时,Godot 会自动调用 _init(value) 方法: load("res://new_script.gd").new(111) class_name CustomModifier extends SkeletonModifier3D