游戏开发编程技术
综合
游戏平台比例 - 手机游戏50%(中国占比40%)、PC电脑游戏20%(Steam占比大)、主机游戏30%、网页游戏1% Steam抽成30%、Epic抽成12%、Google Play抽成30%、App Store抽成30%;国内游戏分发平台 - https://www.taptap.cn/。 游戏制作难度:解密 > ACT > FPS 建模格式 - OBJ+MTL 分辨率使用IDE默认的1152×648 见源码 Godot .NET导出apk大小为22MiB起,仅通过apksigner压缩签名(Android SDK Build-Tools 位于/build-tools/),并不执行Android SDK编译; 可使用 com.example.$genname 变量符来命名唯一名字,$genname取值项目名并自动转为小写。 勾选Gradle构建后*.aab则44MiB+,*.apk则92MB+,且必须进行完整的Android SDK编译。 清理Godot IDE的数据目录:rm -r ~/AppData/Local/Godot/,~/AppData/Roaming/Godot/ Android设备已有8成支持Vulkan 1.1了,故首选Forward Mobile,导出桌面时,则可临时切换为Forward Plus;如果用来做桌面软件,应启用application/run/low_processor_mode。 Godot核心 游戏制作 游戏动画&骨骼蒙皮 游戏引擎IDE 手机游戏开发
核心概念
特殊: Windows默认对窗口化游戏限制帧速,解除方式:屏幕->显示卡->默认图形设置->“窗口化游戏优化”。 基础: GDS属于动态语言,故也能通过索引名访问属性,obj.["p"] 和 obj.p 等价。 Godot 3D的1个单位等于1米(m); 红色X指两边,绿色Y指上/下,蓝色Z指前/后。 归一化的向量表示向量的方向,不归一则表示位置。 3D节点首选Transform控制平移、旋转、缩放等,尽量少用2D属性Rotation。 Node只描述树形层次,不考虑视觉呈现,Node2D和Node3D则增加了位置(中心点为准)和缩放比(整体缩放/尽量少用),但宽高大小均交由其子类处理,比如sprite2D.get_rect()。 资源模型采用开放标准的glTF(.glb、.gltf/字母为L)格式,次选Blender(.blend)格式。 获取程序名 - ProjectSettings.GetSetting("application/config/name").ToString(); 手机: 获取挖孔屏去除孔洞区域的矩形范围 - DisplayServer.GetDisplaySafeArea(); 引擎: 物理模拟关键点是时间恒定,故此类操作应在_PhysicsProcess中处理,而_Process则处理时间不敏感的行为。 物理帧率 - Physics -> Common -> Physics Ticks per Second(物理 -> 通用 -> 每秒物理周期数): 60 物理处理(Physics processing)函数 - _PhysicsProcess(double delta) 固定帧率:只有固定帧率才能正确且平滑的模拟移动和撞击等物理行为,不至于时快时慢的鬼畜。 _PhysicsProcess函数执行时delta值(帧间时间增量)通常为0.016666666666666666 = 1秒 ÷ 60FPS,大于该值就相当于上一帧执行超时了,故与delta相乘(比如位移)是用来弥补其上一帧超时的亏欠。 空闲处理(Idle processing)函数(尽速帧率/默认30FPS?) - _Process(double delta) 尽速帧率:由于_PhysicsProcess维持了定速的物理行为,故其他快慢无所谓的行为可以放入该函数。 内存释放: 首选node.QueueFree(); 次选立即释放的node.Free(); C#则可以补一个node.Dispose(); 只UI移除用remove_child(); 注意 - node.Dispose()不会释放非托管资源,可能会导致内存泄露,且会处于释放后状态,若再调用Free()则报ObjectDisposedException; 故应先调用node.Free()释放底层资源,然后再调node.Dispose()释放C#托管资源。 节点Free()后再访问就要判定下:GodotObject.IsInstanceValid(obj) 或 IsInstanceIdValid(若持有id) 两种核心基类 - Node(手动Free()释放) - Node3D、Control等 Resource(单例/引用计数自动GC) - PackedScene(即tscn场景类型),Texture,GDScript,CSharpScript等 弱引用解决内存泄露 - var wr=GodotObject.WeakRef(obj); Variant obj=wr.GetRef(); 节点关系: 物质实体(MeshInstance3D)的Position移动会过于平稳机械,应让玩家角色体(CharacterBody3D)包裹一下,通过其MoveAndSlide()来模拟真实世界的惯性或滑动。 玩家角色体(CharacterBody3D/Sprite2D)类似可穿透实物的鬼魂,有中心点但无大小,搭配了子节点碰撞体(CollisionShape3D)就穿不透了,有中心点但无大小,设置了Shape才会有碰撞体积; 或给玩家角色体节点内添加了物质实体(MeshInstance3D)等可见体才能被看到。 玩家角色体或StaticBody2D默认无重力下坠,但RigidBody3D则默认会重力下坠。 CollisionShape3D适合规则型形状物体;CollisionPolygon3D适合多边形精细物体 不设置 物质实体(MeshInstance3D) 的Texture材质属性则啥都看不到,IDE中直接拖入图片即可,或:sprite2D.Texture = ImageTexture.CreateFromImage(Image.LoadFromFile(@"D:\x\x.jpg")); 文件存储: 只读文件夹前缀 res:// 或 可写文件夹前缀 user:// 游戏数据或配置存储目录格式为 - user://x.txt 转化为绝对路径 - ProjectSettings.GlobalizePath("res://x.dll") ResourceLoader.Load(...)不识别绝对路径(C:/x.txt这种),路径前缀必须为 res://、user:// 或 uid:// ,未填写则自动补 res:// ,或用外部文件专用类 Image.LoadFromFile(@"D:\x\x.jpg")。 访问绝对路径文件: 文本格式 - FileAccess.Open(fPath, FileAccess.ModeFlags.Read).GetAsText(); var fa = FileAccess.Open(fPath, FileAccess.ModeFlags.Read); var bytes = fa.GetBuffer((long)fa.GetLength()); fa.Close(); 简单k/v存储: var cf = new ConfigFile(); cf.SetValue("path", "k", "v"); cf.SetValue("path", "k2", v2); // 值支持数组、对象等 cf.Save("user://x.ini"); // 存在则覆盖 if(cf.Load("user://x.ini")==Error.Ok){var v=(string)cf.GetValue("path", "k");} // 以下为读取配置文件: if (cf.Load("user://default.ini") == Error.Ok && cf.HasSection("a.b")) { cf.EraseSection("a.b"); cf.Save("user://default.ini"); } // 存在则覆盖 着色器(Shader): 作用 - 为模型或人物的边缘进行发光和描边(Outline)等。 抠图 - 写着色器清除绿幕,配合VideoStreamPlayer节点透明度,来模拟Godot不支持的WebM透明视频 - https://docs.godotengine.org/zh-cn/4.x/tutorials/animation/playing_videos.html#chroma-key-videos 原理 - 顶点着色器 每个3D坐标顶点调用一次vertex()函数;片段着色器 每个像素调用一次fragment()函数。 x.gdshader shader_type canvas_item; void fragment(){ COLOR = vec4(0.4, 0.6, 0.9, 1.0); }
产品实务
根节点: 根节点通常仅占位而已,故首选基类Node,次选Node3D(红色)、Node2D(紫色/基类为CanvasItem)、Control(多个Anchor锚点布局/绿色/适合HUD等),其他节点类型也可用但适用场景比较特化。 当Control的Anchor布局(绿圈加号图标)乏力时可结合更高级的CenterContainer来简化此过程,被其包裹后直属节点的Anchor配置将失效,全权由其接管。 CenterContainer是只居中不缩放,AspectRatioContainer则是保持宽高比率缩放。 容器布局: HBoxContainer属性theme_override_constants/separation用于设置子节点间距;若想4面均设置,可用MarginContainer的theme_override_constants/margin_top等。 常用: 设置所处碰撞层 - area2D.CollisionLayer = 0b1101; // 倒着数,1为勾中 地板 - 通常用 StaticBody3D + 子节点(CollisionShape3D.Shape属性:无限平面WorldBoundaryShape3D) + (MeshInstance3D.Mesh属性:有限平面PlaneMesh) IDE中物体(RigidBody3D)下陷地板一部分后,实际运行的时候会整体顶到地板上方。 地板和上方物体均应设置碰撞体(CollisionShape3D等),否则会互相穿透或无限坠落。 CollisionShape3D不可见时(visible=false)碰撞体仍然有效,disabled=true时方失效。 俩WorldBoundaryShape3D对撞无效,会报:Collisions between world boundaries are not supported 鼠标、键盘: 鼠标位置 - GetViewport().GetMousePosition(); 不松则每帧触发 - Input.IsActionJustReleased("ui_accept"); 不松也仅按下时的那帧触发 - Input.IsActionPressed("ui_accept")) 键盘键值发送和接收: var iek = new InputEventKey(); iek.CtrlPressed = true; iek.ShiftPressed = true; iek.AltPressed = true; iek.Keycode = Key.Escape; iek.Pressed = true; // 执行后松开 Input.ParseInputEvent(iek); // 模拟键盘按键 // 或 模拟鼠标: var ie = new InputEventMouseButton() { ButtonIndex = MouseButton.Left, Position = GetViewport().GetScreenTransform() * GetGlobalTransformWithCanvas() * new Vector2(10, 20), }; Input.ParseInputEvent(ie); // 只会被Godot程序感知:Ctrl+Shift+Alt+Escape public override void _UnhandledInput(InputEvent ie) { if (ie is InputEventKey iek) { GD.Print(iek.AsText()); if (iek.Pressed && iek.Keycode == Key.Escape) { GetTree().Quit(); } } } 相机: 玩家节点放2个摄像头,一个第一人称视角,另外放一个俯视角,根据情况切换:GetNode("CharacterBody3D/Camera3D").MakeCurrent(); 视角: 走动和站立动画切换:_PhysicsProcess函数内 if (direction != Vector3.Zero) { ... animationPlayer.Play("walk"); } else { ... animationPlayer.Play("idle"); } 前后左右转向:_PhysicsProcess函数内 ... if (velocity.Length() > 0) { var zx = new Vector2(velocity.Z, velocity.X); var n = GetNode ("player"); // 90度转向(向后则180度):vector2.Angle() n.Rotation = new Vector3(n.Rotation.X, zx.Angle(), n.Rotation.Z); } Velocity = velocity; MoveAndSlide(); 射线: public override void _PhysicsProcess(double delta) { var q = PhysicsRayQueryParameters3D.Create(Vector2.Zero, new Vector2(50, 100)); // q.collide_with_areas = true // 使Area3D也参与碰撞 GetWorld3D().DirectSpaceState.IntersectRay(q).Collider; } // if (r.Count > 0){ 获取射线所及碰撞体 } 上文Create(写死入参)可通过鼠标位置为起点: if (@event is InputEventMouseButton emb && emb.Pressed && emb.ButtonIndex == MouseButton.Left) { float RayLength = 1000.0f; // 冗余些,使用时再计算最靠近处实际线段长度。 var camera3D = GetNode ("Camera3D"); var pos = emb.Position; // GetViewport().GetMousePosition(); var from = camera3D.ProjectRayOrigin(pos); // 摄像头的3D中心点position,不转镜头 var to = from + camera3D.ProjectRayNormal(pos) * RayLength; // 返回归一化后的方向,乘上长度则等比例将射线延长出去。 } AI寻路行为: 说明 - 寻路使用导航节点,通过场景级脚本_PhysicsProcess更新AI组的navAgent.set_target_position; 寻路不使用导航节点的方式(敌人脚本):direction = (playerTarget.Position - Position).Normalized(); AI行为状态简单的话用有限状态机,复杂点的可以上行为树(比如边走路边说话的并行动作Parallel)。 AI寻路参考 - https://www.bilibili.com/video/BV1pY411Z7kR/ 行为树插件(3.x起支持C#) - https://godotengine.org/asset-library/asset/1349 https://bitbra.in/beehave/#/manual/leaf_nodes GDS行为树实例 - https://gdscript.com/solutions/godot-behaviour-tree/ C#/Mono行为树插件 - https://github.com/playajames419/Godot4-BehaviorTree GDS行为树插件 - https://godotengine.org/asset-library/asset/2514 行为树原理参考 - https://www.cnblogs.com/KillerAery/p/10007887.html 导航寻路: NavigationServer3D 不是Node,而是一个API类。 NavigationRegion3D 默认生效范围是根节点,只会“烘培”大于其半径 navigation_mesh.agent_radius 值(乘2即顶视图目标体直径)的物体,青色区域标识。 烘焙 - 会跳过type="PackedScene"的节点,可通过SourceGeometryMode设为GROUPS_WITH_CHILDREN分组解决;或将其放入NavigationRegion3D子节点内也能识别, 烘培测试地板 - https://github.com/godotengine/godot-demo-projects/raw/master/3d/material_testers/models/test_bed/test_bed.glb NavigationAgent 节点是根据设置进来的target_position变换为get_next_path_position()进行position移动。 实例用法 - https://www.danieltperry.me/post/godot-navigation/ https://docs.godotengine.org/en/stable/tutorials/navigation/navigation_using_navigationagents.html#actor-as-characterbody3d 放入移动物体(CharacterBody3D)的节点内后:比如作为Player的子节点,就能持续感知到Player当前的Position了。 na.TargetPosition = Vector3.Zero; 或 navAgent.set_target_position(NavigationServer3D.map_get_closest_point(...)) 或 map_get_closest_point_to_segment(camera_ray射线与最近导航区相交处...) # https://github.com/godotengine/godot-demo-projects/blob/master/3d/navigation/navmesh.gd _PhysicsProcess(double delta) var velocity = Velocity; if (!IsOnFloor()){ velocity.Y -= gravity * (float)delta; } // 启用重力 if (!na.IsNavigationFinished()) { var v = GlobalPosition.DirectionTo(na.GetNextPathPosition()) * Speed / 2; velocity = new Vector3(v.X, velocity.Y, v.Z); LookAt(GlobalPosition + velocity, Vector3.Up, true); // 看向目标 } Velocity = velocity; MoveAndSlide(); [可选/可用代码替代] 可取代NavigationAgent的代码方式 - https://www.bilibili.com/video/BV1Gp4y1Z7db/ https://www.youtube.com/watch?v=og4O_YRayRc 关键代码: # 超出导航区则取最近处的导航区位置 var cp = NavigationServer3D.map_get_closest_point(get_world_3d().navigation_map,Vector3(0,0.25,0)) # 起步至终点相连的导航区数组 var points = NavigationServer3D.map_get_path(get_world_3d().navigation_map,global_position,cp,true) func _physics_process(delta): var next_position = points[index] - global_position ... 代码有缺失,请参看视频教程中完整版(其未提供源码工程) ... Mesh网格: SurfaceTool编程式生成网格 - https://docs.godotengine.org/zh-cn/4.x/tutorials/3d/procedural_geometry/surfacetool.html [比SurfaceTool更底层]编程式生成复杂MeshInstance3D网格 - https://docs.godotengine.org/en/stable/classes/class_arraymesh.html#description [适合实时生成]编程式生成网格 - https://docs.godotengine.org/zh-cn/4.x/classes/class_immediatemesh.html 修改Mesh - https://docs.godotengine.org/zh-cn/4.x/classes/class_meshdatatool.html 碰撞: 可通过MeshInstance3D的工具栏“网格->创建xxx碰撞同级”生成严丝合缝的CollisionShape3D碰撞体。 或 通过(Blender等)3D建模物体名后缀识别为碰撞体 - https://docs.godotengine.org/zh-cn/4.x/tutorials/assets_pipeline/importing_3d_scenes/node_type_customization.html 穿透 - 空心的ConcavePolygonShape3D(“创建Trimesh(三角网格)碰撞同级”)会穿透WorldBoundaryShape3D,仅适用于StaticBody3D,可换用其他“创建...碰撞同级”,或换掉后者。 ConvexPolygonShape3D是实心的,即使对象完全位于其内部,也能够检测到碰撞。 近战武器攻击判定: 添加AnimationPlayer节点,添加轨道:属性轨道 -> 选节点CollisionShape2D属性disabled -> 武器碰撞默认禁用,当动画武器伸出后再“插入关键帧”取消其禁用,进而触发判定。 远程子弹射击: 子弹节点通常用CharacterBody2D或CharacterBody3D,也能用Area2D。 发射出去后不该受玩家枪管视角影响 - https://docs.godotengine.org/zh-cn/4.x/tutorials/scripting/instancing_with_signals.html 3D中展示2D: 直接Node2D或Control,或使用Sprite3D、SubViewport制作跟随3D角色的血条。 NPC对话插件 - Dialogue Manager、Dialogic
游戏版号 - 2017年起中国境内
手机和PC游戏上架必须申请版号!