项目素材及源码
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设置为停用,导入组件。
用户游戏结束操作界面
将VictoryLayer保存为场景。
这里使用中心容器,就是容器里面的东西会布局在屏幕最中间。
/BarbarianBlaster/UserInterface/VictoryLayer.tscn
打开2D试图。
描点选择全屏。
增加PanelContainer组件。
设置最小尺寸。
引入垂直箱容器VBoxContainer。
加入文字并设置样式。
增加水平容器HBoxContainer,并增加button按钮。
重复类似操作,设计3个按钮。
增加脚本。
按钮绑定事件。
using Godot;
using System;
public partial class VictoryLayer : CanvasLayer
{
// 当重玩按钮被点击时调用
public void OnRetryButtonClick()
{
// 打印重玩
GD.Print("重玩");
GetTree().ReloadCurrentScene();
}
// 当退出按钮被点击时调用此方法
public void OnQuitButtonClick()
{
// 打印退出
GD.Print("退出");
GetTree().Quit();
}
// 当点击下一关按钮时调用此方法
public void OnNextButtonClick()
{
// 打印"下一关"
GD.Print("下一关");
}
}
运行:
重新赋值引入:
VictoryLayer场景内显示,引入时隐藏即可。运行,效果正常。
游戏奖励实现
增加一个HBoxContainer,再增加子节点TextureRect。
设置图片为伸展模式,并比例改为以方面为中心,并设置大小。
增加2个标签。
修改代码UserInterface/VictoryLayer.cs
using Godot;
public partial class VictoryLayer : CanvasLayer
{
private static TextureRect _star1;
private static TextureRect _star2;
private static TextureRect _star3;
private static Label _survivedLabel;
private static Label _healthLabel;
public static PlayerBase BaseLevel;
public override void _Ready()
{
_star1 = GetNode<TextureRect>("%Star1");
_star2 = GetNode<TextureRect>("%Star2");
_star3 = GetNode<TextureRect>("%Star3");
_survivedLabel = GetNode<Label>("%SurvivedLabel");
_healthLabel = GetNode<Label>("%HealthLabel");
BaseLevel = (PlayerBase)GetTree().GetFirstNodeInGroup("PlayerBase");
}
public void Victory()
{
// 将Visible属性设置为true
Visible = true;
if (BaseLevel.MaxHealth == BaseLevel.CurrentHealth)
{
_star2.Modulate = new Color(Colors.White);
_healthLabel.Visible = true;
}
}
// 当重玩按钮被点击时调用
public void OnRetryButtonClick()
{
// 打印重玩
GD.Print("重玩");
GetTree().ReloadCurrentScene();
}
// 当退出按钮被点击时调用此方法
public void OnQuitButtonClick()
{
// 打印退出
GD.Print("退出");
GetTree().Quit();
}
// 当点击下一关按钮时调用此方法
public void OnNextButtonClick()
{
// 打印"下一关"
GD.Print("下一关");
}
}
修改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 VictoryLayer _victoryLayer;
private Timer _timer;
public override void _Ready()
{
_timer = (Timer)GetNode("Timer");
_difficultyManager = ExDifficultyManager as DifficultyManager;
_victoryLayer = ExVictoryLayer as VictoryLayer;
// 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;
}
}
_victoryLayer.Victory();
}
}
}
修改银行的金额(Bank)到500,运行:
修改UserInterface/VictoryLayer.cs
using Godot;
public partial class VictoryLayer : CanvasLayer
{
......
public static Bank BankCs;
public override void _Ready()
{
......
BankCs = (Bank)GetTree().GetFirstNodeInGroup("bank");
}
public void Victory()
{
......
// 如果当前金币数大于等于500
if (BankCs.CurrGold >= 500)
{
// 将_star3的颜色设置为白色
_star3.Modulate = new Color(Colors.White);
}
}
......
}
进一步优化,可以如下效果:
使用GLB文件制作酷炫效果。
打开/BarbarianBlaster/BaseLevel.tscn
根节点鼠标右键实例化子场景。
这样,我们可以删除CSGCylinder3D,一个效果非常好的玩家城堡就做好了。
ctrl+shift+O搜索炮塔场景,打开。
/BarbarianBlaster/Turret/turret.tscn
插入场景,并将之前的组件都降级Node3D就够用了。
调整炮塔的Z轴,让炮塔在塔基的上面。
运行会发现,炮楼会跟个旋转。
修改Turret/Turret.cs
using Godot;
namespace BarbarianBlaster.Turret;
public partial class Turret : Node3D
{
// 炮弹实体
[Export] PackedScene _projectilePrefab;
// 打击范围
[Export] float _turretRange = 10.0f;
private Node3D _cannon;
private Node3D _turretBase;
public Path3D EnemyPath;
private PathFollow3D _enemyTarget;
private AnimationPlayer _animationPlayer;
public override void _Ready()
{
_animationPlayer=GetNode<AnimationPlayer>("AnimationPlayer");
_cannon = GetNode<Node3D>("TurretBase/TurretTop/Cannon");
_turretBase = GetNode<Node3D>("TurretBase");
}
public override void _PhysicsProcess(double delta)
{
// 炮塔总是对着最后一个敌人
// Node3D enemy =
// (Node3D)enemyPath.GetChildren().Last();
_enemyTarget = FindBaseTarget();
if (_enemyTarget != null)
{
_turretBase.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 = _cannon.GlobalPosition;
// 射击方向,设计方向是大炮的世界坐标的Z轴方向
// projectileCs.direction = Basis.Z;
if (projectileCs != null) projectileCs.direction = _turretBase.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;
}
}
背景场景显示
打开/BarbarianBlaster/MeshLibraries/SpaceLibrary.tscn
将TurretBase的材质设置为透明(#d63a2900)。
将FreeSpace设置为#3f3f3f。
导出网格库,覆盖即可。
结合GridMap,完成树木和石头的摆放。
修改CSGBox3D为FloorCSGBox3D名称,设置材质。颜色为#8f994d。然后点击烘焙,这样,绿色背景地板就布置好了。
野蛮人加载
双击资源打开,将原生奔跑动作的循环模式设置为Linear(线性),然后选择重新导入。
导入资源并设置为“子节点可编辑”。
这里删掉了之前做的胶囊,并且把颜色给了一个高亮体。
把资源原生的皮肤和骨架复制到高亮体上。
这样,控制显隐,就可以实现打击效果。
然后替换重新制作精灵动画即可。
把动画人物的=奔跑动画设置为进入场景就循环奔跑。
运行,发现人物是倒着走的,可以如下修改。
微调人物Y轴,让人物的脚别陷入地面。
运行:
项目效果润色
右上角点击“项目”-项目设置-搜索“抗锯齿”,将3D抗锯齿设置成4X。
设置可以全屏。
我们这种3D游戏就设置成canvas_items即可。
设置字体。
比如胜利结算页面,我们可以设置主题,这样主题以下的所有字体,都会同步修改。
新建好后可以看到下面有很多预设的主题效果。
开源字体查找网站:
https://fonts.google.com/
比方说我要简体中文的,过滤语言。
这里我选择的是
ZCOOLKuaiLe-Regular.ttf
。现在金币也要使用的话,复制粘贴主题即可。
Label3D不能设置主题,直接设置字体样式即可。
如果是英文的话,还可以快捷设置是否全部大写。
调整关卡难度
修改Turret/Projectile.cs
using Godot;
using System;
public partial class Projectile : Area3D
{
[Export] private float _speed = 30.0f;
[Export] private int _damage = 25;
//默认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 -= _damage;
QueueFree();
}
}
}
修改DifficultyManager/DifficultyManager.cs
......
public partial class DifficultyManager : Node
{
......
// 重写_Ready()方法
public override void _Ready()
{
// 获取Timer节点
_timer = GetNode<Timer>("Timer");
// 开始计时器,计时时间为ExGameLength
_timer.Start(ExGameLength);
// 设置游戏速度
Engine.TimeScale = 5;
}
......
}
运行:
导出项目
window
下载https://github.com/electron/rcedit/releases/tag/v2.0.0
下载rcedit-x64.exe完成,放一个地方,配置好。
右上角“编辑器-编辑器设置-导出-windows”。
导出到
F:\ws\godot_ws\Game\野蛮爆破手
右上角“项目-导出-windows Desktop”。
安卓
https://docs.godotengine.org/en/stable/tutorials/export/exporting_for_android.html
右上角“编辑器-编辑器设置-导出-android”,配置好Android SDK路径和java SDK路径,调试密钥库等信息不用改,用默认的。
导出,生成发布密钥库,找一个能找到路径的目录路径。
2021 年 8 月之后上传到 Google Play 的所有新应用都必须是 AAB (Android App Bundle) 文件。
将 AAB 或 APK 上传到 Google 的 Play 商店需要您使用非调试 密钥库文件;这样的文件可以像这样生成:
keytool -v -genkey -keystore bobokaka.keystore -alias bobokaka -keyalg RSA -validity 10000
我的密钥是123456,别名什么的参数都是bobokaka,国家是cn。
生成。
这里我的godot升级到4.4.1,.net升级到9.0 ,选择gradle打包构建,导出,等待。
这里我报错:
Could not unzip C:\Users\15044\.gradle\wrapper\dists\gradle-8.2-bin\bbg7u40eoinfdyxsxr3z4i7ta\gradle-8.2-bin.zip to C:\Users\15044\.gradle\wrapper\dists\gradle-8.2-bin\bbg7u40eoinfdyxsxr3z4i7ta.
Reason: zip END header not found
Exception in thread "main" java.util.zip.ZipException: zip END header not found
at java.base/java.util.zip.ZipFile$Source.findEND(ZipFile.java:1599)
at java.base/java.util.zip.ZipFile$Source.initCEN(ZipFile.java:1607)
at java.base/java.util.zip.ZipFile$Source.<init>(ZipFile.java:1445)
at java.base/java.util.zip.ZipFile$Source.get(ZipFile.java:1407)
at java.base/java.util.zip.ZipFile$CleanableResource.<init>(ZipFile.java:716)
at java.base/java.util.zip.ZipFile.<init>(ZipFile.java:250)
at java.base/java.util.zip.ZipFile.<init>(ZipFile.java:179)
at java.base/java.util.zip.ZipFile.<init>(ZipFile.java:193)
at org.gradle.wrapper.Install.unzip(Install.java:239)
at org.gradle.wrapper.Install.access$900(Install.java:27)
at org.gradle.wrapper.Install$1.call(Install.java:81)
at org.gradle.wrapper.Install$1.call(Install.java:48)
at org.gradle.wrapper.ExclusiveFileAccessManager.access(ExclusiveFileAccessManager.java:69)
at org.gradle.wrapper.Install.createDist(Install.java:48)
at org.gradle.wrapper.WrapperExecutor.execute(WrapperExecutor.java:107)
at org.gradle.wrapper.GradleWrapperMain.main(GradleWrapperMain.java:63)
把C:\Users\15044.gradle\wrapper\dists下的都删掉,重新导出:
继续报错:
Downloading https://services.gradle.org/distributions/gradle-8.2-bin.zip
Exception in thread "main" java.io.IOException: Downloading from https://services.gradle.org/distributions/gradle-8.2-bin.zip failed: timeout
at org.gradle.wrapper.Download.downloadInternal(Download.java:110)
at org.gradle.wrapper.Download.download(Download.java:67)
at org.gradle.wrapper.Install$1.call(Install.java:68)
at org.gradle.wrapper.Install$1.call(Install.java:48)
at org.gradle.wrapper.ExclusiveFileAccessManager.access(ExclusiveFileAccessManager.java:69)
at org.gradle.wrapper.Install.createDist(Install.java:48)
at org.gradle.wrapper.WrapperExecutor.execute(WrapperExecutor.java:107)
at org.gradle.wrapper.GradleWrapperMain.main(GradleWrapperMain.java:63)
Exit Code: 1
国内环境,应该会默认下载失败,手动下载Gradle包浏览器直接打开:https://mirrors.cloud.tencent.com/gradle/gradle-8.2-bin.zip
下载,替换缓存文件至C:\Users\用户名.gradle\wrapper\dists\gradle-8.2-bin\随机哈希目录,无需解压。
导出模板直接卡死,打包不下来,将.net降级到8.0,不用gradle构建,重新打包。
打包完成。