【Godot】3D塔防游戏-野蛮爆破手BarbarianBlaster

项目素材及源码

https://gitee.com/bobokaka/barbarian-blaster

游戏玩法设计

image.png

地板和环境三要素

image.png

照相机参数:


image.png

保存为BaseLevel.tscn场景文件。

制作网格库

创建res://MeshLibraries/SpaceLibrary.tscn 3D界面场景。
创建子节点MeshInstance3D。

image.png

设置材质,颜色为#cdcdcd。创建碰撞体。

image.png

image.png

鼠标选中FreeSpace节点。
右上角点击 场景。


image.png

image.png

image.png

如上,保存。

同上再创建一个炮台网格,颜色改为#d63a29。
两个物体重叠没关系。


image.png

同样的步骤,导出为网格图,然后覆盖保存到同一个文件。

打开SpaceLibrary.tres。我们能看到数组里面有2个网格材料了。


image.png

打开BaseLevel.tscn场景。

增加网格组件GridMap。然后再Mesh Library中,加载SpaceLibrary.tres,可以看到2个网格材料再右侧新增了一个列表展示,而且可以选中并拖动到主体界面中去。


image.png

设计线路

如果发现网格方向不对,可以试试这里,比如光标清除旋转

image.png

image.png

默认情况下,网格和格子对不上。解决方案就是调整右侧Size属性(4,4,4),因为我们网格块是4米x4米的大小。


image.png

倾斜发现,网格是悬空的。因为中心属性Y轴是打开的。


image.png

关掉Y轴为中心。


image.png

shift+鼠标右键,可以选取一块区域,再选中网格材料后,按ctrl+f会一次填充。或者按Del键删除。(这里层错了 应该是0层)


image.png
image.png

如下,红色的是敌人行动路线。


image.png

其他地方用白色填满,然后删除红色。


image.png

调整格子到(0,-4,0)。(这里层错了 应该是0层)


image.png

为敌人创建行走的路线

路径3D组件 Path3D。


image.png

image.png
image.png

创建CSG多边形节点CSGPolygon3D。


image.png

加载图形和配置参数。
最终效果如下


image.png
image.png
image.png

参数分别为:


image.png
image.png

PathFollow3D组件,Path3D的点采样器,必须为PAth3D的子节点,能够控制从开始走到结束的行动节奏。我们这个游戏得关闭循环。


image.png

敌人

image.png

创建圆柱体代表敌人。

image.png

敌人单独生成新的场景。保存到res://Enemy/enemy.tscn
image.png

image.png

根节点增加脚本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。

image.png

image.png

PlayerBase节点位置(-10,0,-12)。
进入PlayerBase场景
image.png

圆柱体颜色#e20005

根节点增加PlayerBase.cs脚本。

using Godot;
using System;

public partial class PlayerBase : Node3D
{

    public void takeDamage()
    {
        GD.Print("takeDamage");
    }
}

运行,发现敌人只要碰撞到基地(路程进度100%)才会触发碰撞。

基地掉血

增加文本。


image.png

image.png

修改脚本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个敌人,修改初始位置。


image.png

游戏掉血开发完成。

优化一下显示文字。

......
  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。

image.png
image.png

根节点增加脚本RayPickerCamera.cs。


image.png
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);
    }

打开调试的 显示碰撞区域。


image.png

运行:


image.png

修改脚本
    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()); 
    }
image.png

放置网格

修改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);
            }
        }
    }
}

这样,鼠标悬浮的网格都会编程红色。


image.png

增加点击变红色和鼠标样式交互。

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();
        }
    }
}

制作炮塔

创建res://Turret/turret.tscn场景

image.png

image.png

再增加2个节点。


image.png

she

创建Node3D节点TurretManager。


image.png

创建脚本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)
    {
    }
}
image.png

修改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;
    }
}

运行,可以发现炮塔能放到网格中了。


image.png

炮弹

新建炮弹场景res://Turret/projectile.tscn。设置颜色 #aa2820

image.png

根节点新增脚本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;
    }
}

将炮弹场景加入炮塔场景中


image.png

运行,可以看到子弹发射出去,但是从地图中间。


image.png

现在需要炮塔发射炮弹,入炮塔场景删除炮弹场景,炮塔场景新增脚本
新增计时器组件。


image.png

image.png
image.png

res://Turret/Turret.cs

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;
    }
}
image.png

炮弹销毁

炮弹场景增加计时器。

image.png

绑定响应事件。
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;
    }}
image.png

打出伤害

res://Enemy/enemy.tscn增加碰撞体和分组

image.png

修改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();
        }
    }
}

修改炮弹增加退出:


image.png
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;
    }
}

动画播放

image.png

新增组件DamageHight


image.png

image.png

动画播放新增动画。


image.png

调整参数,增加visible属性动画控制。


image.png

将默认状态重置为不可见。
image.png

修改脚本,增加动画播放。

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轴,模拟炮台后坐力动画。


image.png

image.png

image.png

修改脚本:

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;
    }
}

运行:


image.png

自动创建敌人

修改res://BaseLevel.tscn场景。

image.png

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();
        }
    }
}
image.png
image.png

增加组件MarginContainer。组件下增加一个文本。


image.png

image.png

创建脚本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;
......
}
image.png

敌人难度曲线增加

增加Node节点DifficultyManager,当做困难管理器。
该节点下新增节点Timer。


image.png

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());
    }
}
image.png

image.png

记得重新加载一下节点(删掉,然后重新拉入),否则代码不会生效:


image.png

修改EnemyPath节点,


image.png

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();
    }
}

通过绑定困难曲线联动时间工具节点。

image.png

ExGameLength调成30f。


image.png

敌人生命值曲线增加

困难管理器增加导入属性。

    // 制作敌人生命值变化曲线
    [Export] public Curve ExEnemyHealthCurve;
image.png

记得参数是存入场景中,再重新加载场景。

定义一个信号结束生产敌人

修改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);
    }
}

增加信号并绑定:


image.png
image.png

修改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();
        }
    }
......

程序结束时,页面重新加载,报错:


image.png

启动CSG多边形父子依赖就好。


image.png

游戏通关

所有敌人被消灭则意味着通关。
增加游戏通关结算页面ExVictoryLayer。


image.png
image.png

将颜色设置半透明。


image.png

修改脚本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设置为停用,导入组件。


image.png

用户游戏结束操作界面

将VictoryLayer保存为场景。
这里使用中心容器,就是容器里面的东西会布局在屏幕最中间。
/BarbarianBlaster/UserInterface/VictoryLayer.tscn

打开2D试图。


image.png

image.png

image.png

描点选择全屏。


image.png

增加PanelContainer组件。


image.png

设置最小尺寸。


image.png

引入垂直箱容器VBoxContainer。


image.png

加入文字并设置样式。


image.png
image.png

增加水平容器HBoxContainer,并增加button按钮。


image.png

重复类似操作,设计3个按钮。


image.png

增加脚本。
image.png

按钮绑定事件。

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("下一关");
    }
}
image.png

运行:


image.png

重新赋值引入:


image.png

VictoryLayer场景内显示,引入时隐藏即可。运行,效果正常。

游戏奖励实现

增加一个HBoxContainer,再增加子节点TextureRect。


image.png

设置图片为伸展模式,并比例改为以方面为中心,并设置大小。


image.png
image.png
image.png
image.png
image.png

增加2个标签。


image.png

修改代码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,运行:


image.png
image.png

修改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);
        }
    }
......
}
image.png

进一步优化,可以如下效果:


image.png

使用GLB文件制作酷炫效果。

打开/BarbarianBlaster/BaseLevel.tscn

根节点鼠标右键实例化子场景。


image.png
image.png

image.png

这样,我们可以删除CSGCylinder3D,一个效果非常好的玩家城堡就做好了。


image.png

ctrl+shift+O搜索炮塔场景,打开。
/BarbarianBlaster/Turret/turret.tscn
插入场景,并将之前的组件都降级Node3D就够用了。


image.png

调整炮塔的Z轴,让炮塔在塔基的上面。


image.png

运行会发现,炮楼会跟个旋转。

修改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


image.png

将TurretBase的材质设置为透明(#d63a2900)。

image.png

将FreeSpace设置为#3f3f3f。

image.png

导出网格库,覆盖即可。


image.png
image.png

结合GridMap,完成树木和石头的摆放。


image.png

修改CSGBox3D为FloorCSGBox3D名称,设置材质。颜色为#8f994d。然后点击烘焙,这样,绿色背景地板就布置好了。


image.png
image.png

野蛮人加载

双击资源打开,将原生奔跑动作的循环模式设置为Linear(线性),然后选择重新导入。


image.png

导入资源并设置为“子节点可编辑”。


image.png
image.png

这里删掉了之前做的胶囊,并且把颜色给了一个高亮体。


image.png

把资源原生的皮肤和骨架复制到高亮体上。


image.png

这样,控制显隐,就可以实现打击效果。

image.png

然后替换重新制作精灵动画即可。


image.png

把动画人物的=奔跑动画设置为进入场景就循环奔跑。


image.png

运行,发现人物是倒着走的,可以如下修改。


image.png

微调人物Y轴,让人物的脚别陷入地面。


image.png

运行:


image.png

项目效果润色

右上角点击“项目”-项目设置-搜索“抗锯齿”,将3D抗锯齿设置成4X。


image.png

设置可以全屏。


image.png

我们这种3D游戏就设置成canvas_items即可。

设置字体。
比如胜利结算页面,我们可以设置主题,这样主题以下的所有字体,都会同步修改。


image.png

新建好后可以看到下面有很多预设的主题效果。


image.png

开源字体查找网站:
https://fonts.google.com/
比方说我要简体中文的,过滤语言。

image.png

这里我选择的是ZCOOLKuaiLe-Regular.ttf
image.png

现在金币也要使用的话,复制粘贴主题即可。
image.png

Label3D不能设置主题,直接设置字体样式即可。


image.png
image.png

如果是英文的话,还可以快捷设置是否全部大写。


image.png

调整关卡难度

修改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;
    }

......
}

运行:


image.png

导出项目

window

下载https://github.com/electron/rcedit/releases/tag/v2.0.0

image.png

下载rcedit-x64.exe完成,放一个地方,配置好。
右上角“编辑器-编辑器设置-导出-windows”。
image.png

image.png

导出到F:\ws\godot_ws\Game\野蛮爆破手
右上角“项目-导出-windows Desktop”。
image.png

image.png

image.png

安卓

https://docs.godotengine.org/en/stable/tutorials/export/exporting_for_android.html
右上角“编辑器-编辑器设置-导出-android”,配置好Android SDK路径和java SDK路径,调试密钥库等信息不用改,用默认的。

image.png

image.png

导出,生成发布密钥库,找一个能找到路径的目录路径。
2021 年 8 月之后上传到 Google Play 的所有新应用都必须是 AAB (Android App Bundle) 文件。

将 AAB 或 APK 上传到 Google 的 Play 商店需要您使用非调试 密钥库文件;这样的文件可以像这样生成:

keytool -v -genkey -keystore bobokaka.keystore -alias bobokaka -keyalg RSA -validity 10000
image.png

image.png

我的密钥是123456,别名什么的参数都是bobokaka,国家是cn。
生成。


image.png

这里我的godot升级到4.4.1,.net升级到9.0 ,选择gradle打包构建,导出,等待。


image.png
image.png

这里我报错:

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\随机哈希目录,无需解压。

image.png

导出模板直接卡死,打包不下来,将.net降级到8.0,不用gradle构建,重新打包。


image.png

打包完成。

image.png
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容