,握基本语法、逻辑、面向对象、数组、字典、委托、接口等知识即可。
API中文文档:https://www.bookstack.cn/read/godot-4.2-zh/
脚本编辑软件:https://www.bookstack.cn/read/godot-4.2-zh/678acee2979bfdbf.md
常见的生命周期方法
using Godot;
using System;
public partial class Sprite2d : Sprite2D
{
//节点添加到节点树中的时候调用
public override void _EnterTree()
{
GD.Print("enterTree");
}
//节点加载完成,这里初始化
public override void _Ready()
{
GD.Print("Ready");
}
//帧
public override void _Process(double delta) { }
//游戏逻辑
//每次物理系统计算,会调用一次1 个引用
public override void _PhysicsProcess(double delta)
{
}
//节点离开节点树,销毁
public override void _ExitTree()
{
GD.Print("ExitTree");
}
}
鼠标和按键
using Godot;
using System;
public partial class InputTest : Node
{
public override void _Ready()
{
// 鼠标设置:显示、隐藏鼠标、限制在游戏窗口
// 限制并且隐藏
Input.MouseMode = Input.MouseModeEnum.ConfinedHidden;
// Input.MouseMode = Input.MouseModeEnum.Confined;
}
public override void _Process(double delta)
{
// 按键事件
//是否按下键盘的B键
if (Input.IsKeyPressed(Key.B))
{
GD.Print("按下了B键");
}
}
}
鼠标和键盘事件处理
using Godot;
using System;
public partial class InputTest : Node
{
public override void _Ready()
{
// 鼠标设置:显示、隐藏鼠标、限制在游戏窗口
// 限制并且隐藏
// Input.MouseMode = Input.MouseModeEnum.ConfinedHidden;
// Input.MouseMode = Input.MouseModeEnum.Confined;
}
public override void _Process(double delta)
{
// 按键事件
//是否按下键盘的B键
if (Input.IsKeyPressed(Key.B))
{
GD.Print("按下了B键");
}
}
public override void _Input(InputEvent @event)
{
base._Input(@event);
// 如果是键盘事件,转成键盘事件去操作
if (@event is InputEventKey)
{
//转成键盘事件
var key = @event as InputEventKey;
// 判断当前是否按下的是V键
if (key.Keycode == Key.V)
{
GD.Print("按下了V键");
// 判断当前是否是持续按压
if (key.IsEcho())
{
GD.Print("持续按压V键");
}
// 判断当前是否是刚按下
else if (key.IsPressed())
{
GD.Print("刚按下V键");
}
// 判断当前是否是抬起瞬间
else if (key.IsReleased())
{
GD.Print("抬起V键");
}
}
}
// 如果是鼠标事件,转成鼠标事件去操作
if (@event is InputEventMouse)
{
//转成鼠标事件
var mouse = @event as InputEventMouse;
// 判断当前是否按下
if (mouse.IsPressed())
{
GD.Print("按下鼠标:", mouse.Position, mouse.ButtonMask);
}
}
}
}
按键映射
public override void _Process(double delta)
{
// 按键事件
//是否按下键盘的B键
if (Input.IsKeyPressed(Key.B))
{
GD.Print("按下了B键");
}
// 按下虚拟按键
if (Input.IsActionJustPressed("跳跃"))
{
GD.Print("按下跳跃");
}
if (Input.IsActionPressed("跳跃"))
{
GD.Print("跳跃中");
}
if (Input.IsActionJustReleased("跳跃"))
{
GD.Print("结束跳跃");
}
// 获取按键力度
float s = Input.GetActionStrength("跳跃");
GD.Print("跳跃力度:", s);
}
获取虚拟的左和右。
public override void _Process(double delta)
{
// 按键事件
//是否按下键盘的B键
if (Input.IsKeyPressed(Key.B))
{
GD.Print("按下了B键");
}
// 按下虚拟按键
if (Input.IsActionJustPressed("跳跃"))
{
GD.Print("按下跳跃");
}
if (Input.IsActionPressed("跳跃"))
{
GD.Print("跳跃中");
}
if (Input.IsActionJustReleased("跳跃"))
{
GD.Print("结束跳跃");
}
// 获取按键力度
float s = Input.GetActionStrength("跳跃");
GD.Print("跳跃力度:", s);
// 获取一个x轴,水平轴
float horizontal = Input.GetAxis("左", "右");
GD.Print("方向:", horizontal);
}
上下左右
Vector2 dir = Input.GetVector("左", "右", "上", "下");
GD.Print("方向:", dir);
代码操作节点
using Godot;
using System;
public partial class NodeOp : Node
{
public override void _Ready()
{
}
public override void _Process(double delta)
{
if (Input.IsActionJustPressed("左"))
{
//获取当前场景的根节点
Node root = this.GetTree().CurrentScene;
// 去寻找Sprite2D节点
Node test = root.FindChild("Sprite2D");
// 删除Sprite2D节点
test.QueueFree();
}
}
}
移动节点
using Godot;
using System;
public partial class NodeOp : Node
{
public override void _Ready()
{
}
public override void _Process(double delta)
{
if (Input.IsActionJustPressed("左"))
{
//获取当前场景的根节点
Node root = this.GetTree().CurrentScene;
// 去寻找InputTest节点
Node test = root.FindChild("Sprite2D");
// 删除InputTest节点
// test.QueueFree();
// 从节点树拿出来
root.RemoveChild(test);
// 将节点添加到当前节点下
this.AddChild(test);
}
}
}
添加节点
if (Input.IsActionJustPressed("右"))
{
//获取当前场景的根节点
Node root = this.GetTree().CurrentScene;
Node2D node2D = new Node2D();
node2D.Name = "New";
this.AddChild(node2D);
}
找父节点
if (Input.IsActionJustPressed("右"))
{
Node2D node2D = new Node2D();
node2D.Name = "New";
Node3D node = this.GetParent<Node3D>();
node.AddChild(node2D);
}
找任意节点
if (Input.IsActionJustPressed("右"))
{
Node2D node2D = new Node2D();
node2D.Name = "New";
// 绝对路径
Node2D node = GetNode<Node2D>("/root/Home/p2/test");
node.AddChild(node2D);
}
// 相对路径
Node2D node = GetNode<Node2D>("../test1");
node.AddChild(node2D);
切换场景
if (Input.IsActionJustPressed("右"))
{
SceneTree st = this.GetTree();
st.ChangeSceneToFile("res://home/home.tscn");
}
using Godot;
using System;
public partial class NodeOp : Node
{
// 新场景
[Export] public PackedScene NewScene;
public override void _Ready()
{
}
public override void _Process(double delta)
{
if (Input.IsActionJustPressed("右"))
{
// 跳转场景方法1
// SceneTree st = this.GetTree();
// st.ChangeSceneToFile("res://game1/game1.tscn");
// 跳转场景方法2
SceneTree st = this.GetTree();
st.ChangeSceneToPacked(NewScene);
}
}
}
拖动场景放入,效果一致。
只需要另一个场景中的子节点,加入当前节点
比如子弹场景,加入当前场景
if (Input.IsActionJustPressed("左"))
{
// 实例化新场景,并返回一个根节点
Node node = NewScene.Instantiate();
//添加到根节点
this.GetTree().CurrentScene.AddChild(node);
}
自动加载脚本
创建一个主目录下的node节点下的脚本。
假设脚本是对游戏做一些管理设定,只要游戏运行就一直存活。
因此,一般情况下,需要将该脚本设置成单例。
运行后会发现脚本已经运行在场景中,且切换场景一直存在。
常用属性代码设置
比如一个精灵
//CanvasItem常用属性//是否显示
this.Visible = true;
//渲染顺序
this.ZIndex = 10;
// 是否相对本身所在顺序加权
this.ZAsRelative = false;
//Node2D常用属性
////位置
this.Position = new Vector2(500, 300);
//旋转
// this.Rotation =0.1f; // 幅度
//this.RotationDegrees = 30; // 角度 30度
// 缩放
this.Scale = new Vector2(2, 2);
//倾斜
this.Skew = 30;
//帧
public override void _Process(double delta)
{
// 获取鼠标位置
var pos = GetGlobalMousePosition();
// 看向某个点
LookAt(pos);
}
动态加载精灵图片
this.Texture=GD.Load<Texture2D>("res://icon.svg");
或者
[Export]
public Texture2D newTexture;
//节点加载完成,这里初始化
public override void _Ready()
{
this.Texture=newTexture;
}
出现的图标只有四分之一,可以设置中心点偏移模式:
public override void _Ready()
{
this.Texture=newTexture;
// 中心点还是左上角
this.Centered = false;
}
物品分组是个好习惯
//帧
public override void _Process(double delta)
{
if (Input.IsActionJustPressed("右"))
{
// 获得分组中的节点
var enemys = this.GetTree().GetNodesInGroup("小兵");
// 遍历敌人 销毁
foreach (Node enemy in enemys)
{
// 销毁
enemy.QueueFree();
}
}
}
给每个敌人挂一个脚本
using Godot;
using System;
public partial class Dogface : Node
{
public void test()
{
GD.Print("我是小兵哈哈哈哈");
}
}
找个随便其他存活脚本执行如下代码:
//帧
public override void _Process(double delta)
{
if (Input.IsActionJustPressed("左"))
{
// 给分组节点发消息
this.GetTree().CallGroup("小兵", "test");
}
}
脚本动态加入分组
if (Input.IsActionJustPressed("上"))
{
// 将当前节点加到敌人分组
this.AddToGroup("小兵");
}
信号的接发
所谓信号,其实就是事件监听和响应。
左侧按钮点击,会导致右侧图标消失。
不同的控件有不同的信号方法:
创建脚本MyButton.cs
并绑定button按钮:
using Godot;
using System;
public partial class MyButton : Node
{
public override void _Ready()
{
}
public override void _Process(double delta)
{
}
public void buttonClick()
{
// 找到节点并删除
Node2D node = GetNode<Node2D>("/root/Home/Node2D/Sprite2D");
node.QueueFree();
}
}
脚本动态连接信号
using Godot;
using System;
public partial class MyButton : Node
{
public override void _Ready()
{
// 动态连接信号
this.Connect("pressed", new Callable(this,"buttonClick"));
}
public override void _Process(double delta)
{
}
public void buttonClick()
{
// 找到节点并删除
Node2D node = GetNode<Node2D>("/root/Home/Node2D/Sprite2D");
node.QueueFree();
}
}
自定义信号(事件)
如果有vue知识,就会发现等同于emit和@事件。
A.cs
using Godot;
using System;
public partial class A : Sprite2D
{
// 信号:声明一个委托
[Signal]
public delegate void MySignalEventHandler();
public override void _Ready()
{
}
public override void _Process(double delta)
{
if (Input.IsActionJustPressed("左"))
{
// 发射信号
EmitSignal("MySignal");
}
}
}
B.cs
using Godot;
using System;
public partial class B : Sprite2D
{
public override void _Ready()
{
}
public override void _Process(double delta)
{
}
public void buttonClick()
{
Godot.GD.Print("接收到信号了!");
}
}
动画精灵
脚本控制动画播放
using Godot;
using System;
public partial class MyAnimatedSprite2d : AnimatedSprite2D
{
public override void _Ready()
{
}
public override void _Process(double delta)
{
if(Input.IsActionPressed("左"))
{
this.Play("idle");
}
if(Input.IsActionPressed("右"))
{
this.Play("attack");
}
}
}
动画播放
创建一个角色的动画
选择纹理。
右击,插入关键帧
点一下关键帧
因为有2个图像,所以只需要2个图像关键帧:
右侧勾选,循环:
一秒长度太长,改成0.4秒
调整参数,一号关键帧在0.0秒,2号在0.2秒:
动画播放就做完了。
增加代码脚本:
using Godot;
using System;
public partial class MyAnimationPlayer : AnimationPlayer
{
public override void _Ready()
{
}
public override void _Process(double delta)
{
if(Input.IsActionPressed("左"))
{
this.Play("idle");
}
if(Input.IsActionPressed("右"))
{
this.Play("run");
}
}
}
效果一致。
其他常用函数
Pause 暂停播放
Stop 播放停止
IsPlaying 是否在播放
动画播放和上面的精灵动画相比,好处是 动画播放还能做其他更多的事情。
比方run的同,能改变位置和缩放。
光照
2D主要有2种灯光,平行光DirectionalLight2D和点光源PointLight2D 。
2D种的平行光,可以理解为全局性的灯光。
点光源是需要纹理的:
灯光遮罩,只有匹配上的组件,才会有灯光。
投影(阴影)
希望灯光照射到精灵上有阴影区域
首先系统不清楚阴影范围要多大。
设置障碍物范围。
设置灯光支持阴影,启用:
要有背景才能看出阴影,增加一个背景精灵即可:
可以设置阴影的颜色:
声音
代码控制:
using Godot;
using System;
public partial class MyAudioStreamPlayer : AudioStreamPlayer
{
public override void _Ready()
{
}
public override void _Process(double delta)
{
if (Input.IsActionPressed("左"))
{
if (this.Playing==false)
{
this.Play();
}
}
if (Input.IsActionPressed("右"))
{
this.StreamPaused = true;
}
}
}
代码实现音频播放
using Godot;
using System;
public partial class MyNode2d : Node2D
{
AudioStreamPlayer audioStreamPlayer;
[Export] private AudioStream bgm;
public override void _Ready()
{
// 实例化节点
audioStreamPlayer = new AudioStreamPlayer();
// 加到当前子节点
this.AddChild(audioStreamPlayer);
// 加载音频
audioStreamPlayer.Stream = bgm;
audioStreamPlayer.Play();
}
public override void _Process(double delta)
{
}
}
碰撞
如下,敌人和玩家,需要检测碰撞,但精灵类是只负责展示,没办法检测碰撞的。
增加碰撞区域类,同时会报警告,因为没有设置碰撞区域,设置区域需要增加一个子类,2D有多边形和规则图形。
碰撞触发和碰撞离开:
射线碰撞
一个射线一个精灵。
默认勾选,排除父节点碰撞。
增加精灵碰撞体:
射线支持和2种产生碰撞,一种是物理碰撞,一种是区域碰撞。
增加脚本
···
using Godot;
using System;
public partial class MyRayCast2d : RayCast2D
{
public override void _Ready()
{
}
public override void _Process(double delta)
{
// 无论什么状态,立即更新光线的碰撞信息,而不等待下一个_physics_process调用。
// this.ForceRaycastUpdate();
}
/**
* 物理更新函数
*/
public override void _PhysicsProcess(double delta)
{
// 每次按下左键检测一次 是否 碰撞
if(Input.IsActionPressed("左"))
{
if (this.IsColliding())
{
// 拿到碰撞的区域
Area2D area2D = this.GetCollider() as Area2D;
GD.Print("检测到了:" + area2D.Name);
}
else
{
GD.Print("没检测到");
}
}
}
}
···
类似效果还有组件ShapeCast2D,区别是射线区域,有一个宽度范围。
物理碰撞
上面做了区域碰撞,godot还有静态物理区域组件:
假设做一个2d的横版游戏的地面:
这个组件也需要增加碰撞区域:
增加一个玩家,游戏会动的碰撞区域——刚体:
运行,玩家会落下来,倾斜墙体,人物会有物理效果。
一些属性:
控制玩家
RigidBody2D增加脚本MyPlayer.cs
using Godot;
using System;
public partial class MyPlayer : RigidBody2D
{
public override void _Ready()
{
}
public override void _Process(double delta)
{
}
/**
* 物理·相关的写到这个里面
*/
public override void _PhysicsProcess(double delta)
{
// 通过获取水平轴控制左右移动
float horizontal = Input.GetAxis("左", "右");
// 速度
this.LinearVelocity = new Vector2(horizontal * 100, this.LinearVelocity.Y);
}
}
把人物的碰撞区域换成胶囊形状
会发现是滚着走的
锁定旋转
using Godot;
using System;
public partial class MyPlayer : RigidBody2D
{
public override void _Ready()
{
this.LockRotation = true;
}
public override void _Process(double delta)
{
}
/**
* 物理·相关的写到这个里面
*/
public override void _PhysicsProcess(double delta)
{
// 通过获取水平轴控制左右移动
float horizontal = Input.GetAxis("左", "右");
// 速度
this.LinearVelocity = new Vector2(horizontal * 300, this.LinearVelocity.Y);
}
}
增加跳跃:
...
/**
* 物理·相关的写到这个里面
*/
public override void _PhysicsProcess(double delta)
{
// 通过获取水平轴控制左右移动
float horizontal = Input.GetAxis("左", "右");
// 速度
this.LinearVelocity = new Vector2(horizontal * 300, this.LinearVelocity.Y);
// 如果按了空格,就跳跃
if (Input.IsActionPressed("跳跃"))
{
this.LinearVelocity = new Vector2(this.LinearVelocity.X,-600);
}
}
这里发现玩家可以无限跳跃,限制只能跳跃最多2次:
using Godot;
using System;
public partial class MyPlayer : RigidBody2D
{
public override void _Ready()
{
this.LockRotation = true;
}
public override void _Process(double delta)
{
}
/**
* 物理·相关的写到这个里面
*/
public override void _PhysicsProcess(double delta)
{
// 通过获取水平轴控制左右移动
float horizontal = Input.GetAxis("左", "右");
// 速度
this.LinearVelocity = new Vector2(horizontal * 300, this.LinearVelocity.Y);
// 如果按了空格,就跳跃
if (Input.IsActionPressed("跳跃"))
{
this.LinearVelocity = new Vector2(this.LinearVelocity.X,-600);
}
}
public void bodyEntered(Node body)
{
GD.Print("碰撞");
}
}
发现无法触发:
增加代码:
using Godot;
using System;
public partial class MyPlayer : RigidBody2D
{
public override void _Ready()
{
// 锁定旋转
this.LockRotation = true;
// 如果要做碰撞检测,需要设置:
this.ContactMonitor = true;
this.MaxContactsReported = 1;
}
public override void _Process(double delta)
{
}
/**
* 物理·相关的写到这个里面
*/
public override void _PhysicsProcess(double delta)
{
// 通过获取水平轴控制左右移动
float horizontal = Input.GetAxis("左", "右");
// 速度
this.LinearVelocity = new Vector2(horizontal * 300, this.LinearVelocity.Y);
// 如果按了空格,就跳跃
if (Input.IsActionPressed("跳跃"))
{
this.LinearVelocity = new Vector2(this.LinearVelocity.X,-600);
}
}
public void bodyEntered(Node body)
{
GD.Print("碰撞");
}
}
如果不通过代码,也可以通过勾选配置:
using Godot;
using System;
public partial class MyPlayer : RigidBody2D
{
private bool isGround;
private int maxDump = 0;
public override void _Ready()
{
// 锁定旋转
this.LockRotation = true;
// 如果要做碰撞检测,需要设置:
this.ContactMonitor = true;
this.MaxContactsReported = 1;
}
public override void _Process(double delta)
{
}
/**
* 物理·相关的写到这个里面
*/
public override void _PhysicsProcess(double delta)
{
// 通过获取水平轴控制左右移动
float horizontal = Input.GetAxis("左", "右");
// 速度
this.LinearVelocity = new Vector2(horizontal * 300, this.LinearVelocity.Y);
// 如果按了空格,就跳跃(限制只跳跃一次)
// if (Input.IsActionPressed("跳跃")&&isGround)
// {
// this.LinearVelocity = new Vector2(this.LinearVelocity.X, -600);
// }
// 如果按了空格,就跳跃(限制最多跳跃2次)
if (Input.IsActionJustPressed("跳跃")&&maxDump< 2)
{
maxDump++;
GD.Print(maxDump);
this.LinearVelocity = new Vector2(this.LinearVelocity.X, -600);
}
}
public void bodyEntered(Node body)
{
// isGround = true;
maxDump = 0;
}
public void bodyExited(Node body)
{
// isGround = false;
}
}
角色控制
godot提供一种专门用于角色控制的组件CharacterBody2D。
该控件本身既没有重力,也不会滚动,一切都需要用代码来实现。
创建脚本的时候,可以用基础2d移动模板,实现角色控制。
自动生成如下代码:
using Godot;
using System;
public partial class CharacterBody2d : CharacterBody2D
{
public const float Speed = 300.0f;
public const float JumpVelocity = -400.0f;
public override void _PhysicsProcess(double delta)
{
Vector2 velocity = Velocity;
// Add the gravity.
if (!IsOnFloor())
{
velocity += GetGravity() * (float)delta;
}
// Handle Jump.
if (Input.IsActionJustPressed("ui_accept") && IsOnFloor())
{
velocity.Y = JumpVelocity;
}
// Get the input direction and handle the movement/deceleration.
// As good practice, you should replace UI actions with custom gameplay actions.
Vector2 direction = Input.GetVector("ui_left", "ui_right", "ui_up", "ui_down");
if (direction != Vector2.Zero)
{
velocity.X = direction.X * Speed;
}
else
{
velocity.X = Mathf.MoveToward(Velocity.X, 0, Speed);
}
Velocity = velocity;
MoveAndSlide();
}
}
生成的代码省略很多this,为了可 阅读性,可以加上:
using Godot;
using System;
public partial class CharacterBody2d : CharacterBody2D
{
public const float Speed = 300.0f;
public const float JumpVelocity = -400.0f;
public override void _PhysicsProcess(double delta)
{
Vector2 velocity = this.Velocity;
// Add the gravity.
if (!this.IsOnFloor())
{
velocity += this.GetGravity() * (float)delta;
}
// Handle Jump.
if (Input.IsActionJustPressed("ui_accept") && this.IsOnFloor())
{
velocity.Y = JumpVelocity;
}
// Get the input direction and handle the movement/deceleration.
// As good practice, you should replace UI actions with custom gameplay actions.
Vector2 direction = Input.GetVector("ui_left", "ui_right", "ui_up", "ui_down");
if (direction != Vector2.Zero)
{
velocity.X = direction.X * Speed;
}
else
{
velocity.X = Mathf.MoveToward(Velocity.X, 0, Speed);
}
this.Velocity = velocity;
MoveAndSlide();
}
}
想要一切修改生效需要最后执行MoveAndSlide();
方法。
角色控制具体用刚体还是角色刚体,取决于开发者实际需求,两者通过调整,效果都可以达到一致。
UI界面
本质上就是用到Control下的组件实现UI界面效果。
比方Home页面增加UI界面,一般做法并不是在Home场景下,增加各种Contorl下组件,而是创建一个专门的UI场景。
而dodot还真有创建新的“UI场景”。
其实,用户界面场景,就是根节点为Control的场景。