项目素材及源码
https://gitee.com/bobokaka/barbarian-blaster
游戏玩法设计
地板和环境三要素
照相机参数:
保存为BaseLevel.tscn场景文件。
制作网格库
创建res://MeshLibraries/SpaceLibrary.tscn 3D界面场景。
创建子节点MeshInstance3D。
设置材质,颜色为#cdcdcd
。创建碰撞体。
鼠标选中FreeSpace节点。
右上角点击 场景。
如上,保存。
同上再创建一个炮台网格,颜色改为#d63a29。
两个物体重叠没关系。
同样的步骤,导出为网格图,然后覆盖保存到同一个文件。
打开SpaceLibrary.tres。我们能看到数组里面有2个网格材料了。
打开BaseLevel.tscn场景。
增加网格组件GridMap。然后再Mesh Library中,加载SpaceLibrary.tres,可以看到2个网格材料再右侧新增了一个列表展示,而且可以选中并拖动到主体界面中去。
设计线路
如果发现网格方向不对,可以试试这里,比如光标清除旋转
。
默认情况下,网格和格子对不上。解决方案就是调整右侧Size属性(4,4,4),因为我们网格块是4米x4米的大小。
倾斜发现,网格是悬空的。因为中心属性Y轴是打开的。
关掉Y轴为中心。
shift+鼠标右键,可以选取一块区域,再选中网格材料后,按ctrl+f会一次填充。或者按Del键删除。(这里层错了 应该是0层)
如下,红色的是敌人行动路线。
其他地方用白色填满,然后删除红色。
调整格子到(0,-4,0)。(这里层错了 应该是0层)
为敌人创建行走的路线
路径3D组件 Path3D。
创建CSG多边形节点CSGPolygon3D。
加载图形和配置参数。
最终效果如下
参数分别为:
PathFollow3D组件,Path3D的点采样器,必须为PAth3D的子节点,能够控制从开始走到结束的行动节奏。我们这个游戏得关闭循环。
敌人
创建圆柱体代表敌人。
敌人单独生成新的场景。保存到res://Enemy/enemy.tscn。
根节点增加脚本Enemy.cs
using Godot;
using System;
public partial class Enemy : PathFollow3D
{
[Export] public double speed = 2.5;
private Node3D playerBase;
private PlayerBase playerBaseScript;
public override void _Ready()
{
playerBase = (Node3D)GetTree().GetFirstNodeInGroup("PlayerBase");
}
public override void _Process(double delta)
{
this.Progress += (float)(delta * speed);
if (playerBase != null)
{
playerBaseScript = playerBase as PlayerBase;
}
if (ProgressRatio >= 1.0)
{
// 调用ScriptA中的方法
playerBaseScript?.takeDamage();
}
}
}
增加玩家基地PlayerBase。增加一个Node3D节点下,挂在CSGCylinder3D子节点,然后独立成场景PlayerBase.tscn。
PlayerBase节点位置(-10,0,-12)。
进入PlayerBase场景
圆柱体颜色
#e20005
根节点增加PlayerBase.cs脚本。
using Godot;
using System;
public partial class PlayerBase : Node3D
{
public void takeDamage()
{
GD.Print("takeDamage");
}
}
运行,发现敌人只要碰撞到基地(路程进度100%)才会触发碰撞。
基地掉血
增加文本。
修改脚本PlayerBase.cs
using Godot;
using System;
public partial class PlayerBase : Node3D
{
[Export] public int MaxHealth=5;
private Label3D _label3D;
public override void _Ready()
{
_label3D = GetNode<Label3D>("Label3D");
_label3D.Text =MaxHealth.ToString();
}
public void takeDamage()
{
GD.Print("takeDamage");
MaxHealth--;
_label3D.Text =MaxHealth.ToString();
}
}
修改脚本Enemy.cs
using Godot;
using System;
public partial class Enemy : PathFollow3D
{
[Export] public double speed = 2.5;
private Node3D playerBase;
private PlayerBase playerBaseScript;
public override void _Ready()
{
playerBase = (Node3D)GetTree().GetFirstNodeInGroup("PlayerBase");
}
public override void _Process(double delta)
{
this.Progress += (float)(delta * speed);
if (playerBase != null)
{
playerBaseScript = playerBase as PlayerBase;
}
if (ProgressRatio >= 1.0)
{
// 调用Script中的方法
playerBaseScript?.takeDamage();
// 停止运行_Process函数
SetProcess(false);
// Free();
}
}
}
一波敌人
修改脚本PlayerBase.cs
。
using Godot;
using System;
public partial class PlayerBase : Node3D
{
[Export] public int MaxHealth = 5;
private int _currentHealth;
private Label3D _label3D;
public int CurrentHealth
{
get => _currentHealth;
set
{
_currentHealth = value;
GD.Print("生命值改变!");
_label3D.Text = value.ToString();
// 重新加载游戏
if (value < 1)
{
GetTree().ReloadCurrentScene();
}
}
}
public override void _Ready()
{
_label3D=GetNode<Label3D>("Label3D");
CurrentHealth= MaxHealth;
}
public void takeDamage()
{
GD.Print("takeDamage");
CurrentHealth -= 1;
}
}
然后复制5个敌人,修改初始位置。
游戏掉血开发完成。
优化一下显示文字。
......
private Color colorRed = new Color(Colors.Red);
private Color colorWhite = new Color(Colors.White);
public int CurrentHealth
{
get => _currentHealth;
set
{
_currentHealth = value;
GD.Print("生命值改变!");
_label3D.Text = value + "/" + MaxHealth;
Color color = colorRed.Lerp(colorWhite, (float)value / MaxHealth);
_label3D.Modulate = color;
// 重新加载游戏
if (value < 1)
{
GetTree().ReloadCurrentScene();
}
}
}
......
鼠标输入
将相机独立成一个场景ray_picker_camera.tscn。
根节点增加脚本RayPickerCamera.cs。
using Godot;
using System;
public partial class RayPickerCamera : Camera3D
{
private RayCast3D _rayCast3D;
public override void _Ready()
{
_rayCast3D = GetNode<RayCast3D>("RayCast3D");
}
public override void _Process(double delta)
{
// 获取射线和鼠标交叉的坐标点
Vector2 mousePosition = GetViewport().GetMousePosition();
GD.Print(mousePosition.ToString());
}
}
运行后,鼠标移动,可以看到坐标变化打印。
修改脚本。
public override void _Process(double delta)
{
// 获取射线和鼠标交叉的坐标点
Vector2 mousePosition = GetViewport().GetMousePosition();
// GD.Print(mousePosition.ToString());
// ProjectLocalRayNormal 从屏幕点位置返回沿相机方向的法向量。
// 再赋值给射线的目标位置
_rayCast3D.TargetPosition = ProjectLocalRayNormal(mousePosition);
}
打开调试的 显示碰撞区域。
运行:
修改脚本
public override void _Process(double delta)
{
// 获取射线和鼠标交叉的坐标点
Vector2 mousePosition = GetViewport().GetMousePosition();
// GD.Print(mousePosition.ToString());
// ProjectLocalRayNormal 从屏幕点位置返回沿相机方向的法向量。
// 再赋值给射线的目标位置
// 因为指向射线只有单位向量(1米),这里乘以100,使向量穿过任何地面
_rayCast3D.TargetPosition = ProjectLocalRayNormal(mousePosition) * 100.0f;
// ForceRaycastUpdate:立即更新光线的碰撞信息,而不等待下一个_physics_process调用。
// 例如,当射线或它的父射线改变了状态时,使用此方法。注意:Enabled并不需要为true才能正常工作。
_rayCast3D.ForceRaycastUpdate();
// GetCollider:打印得到的第一个对撞对象。如果没有对象与射线相交,则返回null(即iscollision()返回false)。
// GetCollisionPoint:返回在全局坐标系中光线与最近物体相交的碰撞点。
GD.PrintT(_rayCast3D.GetCollider(),_rayCast3D.GetCollisionPoint());
}
放置网格
修改RayPickerCamera.cs脚本
using Godot;
using System;
public partial class RayPickerCamera : Camera3D
{
private RayCast3D _rayCast3D;
[Export] public GridMap gridMap;
public override void _Ready()
{
_rayCast3D = GetNode<RayCast3D>("RayCast3D");
}
public override void _Process(double delta)
{
// 获取射线和鼠标交叉的坐标点
Vector2 mousePosition = GetViewport().GetMousePosition();
// GD.Print(mousePosition.ToString());
// ProjectLocalRayNormal 从屏幕点位置返回沿相机方向的法向量。
// 再赋值给射线的目标位置
// 因为指向射线只有单位向量(1米),这里乘以100,使向量穿过任何地面
_rayCast3D.TargetPosition = ProjectLocalRayNormal(mousePosition) * 100.0f;
// ForceRaycastUpdate:立即更新光线的碰撞信息,而不等待下一个_physics_process调用。
// 例如,当射线或它的父射线改变了状态时,使用此方法。
_rayCast3D.ForceRaycastUpdate();
// GetCollider:打印得到的第一个对撞对象。如果没有对象与射线相交,则返回null(即iscollision()返回false)。
// GetCollisionPoint:返回在全局坐标系中光线与最近物体相交的碰撞点。
// GD.PrintT(_rayCast3D.GetCollider(),_rayCast3D.GetCollisionPoint());
// 如果撞上了什么东西
if (_rayCast3D.IsColliding())
{
GodotObject collider = _rayCast3D.GetCollider();
if (collider is GridMap)
{
Vector3 collisionPoint = _rayCast3D.GetCollisionPoint();
Vector3I cell = gridMap.LocalToMap(collisionPoint);
// GD.Print(cell);
gridMap.SetCellItem(cell, 1);
}
}
}
}
这样,鼠标悬浮的网格都会编程红色。
增加点击变红色和鼠标样式交互。
using Godot;
using System;
public partial class RayPickerCamera : Camera3D
{
private RayCast3D _rayCast3D;
[Export] public GridMap gridMap;
public override void _Ready()
{
_rayCast3D = GetNode<RayCast3D>("RayCast3D");
}
public override void _Process(double delta)
{
// 获取射线和鼠标交叉的坐标点
Vector2 mousePosition = GetViewport().GetMousePosition();
// GD.Print(mousePosition.ToString());
// ProjectLocalRayNormal 从屏幕点位置返回沿相机方向的法向量。
// 再赋值给射线的目标位置
// 因为指向射线只有单位向量(1米),这里乘以100,使向量穿过任何地面
_rayCast3D.TargetPosition = ProjectLocalRayNormal(mousePosition) * 100.0f;
// ForceRaycastUpdate:立即更新光线的碰撞信息,而不等待下一个_physics_process调用。
// 例如,当射线或它的父射线改变了状态时,使用此方法。
_rayCast3D.ForceRaycastUpdate();
// GetCollider:打印得到的第一个对撞对象。如果没有对象与射线相交,则返回null(即iscollision()返回false)。
// GetCollisionPoint:返回在全局坐标系中光线与最近物体相交的碰撞点。
// GD.PrintT(_rayCast3D.GetCollider(),_rayCast3D.GetCollisionPoint());
// 如果撞上了什么东西
if (_rayCast3D.IsColliding())
{
Input.SetDefaultCursorShape(Input.CursorShape.PointingHand);
GodotObject collider = _rayCast3D.GetCollider();
if (collider is GridMap)
{
// 鼠标点击
if (Input.IsActionPressed("click"))
{
Vector3 collisionPoint = _rayCast3D.GetCollisionPoint();
Vector3I cell = gridMap.LocalToMap(collisionPoint);
// GD.Print(cell);
// 如果没有炮台,再出现炮台
if (gridMap.GetCellItem(cell) == 0)
{
gridMap.SetCellItem(cell, 1);
}
}
}
}
else
{
Input.SetDefaultCursorShape();
}
}
}
制作炮塔
再增加2个节点。
创建Node3D节点TurretManager。
创建脚本TurretManager.cs。
using Godot;
using System;
public partial class TurretManager : Node3D
{
[Export]
public PackedScene turretScene;
public override void _Ready()
{
Node newTurret = turretScene.Instantiate();
AddChild(newTurret);
}
public override void _Process(double delta)
{
}
}
修改res://RayPickerCamera/RayPickerCamera.cs
using Godot;
using System;
public partial class RayPickerCamera : Camera3D
{
private RayCast3D _rayCast3D;
[Export] public GridMap gridMap;
[Export] public Node3D curretManage;
private TurretManager turretManagerScript;
public override void _Ready()
{
_rayCast3D = GetNode<RayCast3D>("RayCast3D");
if (curretManage != null)
{
turretManagerScript = curretManage as TurretManager;
}
}
public override void _Process(double delta)
{
// 获取射线和鼠标交叉的坐标点
Vector2 mousePosition = GetViewport().GetMousePosition();
// GD.Print(mousePosition.ToString());
// ProjectLocalRayNormal 从屏幕点位置返回沿相机方向的法向量。
// 再赋值给射线的目标位置
// 因为指向射线只有单位向量(1米),这里乘以100,使向量穿过任何地面
_rayCast3D.TargetPosition = ProjectLocalRayNormal(mousePosition) * 100.0f;
// ForceRaycastUpdate:立即更新光线的碰撞信息,而不等待下一个_physics_process调用。
// 例如,当射线或它的父射线改变了状态时,使用此方法。
_rayCast3D.ForceRaycastUpdate();
// GetCollider:打印得到的第一个对撞对象。如果没有对象与射线相交,则返回null(即iscollision()返回false)。
// GetCollisionPoint:返回在全局坐标系中光线与最近物体相交的碰撞点。
// GD.PrintT(_rayCast3D.GetCollider(),_rayCast3D.GetCollisionPoint());
// 如果撞上了什么东西
if (_rayCast3D.IsColliding())
{
Input.SetDefaultCursorShape(Input.CursorShape.PointingHand);
GodotObject collider = _rayCast3D.GetCollider();
if (collider is GridMap)
{
// 鼠标点击
if (Input.IsActionPressed("click"))
{
Vector3 collisionPoint = _rayCast3D.GetCollisionPoint();
Vector3I cell = gridMap.LocalToMap(collisionPoint);
// GD.Print(cell);
// 如果没有炮台,再出现炮台
if (gridMap.GetCellItem(cell) == 0)
{
gridMap.SetCellItem(cell, 1);
//得到网格的坐本地坐标空间中的位置。(全局坐标)
Vector3 tilePosition = gridMap.MapToLocal(cell);
// 传递给炮塔创建
turretManagerScript.buildTurret(tilePosition);
}
}
}
}
else
{
Input.SetDefaultCursorShape();
}
}
}
修改TurretManager.cs
using Godot;
using System;
public partial class TurretManager : Node3D
{
// 可实例化的场景
[Export] public PackedScene turretScene;
public override void _Ready()
{
}
public override void _Process(double delta)
{
}
public void buildTurret(Vector3 turretPosition)
{
// 必须强转成Node3D才有GlobalPosition属性
Node3D newTurret = (Node3D)turretScene.Instantiate();
AddChild(newTurret);
// 设置全球坐标
newTurret.GlobalPosition = turretPosition;
}
}
运行,可以发现炮塔能放到网格中了。
炮弹
新建炮弹场景res://Turret/projectile.tscn。设置颜色 #aa2820
根节点新增脚本res://Turret/Projectile.cs
using Godot;
using System;
public partial class Projectile : Area3D
{
[Export] private float speed = 30.0f;
//默认z轴方向的向量
Vector3 direction = Vector3.Forward;
public override void _PhysicsProcess(double delta)
{
Position += direction * speed * (float)delta;
}
}
将炮弹场景加入炮塔场景中
运行,可以看到子弹发射出去,但是从地图中间。
现在需要炮塔发射炮弹,入炮塔场景删除炮弹场景,炮塔场景新增脚本
新增计时器组件。
using Godot;
using System;
public partial class Turret : Node3D
{
[Export] PackedScene projectilePrefab;
private MeshInstance3D turretTop;
public override void _Ready()
{
turretTop = GetNode<MeshInstance3D>("TurretBase/TurretTop");
}
public void onTimerTimeout()
{
Node3D shot = (Node3D)projectilePrefab.Instantiate();
AddChild(shot);
shot.GlobalPosition = turretTop.GlobalPosition;
}
}
炮弹销毁
炮弹场景增加计时器。
绑定响应事件。
res://Turret/Projectile.cs
using Godot;
using System;
public partial class Projectile : Area3D
{
[Export] private float speed = 30.0f;
//默认z轴方向的向量
Vector3 direction = Vector3.Forward;
public override void _PhysicsProcess(double delta)
{
Position += direction * speed * (float)delta;
}
public void onTimerTimeout()
{
QueueFree();
}
}
炮塔和炮弹打向敌人
修改TurretManager.cs
using Godot;
using System;
public partial class TurretManager : Node3D
{
// 可实例化的场景
[Export] public PackedScene turretScene;
// 导出敌人的路径变量
[Export] public Path3D enemyPath;
public override void _Ready()
{
}
public override void _Process(double delta)
{
}
public void buildTurret(Vector3 turretPosition)
{
// 必须强转成Node3D才有GlobalPosition属性
Node3D newTurret = (Node3D)turretScene.Instantiate();
Turret turretCs = newTurret as Turret;
AddChild(newTurret);
// 设置全球坐标
newTurret.GlobalPosition = turretPosition;
turretCs.enemyPath = enemyPath;
}
}
修改Turret.cs
using Godot;
using System;
using System.Linq;
public partial class Turret : Node3D
{
[Export] PackedScene projectilePrefab;
private MeshInstance3D turretTop;
public Path3D enemyPath;
public override void _Ready()
{
turretTop = GetNode<MeshInstance3D>("TurretBase/TurretTop");
}
public override void _PhysicsProcess(double delta)
{
// 炮塔总是对着最后一个敌人
Node3D enemy =
(Node3D)enemyPath.GetChildren().Last();
{
LookAt(enemy.GlobalPosition, Vector3.Up, true);
}
}
public void onTimerTimeout()
{
Area3D shot = (Area3D)projectilePrefab.Instantiate();
AddChild(shot);
Projectile projectileCs = shot as Projectile;
// 该节点的全局位置
shot.GlobalPosition = turretTop.GlobalPosition;
// 射击方向
// projectileCs.direction = Basis.Z;
projectileCs.direction = GlobalTransform.Basis.Z;
}}
打出伤害
res://Enemy/enemy.tscn增加碰撞体和分组
修改res://Enemy/Enemy.cs增加血量:
using Godot;
using System;
public partial class Enemy : PathFollow3D
{
[Export] public double speed = 2.5;
[Export] public int MaxHealth = 50;
private Node3D playerBase;
private PlayerBase playerBaseScript;
private int _currentHealth;
public int CurrentHealth
{
get => _currentHealth;
set
{
_currentHealth = value;
// 销毁自身
if (value < 1)
{
QueueFree();
}
}
}
public override void _Ready()
{
CurrentHealth = MaxHealth;
playerBase = (Node3D)GetTree().GetFirstNodeInGroup("PlayerBase");
}
public override void _Process(double delta)
{
this.Progress += (float)(delta * speed);
if (playerBase != null)
{
playerBaseScript = playerBase as PlayerBase;
}
if (ProgressRatio >= 1.0)
{
// 调用Script中的方法
playerBaseScript?.takeDamage();
// 停止运行_Process函数
SetProcess(false);
// Free();
}
}
}
修改炮弹增加退出:
using Godot;
using System;
public partial class Projectile : Area3D
{
[Export] private float speed = 30.0f;
//默认z轴方向的向量
public Vector3 direction = Vector3.Forward;
public override void _PhysicsProcess(double delta)
{
Position += direction * speed * (float)delta;
}
public void onTimerTimeout()
{
QueueFree();
}
public void _on_area_entered(Area3D area)
{
if (area.IsInGroup("enemy_area"))
{
Enemy enemyCs = area.GetParent() as Enemy;
enemyCs.CurrentHealth -= 25;
QueueFree();
}
}
}
优化一下Tureet.cs
using Godot;
using System;
using System.Linq;
public partial class Turret : Node3D
{
[Export] PackedScene projectilePrefab;
private MeshInstance3D turretTop;
public Path3D enemyPath;
private PathFollow3D enemyTarget;
public override void _Ready()
{
turretTop = GetNode<MeshInstance3D>("TurretBase/TurretTop");
}
public override void _PhysicsProcess(double delta)
{
// 炮塔总是对着最后一个敌人
// Node3D enemy =
// (Node3D)enemyPath.GetChildren().Last();
enemyTarget = findBaseTarget();
if (enemyTarget != null)
{
LookAt(enemyTarget.GlobalPosition, Vector3.Up, true);
}
}
public void onTimerTimeout()
{
// 没有目标,停止射击
if (enemyTarget != null)
{
Area3D shot = (Area3D)projectilePrefab.Instantiate();
AddChild(shot);
Projectile projectileCs = shot as Projectile;
// 该节点的全局位置
shot.GlobalPosition = turretTop.GlobalPosition;
// 射击方向
// projectileCs.direction = Basis.Z;
projectileCs.direction = GlobalTransform.Basis.Z;
}
}
// 永远打最前面一个敌人
public PathFollow3D findBaseTarget()
{
PathFollow3D bestTarget = null;
float baseProgress = 0.0f;
foreach (Node enemy in enemyPath.GetChildren())
{
if (enemy is PathFollow3D)
{
PathFollow3D enemyPf = enemy as PathFollow3D;
if (enemyPf.Progress > baseProgress)
{
bestTarget = enemyPf;
baseProgress = enemyPf.Progress;
}
}
}
return bestTarget;
}
}
约束炮塔打击范围
using Godot;
using System;
using System.Linq;
public partial class Turret : Node3D
{
// 炮弹实体
[Export] PackedScene projectilePrefab;
// 打击范围
[Export] float turretRange = 10.0f;
private MeshInstance3D turretTop;
public Path3D enemyPath;
private PathFollow3D enemyTarget;
public override void _Ready()
{
turretTop = GetNode<MeshInstance3D>("TurretBase/TurretTop");
}
public override void _PhysicsProcess(double delta)
{
// 炮塔总是对着最后一个敌人
// Node3D enemy =
// (Node3D)enemyPath.GetChildren().Last();
enemyTarget = findBaseTarget();
if (enemyTarget != null)
{
LookAt(enemyTarget.GlobalPosition, Vector3.Up, true);
}
}
public void onTimerTimeout()
{
// 没有目标,停止射击
if (enemyTarget != null)
{
Area3D shot = (Area3D)projectilePrefab.Instantiate();
AddChild(shot);
Projectile projectileCs = shot as Projectile;
// 该节点的全局位置
shot.GlobalPosition = turretTop.GlobalPosition;
// 射击方向
// projectileCs.direction = Basis.Z;
projectileCs.direction = GlobalTransform.Basis.Z;
}
}
// 永远打最前面一个敌人
public PathFollow3D findBaseTarget()
{
PathFollow3D bestTarget = null;
float baseProgress = 0.0f;
foreach (Node enemy in enemyPath.GetChildren())
{
if (enemy is PathFollow3D)
{
PathFollow3D enemyPf = enemy as PathFollow3D;
// 返回2个向量之间的距离
float distanceTo = GlobalPosition.DistanceTo(enemyPf.GlobalPosition);
if (enemyPf.Progress > baseProgress && distanceTo <= turretRange)
{
bestTarget = enemyPf;
baseProgress = enemyPf.Progress;
}
}
}
return bestTarget;
}
}
动画播放
新增组件DamageHight
动画播放新增动画。
调整参数,增加visible属性动画控制。
将默认状态重置为不可见。
修改脚本,增加动画播放。
using Godot;
using System;
public partial class Enemy : PathFollow3D
{
[Export] public double speed = 2.5;
[Export] public int MaxHealth = 50;
private Node3D playerBase;
private PlayerBase playerBaseScript;
private AnimationPlayer animationPlayer;
private int _currentHealth;
public int CurrentHealth
{
get => _currentHealth;
set
{
if (value < CurrentHealth)
{
animationPlayer.Play("TakeDamage");
}
_currentHealth = value;
// 销毁自身
if (value < 1)
{
QueueFree();
}
}
}
public override void _Ready()
{
animationPlayer=GetNode<AnimationPlayer>("AnimationPlayer");
CurrentHealth = MaxHealth;
playerBase = (Node3D)GetTree().GetFirstNodeInGroup("PlayerBase");
}
public override void _Process(double delta)
{
this.Progress += (float)(delta * speed);
if (playerBase != null)
{
playerBaseScript = playerBase as PlayerBase;
}
if (ProgressRatio >= 1.0)
{
// 调用Script中的方法
playerBaseScript?.takeDamage();
// 停止运行_Process函数
SetProcess(false);
// Free();
}
}
}
同理,炮台也增加动画,控制TurretTop的Z轴,模拟炮台后坐力动画。
修改脚本:
using Godot;
using System;
using System.Linq;
public partial class Turret : Node3D
{
// 炮弹实体
[Export] PackedScene projectilePrefab;
// 打击范围
[Export] float turretRange = 10.0f;
private MeshInstance3D turretTop;
public Path3D enemyPath;
private PathFollow3D enemyTarget;
private AnimationPlayer animationPlayer;
public override void _Ready()
{
animationPlayer=GetNode<AnimationPlayer>("AnimationPlayer");
turretTop = GetNode<MeshInstance3D>("TurretBase/TurretTop");
}
public override void _PhysicsProcess(double delta)
{
// 炮塔总是对着最后一个敌人
// Node3D enemy =
// (Node3D)enemyPath.GetChildren().Last();
enemyTarget = findBaseTarget();
if (enemyTarget != null)
{
LookAt(enemyTarget.GlobalPosition, Vector3.Up, true);
}
}
public void onTimerTimeout()
{
// 没有目标,停止射击
if (enemyTarget != null)
{
Area3D shot = (Area3D)projectilePrefab.Instantiate();
AddChild(shot);
Projectile projectileCs = shot as Projectile;
// 该节点的全局位置
shot.GlobalPosition = turretTop.GlobalPosition;
// 射击方向
// projectileCs.direction = Basis.Z;
projectileCs.direction = GlobalTransform.Basis.Z;
animationPlayer.Play("fire");
}
}
// 永远打最前面一个敌人
public PathFollow3D findBaseTarget()
{
PathFollow3D bestTarget = null;
float baseProgress = 0.0f;
foreach (Node enemy in enemyPath.GetChildren())
{
if (enemy is PathFollow3D)
{
PathFollow3D enemyPf = enemy as PathFollow3D;
// 返回2个向量之间的距离
float distanceTo = GlobalPosition.DistanceTo(enemyPf.GlobalPosition);
if (enemyPf.Progress > baseProgress && distanceTo <= turretRange)
{
bestTarget = enemyPf;
baseProgress = enemyPf.Progress;
}
}
}
return bestTarget;
}
}
运行:
自动创建敌人
修改res://BaseLevel.tscn场景。
EnemyPath节点增加脚本res://EnemyPath.cs
using Godot;
using System;
public partial class EnemyPath : Path3D
{
[Export] public PackedScene EnemyScene;
[Export] public int EnemyCount = 20;
private int currEnemyCount;
public override void _Ready()
{
currEnemyCount = EnemyCount;
}
public override void _Process(double delta)
{
}
public void spawnEnemy()
{
Node3D newEnemy = (Node3D)EnemyScene.Instantiate();
AddChild(newEnemy);
}
public void _on_timer_timeout()
{
if (currEnemyCount > 0)
{
currEnemyCount--;
spawnEnemy();
}
}
}
钱
增加组件MarginContainer。组件下增加一个文本。
创建脚本Bank.cs
using Godot;
using System;
public partial class Bank : MarginContainer
{
[Export] public int startingGold = 150;
private Label label;
private int _currGold;
public int CurrGold
{
get => _currGold;
set
{
_currGold = Math.Max(value, 0);
label.Text = $"金币:{_currGold}";
}
}
public override void _Ready()
{
label = GetNode<Label>("Label");
CurrGold = startingGold;
}
public override void _Process(double delta)
{
}
}
修改res://RayPickerCamera/RayPickerCamera.cs,让敌人产生受金币限制。
using Godot;
using System;
public partial class RayPickerCamera : Camera3D
{
private RayCast3D _rayCast3D;
[Export] public GridMap gridMap;
[Export] public Node3D curretManage;
private Bank bankCs;
private PlayerBase playerBaseCs;
private TurretManager turretManagerScript;
public override void _Ready()
{
bankCs = GetTree().GetFirstNodeInGroup("bank") as Bank;
playerBaseCs = GetTree().GetFirstNodeInGroup("PlayerBase") as PlayerBase;
_rayCast3D = GetNode<RayCast3D>("RayCast3D");
if (curretManage != null)
{
turretManagerScript = curretManage as TurretManager;
}
}
public override void _Process(double delta)
{
// 获取射线和鼠标交叉的坐标点
Vector2 mousePosition = GetViewport().GetMousePosition();
// GD.Print(mousePosition.ToString());
// ProjectLocalRayNormal 从屏幕点位置返回沿相机方向的法向量。
// 再赋值给射线的目标位置
// 因为指向射线只有单位向量(1米),这里乘以100,使向量穿过任何地面
_rayCast3D.TargetPosition = ProjectLocalRayNormal(mousePosition) * 100.0f;
// ForceRaycastUpdate:立即更新光线的碰撞信息,而不等待下一个_physics_process调用。
// 例如,当射线或它的父射线改变了状态时,使用此方法。
_rayCast3D.ForceRaycastUpdate();
// GetCollider:打印得到的第一个对撞对象。如果没有对象与射线相交,则返回null(即iscollision()返回false)。
// GetCollisionPoint:返回在全局坐标系中光线与最近物体相交的碰撞点。
// GD.PrintT(_rayCast3D.GetCollider(),_rayCast3D.GetCollisionPoint());
// 如果撞上了什么东西
if (_rayCast3D.IsColliding())
{
// 有钱才允许建炮台
if (bankCs.CurrGold >= playerBaseCs.TurretCost)
{
// 鼠标变小手
Input.SetDefaultCursorShape(Input.CursorShape.PointingHand);
GodotObject collider = _rayCast3D.GetCollider();
if (collider is GridMap)
{
// 鼠标点击
if (Input.IsActionPressed("click"))
{
Vector3 collisionPoint = _rayCast3D.GetCollisionPoint();
Vector3I cell = gridMap.LocalToMap(collisionPoint);
// GD.Print(cell);
// 如果没有炮台,再出现炮台
if (gridMap.GetCellItem(cell) == 0)
{
gridMap.SetCellItem(cell, 1);
//得到网格的坐本地坐标空间中的位置。(全局坐标)
Vector3 tilePosition = gridMap.MapToLocal(cell);
// 传递给炮塔创建
turretManagerScript.buildTurret(tilePosition);
// 建好炮台就扣钱
bankCs.CurrGold -= playerBaseCs.TurretCost;
}
}
}
}
else
{
// 鼠标样式小手变默认
Input.SetDefaultCursorShape();
}
}
else
{
Input.SetDefaultCursorShape();
}
}
}
修改res://Enemy/Enemy.cs,让敌人死亡提供金币。
using Godot;
using System;
public partial class Enemy : PathFollow3D
{
[Export] public double speed = 2.5;
[Export] public int MaxHealth = 50;
[Export] public int DeadGold = 15;
private Node3D playerBase;
private PlayerBase playerBaseScript;
private AnimationPlayer animationPlayer;
private Bank bankCs;
private int _currentHealth;
public int CurrentHealth
{
get => _currentHealth;
set
{
if (value < CurrentHealth)
{
animationPlayer.Play("TakeDamage");
}
_currentHealth = value;
// 销毁自身
if (value < 1)
{
bankCs.CurrGold += DeadGold;
QueueFree();
}
}
}
public override void _Ready()
{
bankCs = GetTree().GetFirstNodeInGroup("bank") as Bank;
animationPlayer = GetNode<AnimationPlayer>("AnimationPlayer");
CurrentHealth = MaxHealth;
playerBase = (Node3D)GetTree().GetFirstNodeInGroup("PlayerBase");
}
public override void _Process(double delta)
{
this.Progress += (float)(delta * speed);
if (playerBase != null)
{
playerBaseScript = playerBase as PlayerBase;
}
if (ProgressRatio >= 1.0)
{
// 调用Script中的方法
playerBaseScript?.takeDamage();
// 停止运行_Process函数
SetProcess(false);
// Free();
}
}
}
修改res://PlayerBase/PlayerBase.cs,初始化炮台制造的耗费。
public partial class PlayerBase : Node3D
{
......
[Export] public int TurretCost = 100;
......
}
敌人难度曲线增加
增加Node节点DifficultyManager,当做困难管理器。
该节点下新增节点Timer。
DifficultyManager挂载脚本DifficultyManager/DifficultyManagerCs.cs。
using Godot;
using System;
public partial class DifficultyManagerCs : Node
{
/// <summary>
/// 游戏时间长度
/// </summary>
[Export] public float ExGameLength = 10.0f;
// 制作敌人时间曲线
[Export] public Curve ExSpawnTimeCurve;
private Timer _timer;
public override void _Ready()
{
_timer = GetNode<Timer>("Timer");
_timer.Start(ExGameLength);
}
public override void _Process(double delta)
{
GD.Print(getSpawnTime());
}
public float gameProgressRatio()
{
return 1.0f - ((float)_timer.TimeLeft / ExGameLength);
}
public float getSpawnTime()
{
return ExSpawnTimeCurve.Sample(gameProgressRatio());
}
}
记得重新加载一下节点(删掉,然后重新拉入),否则代码不会生效:
修改EnemyPath节点,
EnemyPath.cs,将困难管理器当参数传入路径节点的脚本中,
using Godot;
using System;
using System.Diagnostics;
using Godot.Collections;
/// <summary>
/// 敌人路径
/// </summary>
public partial class EnemyPath : Path3D
{
[Export] public PackedScene ExEnemyScene;
/// <summary>
/// 计时器曲线
/// </summary>
[Export] public Node ExDifficultyManager;
private DifficultyManagerCs difficultyManagerCs;
private Timer exTimer;
public override void _Ready()
{
exTimer = (Timer)GetNode("Timer");
difficultyManagerCs= ExDifficultyManager as DifficultyManagerCs;
GD.Print(difficultyManagerCs);
}
public void spawnEnemy()
{
Node3D newEnemy = (Node3D)ExEnemyScene.Instantiate();
AddChild(newEnemy);
if (difficultyManagerCs != null)
{
exTimer.WaitTime = difficultyManagerCs.getSpawnTime();
GD.Print(exTimer.WaitTime);
}
}
public void _on_timer_timeout()
{
spawnEnemy();
}
}
通过绑定困难曲线联动时间工具节点。
ExGameLength调成30f。
敌人生命值曲线增加
困难管理器增加导入属性。
// 制作敌人生命值变化曲线
[Export] public Curve ExEnemyHealthCurve;
记得参数是存入场景中,再重新加载场景。
定义一个信号结束生产敌人
修改DifficultyManager/DifficultyManager.cs
using Godot;
using System;
using Timer = Godot.Timer;
public partial class DifficultyManager : Node
{
[Signal]
public delegate void StopSpawningEnemiesEventHandler();
/// <summary>
/// 游戏时间长度
/// </summary>
[Export] public float ExGameLength = 10.0f;
// 制作敌人出现时间曲线
[Export] public Curve ExSpawnTimeCurve;
// 制作敌人生命值变化曲线
[Export] public Curve ExEnemyHealthCurve;
private Timer _timer;
public override void _Ready()
{
_timer = GetNode<Timer>("Timer");
_timer.Start(ExGameLength);
}
public override void _Process(double delta)
{
// GD.Print(GetSpawnTime());
}
public float GameProgressRatio()
{
return 1.0f - ((float)_timer.TimeLeft / ExGameLength);
}
/// <summary>
/// 获取敌人出现时间
/// </summary>
/// <returns></returns>
public float GetSpawnTime()
{
// 返回沿曲线存在于X位置偏移处的点的Y值
return ExSpawnTimeCurve.Sample(GameProgressRatio());
}
/// <summary>
/// 获取敌人生命值
/// </summary>
/// <returns></returns>
public int GetEnemyHealth()
{
// 返回沿曲线存在于X位置偏移处的点的Y值
return (int)Math.Round(ExEnemyHealthCurve.Sample(GameProgressRatio()));
}
public void OnTimerTimeout()
{
GD.Print("触发OnTimerTimeout事件!");
// 发送自定义信号
EmitSignal(SignalName.StopSpawningEnemies);
}
}
增加信号并绑定:
修改EnemyPath.cs
using Godot;
using System;
using System.Diagnostics;
using Godot.Collections;
/// <summary>
/// 敌人路径
/// </summary>
public partial class EnemyPath : Path3D
{
[Export] public PackedScene ExEnemyScene;
/// <summary>
/// 计时器曲线
/// </summary>
[Export] public Node ExDifficultyManager;
private DifficultyManager DifficultyManager;
private Timer _timer;
public override void _Ready()
{
_timer = (Timer)GetNode("Timer");
DifficultyManager = ExDifficultyManager as DifficultyManager;
// GD.Print(DifficultyManager);
}
/// <summary>
/// 敌人产卵方法
/// </summary>
public void spawnEnemy()
{
Enemy newEnemy = ExEnemyScene.Instantiate() as Enemy;
if (newEnemy == null)
{
return;
}
// 设置新的最大生命值
newEnemy.MaxHealth = DifficultyManager.GetEnemyHealth();
AddChild(newEnemy);
if (DifficultyManager != null)
{
_timer.WaitTime = DifficultyManager.GetSpawnTime();
// GD.Print(_timer.WaitTime);
GD.Print(newEnemy.CurrentHealth);
}
}
public void _on_timer_timeout()
{
spawnEnemy();
}
/// <summary>
/// 游戏结束,停止敌人产卵
/// </summary>
public void OnDifficultyManagerStopSpawning()
{
GD.Print("进入OnDifficultyManagerStopSpawning");
_timer.Stop();
}
}
完善敌人销毁,修改Enemy/Enemy.cs,增加QueueFree函数使用,还可以调节速度。
......
public override void _Ready()
{
bankCs = GetTree().GetFirstNodeInGroup("bank") as Bank;
animationPlayer = GetNode<AnimationPlayer>("AnimationPlayer");
CurrentHealth = MaxHealth;
playerBase = (Node3D)GetTree().GetFirstNodeInGroup("PlayerBase");
// speed = 13;
}
public override void _Process(double delta)
{
this.Progress += (float)(delta * speed);
if (playerBase != null)
{
playerBaseScript = playerBase as PlayerBase;
}
if (ProgressRatio >= 1.0)
{
// 调用Script中的方法
playerBaseScript?.takeDamage();
// 停止运行_Process函数
SetProcess(false);
// Free();
QueueFree();
}
}
......
程序结束时,页面重新加载,报错:
启动CSG多边形父子依赖就好。
游戏通关
所有敌人被消灭则意味着通关。
增加游戏通关结算页面ExVictoryLayer。
将颜色设置半透明。
修改脚本EnemyPath.cs。
using Godot;
using System;
using System.Diagnostics;
using Godot.Collections;
/// <summary>
/// 敌人路径
/// </summary>
public partial class EnemyPath : Path3D
{
[Export] public PackedScene ExEnemyScene;
/// <summary>
/// 计时器曲线
/// </summary>
[Export] public Node ExDifficultyManager;
[Export] public CanvasLayer ExVictoryLayer;
private DifficultyManager _difficultyManager;
private Timer _timer;
public override void _Ready()
{
_timer = (Timer)GetNode("Timer");
_difficultyManager = ExDifficultyManager as DifficultyManager;
// GD.Print(_difficultyManager);
}
/// <summary>
/// 敌人产卵方法
/// </summary>
public void spawnEnemy()
{
Enemy newEnemy = ExEnemyScene.Instantiate() as Enemy;
if (newEnemy == null)
{
return;
}
// 设置新的最大生命值
newEnemy.MaxHealth = _difficultyManager.GetEnemyHealth();
AddChild(newEnemy);
if (_difficultyManager != null)
{
_timer.WaitTime = _difficultyManager.GetSpawnTime();
// GD.Print(_timer.WaitTime);
GD.Print(newEnemy.CurrentHealth);
// 将树退出的信号连接到enemyDefeated函数
newEnemy.TreeExited += enemyDefeated;
}
}
public void _on_timer_timeout()
{
spawnEnemy();
}
/// <summary>
/// 游戏结束,停止敌人产卵
/// </summary>
public void OnDifficultyManagerStopSpawning()
{
GD.Print("进入OnDifficultyManagerStopSpawning");
_timer.Stop();
}
/// <summary>
/// 敌人都被击败
/// </summary>
public void enemyDefeated()
{
if (_timer.IsStopped())
{
for (int i = 0; i < GetChildren().Count; i++)
{
Node node = GetChildren()[i];
// 如果找到子节点为路径节点,则停止后续代码执行
if (node is PathFollow3D)
{
return;
}
}
GD.Print("你赢了!");
ExVictoryLayer.Visible = true;
}
}
}
将默认visiable设置为停用,导入组件。