从此

游戏设计、关卡交互、动画美工、3D建模/2D Tilemap、节点网格、骨骼骨架、WASD视角、AnimationTree/AnimationPlayer


综合/最新

Godot游戏开发 | 知名游戏作品 | 游戏开发文章

游戏动画师 - 游戏开发中重要程度较大的职位。 新: SpringBoneSimulator3D 可用于制作头发。 综合: 资源通过 Resource ID (RID) 存取,对象通过 Instance Id 存取。 版本判定 if (Engine.GetVersionInfo()["hex"].AsInt32() >= 0x040400){ } 元数据判断 无name则直接打印错误,可指定个回落默认值: _Node.GetMeta("name", new GodotObject()); // new Variant() 依然会被判为 null。 或 if (_Node.HasMeta("name") && "x".Equals(_Node.GetMeta("name", String.Empty).AsString())) { return; } // Equals时需要AsString()匹配类型。 AnimationMixer 通过 TrackSetPath 定位骨头,SkeletonModifier3D 实现类则作为 Skeleton3D 子节点来逐帧影响骨骼行为,编辑时和运行时均有效;比如 LookAtModifier3D(或低配版AimModifier3D) 会控制某个骨头(bone_name),一直朝向某节点(target_node)。 开发阶段可视化: var tm = new TextMesh(); tm.Text = "开发中→"; var mi = new MeshInstance3D(); mi.Mesh = tm; // 指定TextMesh字体可解决:doesn't contain self-intersecting lines AddChild(mi); // 朝向某处 mi.LookAt(Vector3.Down); 统一约定设置: 模型姿势: Rest Pose:Godot动画混合推荐双臂平伸的T-Pose;A-Pose 则更符合人类日常姿势,通常取45°值。 有条件的话,建议创建一个1帧的 RESET 动画,用于 AnimationTree 混合或Root Motion的骨骼参考值。 髋关节(hip joint)即大腿根与骨盆旋转处,髋部的通俗叫法为胯部; 从运动角度看,胯(Hips)连接了3部分:脊柱(Spine)及以上、左腿(LeftUpperLeg)及以下、右腿(RightUpperLeg)及以下。 先建立上接脊柱(Spine)的骶骨,然后建立使裆部产生宽度的左右髋关节,在中心连接一起组成三叉形态的胯,也就是骨骼的Root Bone;因胯部跟随上身情况较多,故只将双腿视为下半身。 BoneAttachment3D 附着至骨头的中心点位置是该骨头的起点端;也可不放入 Skeleton3D 子节点(external_skeleton)。 Player/NPC: WASD 移动+鼠标转向控制的常规做法是:Player 根节点只负责平移,而 Mesh 用来控制旋转。各种视角 - https://www.bilibili.com/video/BV1B341157Xb/ _PhysicsProcess 中修改 GlobalPosition 可能会干扰碰撞和 MoveAndSlide(),故最好计算个修正速度并将其添加到角色的 Velocity 里。 NPC 无需 Camera 和 Input.GetVector(...),直接由导航组件控制(Y轴自然贴地):var v = GlobalPosition.DirectionTo(na.GetNextPathPosition()) * Speed / 2; velocity = new Vector3(v.X, velocity.Y, v.Z); 碰撞时是否同层,CollisionMask 决定被碰撞者: if (cb.Name == Autoload.CurrentPlayer().Name) { cb.CollisionMask = 0b00_00000011; // 自右数起;要撑住角色必须启用地面碰撞。 // 若有内嵌则还应 _ShapeCast3D.CollisionMask = this.CollisionMask; areaHitbox.CollisionMask = cb.CollisionMask; // 以及 Area3D 的 CollisionLayer。 } else // NPC等 { cb.CollisionLayer = 0b0_00000010; cb.CollisionMask = 0b00_00000001; } 武器用 Area3D 制作 Hitbox,Hurtbox;body_entered多次碰撞的话,可换用更多入参的body_shape_entered来深入判断。 血条最简单做法是直接用ProgressBar,跟随3D版NPC头部的话,用支持“布告板 (Billboard)”能力的Sprite3D,想向其加入2D控件可指定其texture 为 SubViewport 容器,来混合提供2D/3D内容。 魂类体力槽 - 跑步、防御时受击、闪避均会消耗,安静时恢复。 动作设计(前摇/闪避等) - https://gwb.tencent.com/community/detail/125828 动画资源循环播放 - 通过“高级导入设置”导入的动画,默认均已自动设为了 Animation.LoopModeEnum.None; 若想改变默认循环模式,可点击行为树内“动画”节点,修改其属性;或者代码方式修改: animationNodeAnimation.UseCustomTimeline = true; ana.StretchTimeScale = false; // 即不循环 ana.LoopMode = Animation.LoopModeEnum.None; 循环播放行为(Animation.LoopModeEnum.Linear):站、走、跑; 不循环播放行为(默认行为):攻击、跳

节点/组件/网格

节点:
  节点:
    CanvasLayer 属性 Layer 数字越大,越能使子节点处于更外层。
    碰撞形状必须处于碰撞对象的直接子节点,若内部嵌套使用,首选性能更好的 Area3D,而非受物理影响的 CharacterBody3D 节点以及 PhysicsBody3D 实现类;直接子节点的碰撞形状,也可以通过 RemoteTransform3D 来传递转换。
    Area3D 事件 body_entered 的 body 是指被碰撞者是 PhysicsBody3D 才触发,area_entered 则对方是 Area3D 进入时才响应。

  2D控件一览图 - https://docs.godotengine.org/zh-cn/4.x/tutorials/ui/control_node_gallery.html
  子节点遍历:
    public void clearChildNodes(Node pNode)
    {
	var nodes = pNode.GetChildren();
	foreach (var node in nodes)
	{
		GD.Print(node.Name);
		node.QueueFree();
		if (node.GetChildCount() > 0) { clearChildNodes(node); }
	}
    }

  动态构建网格:
	var sm = new StandardMaterial3D(); sm.AlbedoColor = Colors.Red;
	var bm = new BoxMesh();	bm.Material = sm;
	var mi = new MeshInstance3D(); mi.Mesh = bm;
	AddChild(mi);

  上色 - 
    网格:
        _MeshInstance3D.Mesh = new PlaneMesh(); // or IDE 中添加
	var sm = new StandardMaterial3D();
	sm.AlbedoColor = new Color(0, 1, 0.5f); // or AlbedoTexture
	_MeshInstance3D.MaterialOverride = sm;

    控件用 x.AddThemeColorOverride("font_color", Godot.Colors.Red) 或:
	var ts = _Button.GetThemeStylebox("normal").Duplicate() as StyleBoxFlat;
	ts.BgColor = new Color(0, 1, 0.5f);
	_Button.AddThemeStyleboxOverride("normal", ts);

  支持包裹碰撞体(CollisionObject3D)的节点 - Area3D(区域重叠+进出信号), PhysicsBody3D(CharacterBody3D, PhysicalBone3D(骨骼专用/布娃娃Ragdoll), RigidBody3D, StaticBody3D);碰撞体必须是其直接子节点;BoneAttachment3D只能附着,提供不了碰撞。
  获取非直接子节点:_Node.FindChildren("@RemoteTransform3D@*", null, true, false);
  锁定左右移动:AxisLockLinearX = true; // AxisLockAngularY = true; // 或锁定旋转。
  两个位置之间距离:this.GlobalPosition.DistanceTo(_Node3D.GlobalPosition); // 单位米
  将父节点空间变动同步至受控节点:_RemoteTransform3D.RemotePath = controlledNode.GetPath();
  角色头顶血条/红字:直接使用带有 Billboard 属性的 Label3D(纯文字)、Sprite3D(图像+SubViewport)。
  全屏:_Control.SetAnchorsPreset(Control.LayoutPreset.FullRect);
  选项卡标题:GetNode("TabContainer").SetTabTitle(0, "x");
  3D 编辑器 MultiMeshInstance3D 的 MultiMesh -> 填充表面 按钮,无法选择目标网格的某个面,只能四面八方的填充,且会覆盖 multimesh 属性,故只能使用代码来动态添加了。

网格:
  网格(Mesh)由表面(Surface)组成,BoxMesh、PlaneMesh由1个表面组成,而CylinderMesh则为3个面,ArrayMesh由调用add_surface_from_arrays的次数决定,表面数组(Array[Dictionary])是由必须的顶点(ARRAY_VERTEX/PackedVector3Array等效C#的Vector3)和可选的法线(ARRAY_NORMAL/实时计算损耗大,故应专门指定/用于光照着色)、UV等子数组组成。
  正方形的每个面无论多大均默认切1次(subdivide_width属性决定切割次数/切割位置是三个边的中点,即1变4),由2个三角形拼接而成,本来要6个顶点组成,但通过6个索引关联后,只需4个顶点即可;注意 CSGPolygon3D 不存在 ArrayType.Index,且旋转90°则“上方”法线就不是 Vector3.Up 了。
  WorldBoundaryShape3D 属于无限平面,尺寸不会止步于碰撞体线框范围。
  Godot IDE 可视化编辑器支持 CSG,即Constructive Solid Geometry(构造实体几何),但如果需要复杂的建模能力,请使用Blender等。
  CSG用法:添加 CSGPolygon3D 至编辑器,并用鼠标调整锚点,支持导出 glTF;Inspector -> Transform -> Rotation Y 轴填 -90 则可沿平面增减调整。
    如果想调整 Blender 制作出来的网格,则需要用 CSGMesh3D 导入。
    CSGCombiner3D 套住后,会从上至下的应用操作。
  ArrayMesh 类创建网格示例:
    var rId = RenderingServer.InstanceCreate();
    RenderingServer.InstanceSetScenario(rId, GetWorld3D().Scenario);

    var v3s = new Vector3[] { new Vector3(-1, 1, 0), new Vector3(1, 1, 0), new Vector3(0, -1, 0) };
    var arrays = new Godot.Collections.Array();
    arrays.Resize((int)Mesh.ArrayType.Max);
    arrays[(int)Mesh.ArrayType.Vertex] = v3s;
    var mesh = new ArrayMesh();
    mesh.AddSurfaceFromArrays(Mesh.PrimitiveType.Triangles, arrays);
    //or mesh = GD.Load("res://MyMesh.obj");

    RenderingServer.InstanceSetBase(rId, mesh.GetRid());
    var v3 = new Vector3(0.5f, 0.1f, 0.5f); // or Vector3.Zero
    var t3d = new Transform3D(Basis.Identity, v3);
    RenderingServer.InstanceSetTransform(rId, t3d);

  代码创建网格工具类 - SurfaceTool

骨骼骨架

骨架重定向:选中 Skeleton3D 节点 -> 重定向(Retarget) -> 新建 BoneMap 并选中 -> Profile:SkeletonProfileHumanoid -> 鼠标选中蓝点会出现模型骨骼名,红点则属未自动映射。
      映射后模型的 Skeleton3D 节点默认变为 GeneralSkeleton,模型内的 AnimationPlayer 轨道名也会同步修改为 SkeletonProfileHumanoid 标准命名 %GeneralSkeleton:Hips。
      因为 %GeneralSkeleton 只能场景内访问,故将纯动画场景内的 AnimationPlayer 提升至模型场景子级:
        var ap = p.GetNode<AnimationPlayer>("attackScene/AnimationPlayer"); ap.Reparent(ap.GetParent().GetParent()); ap.Play("mixamo_com");
        或动态添加 _AnimationLibrary.AddAnimation(animName, al.GetAnimation("mixamo_com")); // 确保根节点处于默认,或设死 ap.RootNode = ap.GetParent().GetPath();

    [不优雅] 模型与动画骨骼名不一致时,也可导出纯文本的动画文件(*.tres),手动替换所有的 tracks/0/path = NodePath("Skeleton3D:mixamorig_Hips");对外发布时,最好转存为 *.anim。
	for (var i = 0; i < anim.GetTrackCount(); i++)
	{
		GD.Print(anim.TrackGetPath(i).GetConcatenatedNames()); // 取冒号前字符;完整值为 %GeneralSkeleton:Hips
		var newPath = new NodePath("../GeneralSkeleton:" + anim.TrackGetPath(i).GetConcatenatedSubNames());
		anim.TrackSetPath(i, newPath);
	}

  骨骼控制权/编辑筛选器:
        _AnimationNode.FilterEnabled = true; // 勾上后 in 端口就会让出部分控制权了;多根就多次执行 SetFilterPath(...)。
        _AnimationNode.SetFilterPath(new NodePath("%GeneralSkeleton:LeftUpperArm"), true); // false去除,无骨骼名则忽略。

  骨骼其他:
    作用 - 使物体具有肢体活动的能力。
    var sk=GetNode<Skeleton3D>("player/Skeleton/Skeleton3D");
    //var c = sk.GetBoneCount(); GD.Print(c);
    // 3D模型节点“在编辑器中打开”->“新建继承”(即存为场景)-> 选中Skeleton3D骨架节点右键“创建物理骨架”(即含碰撞体的PhysicalBone3D)
    // 射线碰到骨骼体后判断命中:raycast.collider.bone_name == "爆头骨骼名"
    sk.PhysicalBonesStartSimulation(); // PhysicalBone3D.bone_name指定3D模型资源内定义的骨骼部位名。
    骨骼映射/导入重定向(Retargeting) - https://docs.godotengine.org/zh-cn/4.x/tutorials/assets_pipeline/retargeting_3d_skeletons.html
      导入时选中Skeleton3D节点,点击“骨骼映射->BoneMap->Profile:SkeletonProfileHumanoid”,窗口Filtered Tracks所示为映射后的标准骨头部位名。
      骨骼名 - Upper Arm即上臂,Forearm或Lower Arm为前臂;Upper Arm即大腿,Lower Leg为小腿

视角

视图约定:
  // 转换即远近+方位
  this.GlobalTransform = new Transform3D(GlobalTransform.Origin, GlobalTransform.Basis);

  // 不升降,只修改平移和绕柱。
  this.Transform = new Transform3D(t3d.Basis, new Vector3(t3d.Origin.X, Transform.Origin.Y, t3d.Origin.Z));

  // 全局距离 - 相对于绝对原点的远近;即3D视图中心
  GD.Print(this.GlobalTransform.Origin);

  // 全局方向 - 相对于绝对正面的方位
  GD.Print(this.GlobalTransform.Basis);

  // 绝对原点 - IDE 3D默认视图 - 后、右、顶
  GD.Print(Vector3.Zero);

  // 绝对朝向 - 物体朝向观众或摄像头的那一面,即IDE的3D后视图
  GD.Print(new Basis(1, 0, 0, 0, 1, 0, 0, 0, 1));

  // 相对于父节点距离 - 0或实际米数;即物体中心
  GD.Print(this.Transform.Origin);

  // 相对于父节点方向 - 索引值只能0或1
  GD.Print(this.Basis); // 等同this.Transform.Basis

计算:
  四元数 (Quaternions / 旋转的代数表示)优于欧拉角 (Euler Angles / 即直接旋转)。
  首选四元数:new Quaternion(x: float, y: float, z: float, w: float); // 入参必须归一化
  旋转Player:p.Quaternion = new Quaternion(Vector3.Up, Mathf.DegToRad(180));
  取反方向 - 加负号即可 -myVector3 with { Y = p.GlobalPosition.Y } // with 可以简化字段修改;以Vector3.Zero方向为基准。

输入:
  Godot 游戏引擎 3D/WASD 移动模板实例
  WASD返回值只有0、±1、±0.70710677(斜向)三种情况,而手柄也已绑定 ui_* 的Action上,16位摇杆返回值为65535种(转换前范围是-32768至32767) - Idle 时则为 Vector3.Zero 或 new Vector3(0, 0, 0)。
  鼠标和手柄摇杆旋转视角时,均前推为仰,后推为趴,左推看左,右推看右。
    Vector2 iv = Input.GetVector("ui_left", "ui_right", "ui_up", "ui_down");
    // 向量非零 (0, 0) + 死区(0.1f)处理
    if (iv.LengthSquared() > DEAD_ZONE_THRESHOLD * DEAD_ZONE_THRESHOLD)
    {
        // Atan2 计算向量弧度入参顺序为 (y, x);返回值范围[-Pi, Pi]
        float rad = Mathf.Atan2(iv.Y, iv.X); // rad += Mathf.Pi / 2.0f; // 向右绕柱修正 90°
        //rad = Mathf.Wrap(rad, -Mathf.Pi, Mathf.Pi); // 规范化弧度范围
        float deg = Mathf.RadToDeg(rad); // 弧度转角度
        // 将角度范围 (-180° 到 180°) 改为非负数
        if (deg < 0) { deg += 360f; } GD.Print("0°-360°: " + deg);
    }

  游戏手柄摇杆 (Analog Stick)支持数万种方位,而键盘则只能控制8种方位;线性霍尔传感器手柄已取代传统碳膜电位器式摇杆,TMR更先进。
  8-Way Directional Movement:
    W,前,0∘
    S,后,180∘
    A,左,270∘
    D,右,90∘
    W + D,右前,45∘
    W + A,左前,315∘
    S + D,右后,135∘
    S + A,左后,225∘

视角:
  射击游戏通常采用FPS;魂类游戏战斗时则是手动锁定某个敌人的方式;也存在自动锁定或吸附较近敌人的方式(《原神》、《枕刀歌:白刃行》)。
  《刺客信条》等 - 在移动时身体前方跟随鼠标,但静止时鼠标进行自由观察,而身体不跟;《法环》与之不同点是,鼠标静止1秒后会回正至身体正前方的朝向。
  挥拳时能否转向,取决于游戏类型,魂系不能,而较宽容刷怪的ARGP则可以。

  Basis 类型能够同时存储旋转和缩放,而四元数 Quaternion 只能存储旋转,后者的作用是可以更直观的绕球体轴旋转,解决了Vector3.rotated(...)对次序敏感的问题 - https://quaternions.online/
  绕轴旋转 - 首选四元数 Quaternion,次选方案是在轴上放个Node3D做旋转原点,然后把 SpringArm3D/Camera3D 套进去转;仅用一次的可调 Transform.Rotated(player.GlobalPosition, Mathf.Pi / 2.0f)。
    看向某位置:LookAt(centerPoint, Vector3.Up);
    其他备选方案(可能有性能问题):
	var centerPoint = player.GlobalPosition;
	// 1. 平移到原点 (相对于被绕节点的位置)
	var gt = GlobalTransform.Translated(-centerPoint);
	// 2. 鼠标绕柱旋转角度
	gt = gt.Rotated(Vector3.Down, Mathf.Pi / 2.0f);
	// 3. 平移回原位
	gt = gt.Translated(centerPoint);
	// 4. 应用变换
	GlobalTransform = gt;

  第一人称视角(FPV)最大特点是摄像头处于眼睛位置,站立或走动时身体会跟随摄像头;
  第三人称视角(TPV)则摄像头和眼睛各行其事,站立或走动时身体会跟随摄像头,审看皮肤时不跟随;摄像头下移摇臂抬高,交战视野会更好;NPC对话时,才会侧身一下不致遮挡,跟同身高的敌人对打时,摄像头稍稍拉高;
  俯视角(Top-Down View)则摄像头固定,行走方向也固定;其他 - 站立或走动时按照身体朝向移动。

  移动 - 非战斗状态为“自由移动”,左右移动时攻击方向为斜向前方45°,W+A同时按则22.5°;第一人称视角的攻击则一直朝向鼠标方向;
      遇到敌人时,可手动切换至“锁定目标移动”,被锁定者会标记个白色亮点,此时你只能面对锁定敌人后退,左右移动时攻击目标依然朝向前方(与行进方向约90°),软锁(原神)则优先考虑玩家目标,锁定者次之。

  WASD移动参考视频(法环) - https://www.bilibili.com/video/BV1XU4y1Z7D9/

  参考:
    Unity自制锁定敌人镜头与自由镜头丝滑切换 - https://www.bilibili.com/opus/828582114360295429

游戏制作实例

类魂:
  走动时正面跟随鼠标指向,站立时不跟随,摄像头自由转向,退后时能看到正脸;锁定敌人后,只能左中右绕敌方半圆形走动,无法越至背后;翻滚时有0.4秒无敌帧。
  锁敌 - 脱锁半径设为20米,绕圈半径定为玩家和敌人直线距离。

其他

按键代码绑定:
  var iek = new InputEventKey();
  iek.Keycode = Key.Shift;
  InputMap.AddAction("ui_shift"); // ui_up、ui_down、ui_left、ui_right 已内置。
  InputMap.ActionAddEvent("ui_shift", iek);

Area3D 的工作流程(推荐)
  武器结构: 将 Area3D 作为角色的武器骨骼(如手或剑柄)的子节点。

  形状配置: 为 Area3D 添加一个或多个 CollisionShape3D 来覆盖剑刃的范围。

  动画同步:

    在角色的挥砍动画中,创建一个 动画轨道 (Animation Track)。

    在动画开始挥动(伤害应开始)的帧,启用这个 Area3D。

    在动画完成挥动(伤害应结束)的帧,禁用这个 Area3D。

  伤害处理: 连接 Area3D 的 body_entered 信号。当信号触发时,判断进入的是否为敌人,如果是,则调用敌人的伤害函数。


骨骼蒙皮

_PhysicsBody3D.GetGravity() 已取代 ProjectSettings.GetSetting("physics/3d/default_gravity").AsSingle();

动画树 Active = false 后骨骼乱掉解决:
  p.GetNode("AP").Play("idle"); // 骨骼归位后再启用动画树。
  at.Active = true;