游戏开发技术 - Godot、Unity(U3D/团结引擎)、Open 3D Engine (O3DE)、Unreal Engine(UE)虚幻游戏引擎 | Steam、Epic 游戏分发平台
游戏开发编程技术
综合
游戏平台比例 - 手机游戏50%(中国占比40%)、PC电脑游戏20%(Steam占比大)、主机游戏30%、网页游戏1% Steam抽成30%、Epic抽成12%、Google Play抽成30%、App Store抽成30%;国内游戏分发平台 - https://www.taptap.cn/。 Steam每年三个新品节,2月、6月、10月。 游戏制作难度:解密 > ACT > FPS 建模格式 - OBJ+MTL 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)
弱引用解决内存泄露 - 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游戏上架必须申请版号!