开始

我们将参加4月的Ludum dare,这是一个为期3天的比赛。为了更好的完成,我们决定做一个小项目熟悉一下。我们的美术小祁提供了她的想法——我们将开发一个女巫召唤怪兽攻打冒险者的游戏!哈哈,听起来很有意思。这是一个塔防+小丑牌类型的游戏。

开发日记

3月7日

今晚,我们确定了游戏主题和具体玩法,不过实现小祁的滑动切换屏幕的方法我一时间没想出办法,目前我只做过按按钮切换场景的游戏,不过,策划Andy给出了很好的建议:可以通过在场景上设置三个摄像机来避免加载问题,是个很好的想法!有点迫不及待的想试试!

3月8日

今天,我实现了滑动游戏!或者说,避免加载场景的游戏?具体实现是通过设置三个摄像机,通过A/D键切换摄像机(改变摄像机的索引,设置显隐)。ヽ( ̄▽ ̄)ノ

我们还有一个设计是可以将第二个场景的卡牌拖入第三个场景,并且可以在某一范围内固定

最开始我的小猪脑转了,想的是使用DontDestoryOnLoad()这个方法,结果试着运行很多次也失败!原来是我本来的思路就是控制摄像机显隐,没有加载新场景!T ^ T 于是,我想了新的办法:

  • 按下鼠标 + 鼠标悬停在图片碰撞体上 -> 认为正在拖拽
  • 抬起鼠标 -> 认为不在拖拽
    • 如果在放置点附近 -> 固定图片

3月9日

我实现了昨天的想法!准备做塔防寻路的具体逻辑~

3月10日

今天,发生了一件让人悲伤的事。。。我理解错了策划的文档!我把第一个要实现的任务点理解错了,沟通后我才发现 T ^ T,今天又和策划交流了一下,确定了接下来要开发的具体内容:做塔防的具体逻辑!!!剩下的内容不急着实现。这件事情告诉我们,开发时要先考虑主要功能,再实现其他内容。

另外,今天我也在塔防游戏的制作方面有所收获!

  • 路标数组(用GUI里的调试画线方法辅助,这个后面写一篇文章记录)
  • 敌人逻辑
    • 寻路:在Update中朝目标点移动,到达目标点后更新目标点
  • 角色随机生成
    • 角色池的使用:创建角色 -> SetActive(false) -> 添加进角色池列表。然后再通过一个public方法返回第一个未激活的角色,在随机生成的脚本中,SetActive(true)

使用角色池的好处是:避免了频繁生成并销毁角色,减少了GC,提高了游戏性能。

3月11日

今天主要学习了ScriptObject脚本来配置数据,用这个脚本配置数据的好处是:在Inspector窗口可视,避免硬编码。

下面是详细的实现步骤:

  • 创建一个C#脚本,继承ScriptableObject脚本,如下:
1
2
3
4
5
6
7
8
9
using UnityEngine;

[CreateAssetMenu(fileName = "EnemyData", menuName = "Scriptable Objects/EnemyData")]
public class EnemyData : ScriptableObject
{
    public float lives;
    public int damage;
    public float speed;
}

这样操作后,下一次在Project窗口右键创建时就可以在Scriptable Objects/EnemyData路径下创建一个叫EnemyData的文件了。

另外,我还学到了一个修改同名字段的方法:选中后按住Ctrl + D(修改几次就按几次)

3月16日

前几天不太舒服加上比较忙,没有做太多游戏方面的更新。休息调整了一下,今天赶了一下进度。

目前为止,我已经完成了以下功能:

  • 敌人寻路
  • 塔的预制体制作
  • 敌人血条

敌人寻路在以上内容已经提及,现在先来记录一下塔的预制体制作相关。

塔的逻辑中,如何检测敌人进入塔的射击范围了呢?我们可以依照之前的思路,先给“塔”这个对象用ScriptObject脚本来配置数据。然后用碰撞器表示攻击范围(可以用OnDrawGizmos辅助查看),进入攻击范围的敌人,我们就把他加入攻击范围的列表,然后处理攻击逻辑。

处理攻击逻辑自然需要一个抛射物,这个抛射物也可以做成池对象来提升性能,在射击时间段中改变抛射物的位置即可。

再回来处理攻击逻辑,只要这个攻击范围的列表长度大于0,我们就需要生成一个抛射物,调用抛射物中的攻击方法。

最后,别忘了将被攻击掉的敌人加入已被移出的列表!方便切换处理下一波。

血条制作方面不多叙述,就是血条的Transform里面的Vector3.x乘以一个当前生命值与初始生命值的比,在Update()函数中不断更新就可以。

3月22日

放置我方防御塔的方面,我本来想的是可能需要瓦片地图来实现(?不过这样还得和美术那边商量一下,不过,好消息是,我们组中又招了新的成员,其中,阿然老师比我更有经验,于是我决定去问问他。

他给我提供了一个思路:可以将整个画面定义为一张网格,通过程序设定好哪些地方(坐标)可以放置,这是个很好的注意,于是,我开始实现。

我了解到,这其实是一个经典写法,核心思路是将「世界坐标」映射到「格子坐标」。整个过程分为三步:

  1. 把“世界坐标”换成“格子坐标”
  2. 用一个结构记录哪些格子允许建造 / 已经被占用
  3. 鼠标移动时吸附到最近格子,点击时检查是否合法再放下建筑

首先来看把世界坐标转为格子坐标,我们需要建一个2D网格系统:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
using UnityEngine;

public class Grid2D : MonoBehaviour
{
public Vector2 origin = Vector2.zero; // 网格原点(世界坐标)
public float cellSize = 1f; // 每个格子大小

// 世界坐标 -> 网格坐标
public Vector2Int WorldToCell(Vector3 worldPos)
{
Vector2 local = (Vector2)worldPos - origin;
int x = Mathf.FloorToInt(local.x / cellSize);
int y = Mathf.FloorToInt(local.y / cellSize);
return new Vector2Int(x, y);
}

// 网格坐标 -> 世界坐标(格子中心)
public Vector3 CellToWorld(Vector2Int cell)
{
float worldX = origin.x + (cell.x + 0.5f) * cellSize;
float worldY = origin.y + (cell.y + 0.5f) * cellSize;
return new Vector3(worldX, worldY, 0f);
}
}
  • FloorToInt 让 [0,1) 都属于同一个格子,更符合“瓷砖”的直觉。
  • +0.5f 是把建筑放在格子中心,也可以改成左下角等。

我们分别讲讲这两个方法,首先来看世界坐标 -> 网格坐标:

首先,我们要知道,格子x = n的范围是[n * cellSize, (n + 1) * cellSize)

然后以(1.8, 2.5)为例,他就落在x = 1, y = 2的范围。

再来看网格坐标 -> 世界坐标:

我们最终得到的都是格子的正中心,返回Vector3是因为Unity中世界坐标默认是3D的,返回Vector3更实用;定位到格子中心是因为通常这个格子中心会作为锚点。

3月30日

最近想实现这样一个功能:拖拽卡牌时可以预览我将要放置的预制体。关于这个功能有一个问题值得思考:预览用的Prefab需要单独制作吗?关于这个问题的答案是:如果预览时的外观与实际需要的一样,功能一样或者更少的情况,就可以不用新做一个预览的Prefab

另外,我还学到了一个新的小知识:当我们启用过滤器时,他会自动忽略trigger的检测,要想检测trigger需要像下面这样设置:

1
2
3
4
var filter = new ContactFilter2D();
filter.useLayerMask = true;       // 启用层过滤
filter.layerMask = forbiddenMask; // 设置要检测的层
filter.useTriggers = true;        // 检测 trigger 碰撞体

3月31日

今天我尝试将我的界面和阿然老师负责的第二个界面结合起来,通过阅读他的代码,其中关于垃圾桶的部分,我学到了UGUI中的事件接口

  • IDropHandler:当物体扔进来的时候。打比方是扔垃圾进垃圾桶时触发
  • IEndDragHandler:当手松开时触发。