塔防游戏十分受欢迎,没有什么比看着你的防护消灭邪恶的入侵者更令人满意的了!在这个由两部分组成的教程中,您将运用 Unity 构建一个塔防游戏!
您将学习怎么…
- 制作一波又一波的敌人
- 让他们遵从路点移动
- 制作和晋级塔,让它们将你的敌人削减直至消失
最终,您将具有该类型的框架,能够对其进行扩展!
留意:您需求了解 Unity 基础知识,例如怎么增加游戏资源和组件、了解预制件并了解一些基本的 C#。
塔防的全貌
在本教程中,您将构建一个塔防游戏,其间敌人(小虫子)爬向归于您和您的喽啰的饼干,这些喽啰当然是怪物!您能够在战略要点放置和晋级怪物,以取得一点金币。
玩家必须在虫子吃你的饼干之前杀死它们。每一波敌人都更难被打败。当你在一切浪潮中幸存下来(胜利!)或五个敌人抵达饼干时,游戏完毕。(失败!)
以下是已完结游戏的屏幕截图:
怪物联合起来!保护饼干!
开端
在 Unity 中翻开 TowerDefense-Part1-Starter 项目。
入门项目包括美术和声响资源,以及预构建的动画和一些有用的脚本。这些脚本与塔防游戏没有直接关系,因而此处不再解释。可是,假如您想了解有关创立 Unity 2D 动画的更多信息,请查看此 Unity 2D 教程。
该项目还包括预制件,稍后您将对其进行扩展以创立人物。最终,该项目包括一个场景及其布景和用户界面设置。
翻开_“Scenes”_文件夹中的 GameScene,并将游戏视图的纵横比设置为 4:3,以保证标签与布景正确对齐。您应该在“游戏”视图中看到以下内容:
学分:
- 该项意图艺术来自Vicki Wenderlich的免费艺术包!你能够在gameartguppy上找到更多很棒的图形。
- 很帅的音乐来自BenSound,他有一些很棒的配乐!
- 感谢Michael Jasper的震撼。
入门项目 – 查看!
资源 – 查看!
迈向统治国际的第一步…嗯,我是说你的塔防游戏…功德圆满!
X 标记点:放置
怪物只能在标有_x_的当地粘贴。
要将它们增加到场景中,请将“Images\Objects\Openspot”从“项目浏览器”拖放到“场景_”视图中。现在,职位并不重要。
在“层次结构”中挑选_“Openspot_”后,单击__Inspector_中的“Add Component”,然后挑选“Box Collider 2D”。Unity 在场景视图中显现带有绿线的框磕碰体。您将运用此磕碰体来检测该方位上的鼠标单击。
Unity 会自动检测磕碰体的适宜尺寸。这有多酷?
依照相同的进程,将Audio\Audio Source组件增加到Openspot。将音频源的_音频编排_设置为 tower_place,您能够在“Audio”文件夹中找到该文件夹,然后停用_“Play On Awake_”。
您需求再创立 11 个点。虽然重复一切这些进程很诱人,但 Unity 有一个很好的处理方案:预制件!
将 Openspot 从“层次结构”拖放到“项目浏览器”的_“预制件_”文件夹中。然后,它的称号在层次结构中变为蓝色,以显现它已连接到预制件。像这样:
现在您现已有了预制件,您能够根据需求创立恣意数量的副本。只需将 Openspot 从_项目浏览器中_的_预制件_文件夹拖放到_场景_视图中即可。执行此操作 11 次,即可在场景中一共创立 12 个 Openspot 目标。
现在运用_Inspector_将这 12 个 Openspot 目标的方位设置为以下坐标:
- (X:-5.2, Y:3.5, Z:0)
- (X:-2.2, Y:3.5, Z:0)
- (X:0.8, Y:3.5, Z:0)
- (X:3.8, Y:3.5, Z:0)
- (X:-3.8, Y:0.4, Z:0)
- (X:-0.8, Y:0.4, Z:0)
- (X:2.2, Y:0.4, Z:0)
- (X:5.2, Y:0.4, Z:0)
- (X:-5.2, Y:-3.0, Z:0)
- (X:-2.2, Y:-3.0, Z:0)
- (X:0.8, Y:-3.0, Z:0)
- (X:3.8, Y:-3.0, Z:0)
完结后,场景应如下所示。
放置怪物
为了便于放置,项意图预制件文件夹包括一个_怪物__预制_件。
怪物预制件 – 随时可用
此刻,它由一个空的游戏目标组成,其间包括三个不同的精灵及其子元素的射击动画。
每个精灵代表不同力气水平的怪物。预制件还包括一个_音频源_组件,每逢怪物发射激光时,您都会触发该组件来播放声响。
您现在将创立一个能够将Monster放置在Openspot上的脚本
在“项目浏览器”中,挑选“预制件”文件夹中的_“Openspot_”。在_查看器_中,点按“增加组件”,然后选取_“新建脚本_”并将其命名_为 PlaceMonster_。挑选C Sharp 作为_语言_,然后单击_创立和增加_。由于您已将脚本增加到 Openspot _预制件_中,因而场景中的一切 Openspot 现在也附加了该脚本。整洁!
双击脚本以在 IDE 中将其翻开。然后增加这两个变量:
public GameObject monsterPrefab;
private GameObject monster;
您将实例化存储在其间的目标的副本monsterPrefab
以创立怪物,并将其存储在其间monster
以便您能够在游戏进程中对其进行操作。
每个方位一个怪物
增加以下办法,每个方位只答应一个怪物:
private bool CanPlaceMonster()
{
return monster == null;
}
在CanPlaceMonster()
你查看monster
变量是否仍然null
.假如是的话,说明这儿现在没有怪物,放一个也能够。
现在增加以下代码,以便在玩家单击此游戏目标时实践放置怪物:
void OnMouseUp()
{
if (CanPlaceMonster())
{
monster = (GameObject)
Instantiate(monsterPrefab, transform.position, Quaternion.identity);
AudioSource audioSource = gameObject.GetComponent<AudioSource>();
audioSource.PlayOneShot(audioSource.clip);
}
}
此代码在鼠标单击或点击时放置一个怪物。那么这是怎么实现的呢?
-
OnMouseUp
当玩家点击游戏目标的物理磕碰器时,Unity 会自动调用。 -
CanPlaceMonster()
调用时,假如回来,此办法会放置一个新怪物true
。 - 您运用 来创立怪物
Instantiate
,该办法创立具有指定方位和旋转的给定预制件的实例。在这种状况下,您复制monsterPrefab
,为其供给当时游戏目标的方位且不进行旋转,将结果转换为 aGameObject
并将其存储在monster
. - 最终,您调用
PlayOneShot
播放附加到目标AudioSource
组件的声响作用。
现在你的PlaceMonster
脚本能够放置一个新的怪物,但你仍然必须指定预制件。
运用正确的预制件
保存文件并切换回 Unity。
要分配 monsterPrefab 变量,请先在项目浏览器的_预制件_文件夹中挑选 Openspot。
在__查看器__中,单击 PlaceMonster(脚本)组件的“怪物_预制件”字段右侧的圆圈,然后从出现的对话框中挑选“Monster”。
就是这样。运转场景并经过单击或点击在各种x点上构建怪物。
成功!你能够制作怪物。然而,它们看起来像一个古怪的糊状物,由于你的怪物的一切子精灵都被制作出来了。接下来,您将处理此问题。
晋级那些怪物
鄙人图中,你会看到你的怪物在更高层次上看起来越来越可怕。
太蓬松了!可是假如你企图偷它的饼干,这个怪物可能会变成杀手。
脚本充任为怪物实现晋级体系的基础。它盯梢怪物在每个等级上应该有多强大,当然还有怪物的当时等级。
现在增加此脚本。
在_项目浏览器中_挑选_预制件/怪物_。增加一个名为 MonsterData 的新 C# 脚本。在 IDE 中翻开脚本,并在类_上方_增加以下代码。MonsterData
[System.Serializable]
public class MonsterLevel
{
public int cost;
public GameObject visualization;
}
这将创立 MonsterLevel
.它对本钱(以金币为单位,稍后会支撑)和特定怪物关卡的视觉表明方式进行分组。
[System.Serializable]
您能够在顶部增加以使类的实例可从查看器进行修改。这使您能够快速更改 Level 类中的一切值,即使在游戏运转时也是如此。它关于平衡你的游戏十分有用。
界说怪物等级
在本例中,您将预界说的怪物等级
存储在列表<T>
中。
为什么不简单地运用MonsterLevel[]
?MonsterLevel
那么,您将屡次需求特定目标的索引。虽然为此编写代码并不困难,但您将运用IndexOf()
,它实现了 的功能Lists
。这次无需重复写一个办法。:]
在 MonsterData.cs 的顶部,增加以下语句:using
using System.Collections.Generic;
这使您能够拜访通用数据结构,因而能够在脚本中运用该类。List<T>
留意:泛型是 C# 的重要组成部分。它们答应您界说类型安全的数据结构,而无需提交类型。这关于列表和集合等容器类十分实用。若要了解有关泛型的详细信息,请查看 C# 泛型简介。
现在将以下变量增加到 MonsterData
中以存储 MonsterLevel
列表:
public List<MonsterLevel> levels;
运用泛型,您能够保证只能包括目标。levels``List``MonsterLevel
保存文件并切换到 Unity 以配置每个阶段。
在_项目浏览器中_挑选_预制件/怪物_。在__查看器__中,您现在能够在 MonsterData(脚本)组件中看到“等级”字段。将其_巨细_设置为 3。
接下来,将每个等级的_本钱_设置为以下值:
- 元素 0:200
- 元素 1:110
- 元素 2:120
现在分配可视化字段值。
在项目浏览器中翻开预_制件/怪物_,以便能够看到其子项。将子 Monster0 拖放到_元素 0_ 的_可视化_字段中。
重复此操作,将怪物 1 分配给_元素 1_,将_怪物_ 2 分配给_元素 2_。请参阅以下演示此进程的 GIF:
挑选预制件_/怪物时,预制件_应如下所示:
在查看器中界说怪物的等级。
界说当时等级
在 IDE 中_切换_回 MonsterData.cs并将另一个变量增加到 MonsterData
。
private MonsterLevel currentLevel;
在私有变量中,currentLevel
您将存储……等等……怪物的当时等级。]
现在设置currentLevel
并使其可供其他脚本拜访。将以下内容增加到MonsterData
, 以及实例变量声明:
public MonsterLevel CurrentLevel
{
get
{
return currentLevel;
}
set
{
currentLevel = value;
int currentLevelIndex = levels.IndexOf(currentLevel);
GameObject levelVisualization = levels[currentLevelIndex].visualization;
for (int i = 0; i < levels.Count; i++)
{
if (levelVisualization != null)
{
if (i == currentLevelIndex)
{
levels[i].visualization.SetActive(true);
}
else
{
levels[i].visualization.SetActive(false);
}
}
}
}
}
那里有相当多的 C#脚本,嗯?把一切都看一遍:
- 界说私有_变量的特点
currentLevel
。_界说特点后,您能够像调用任何其他变量相同调用:asCurrentLevel
(从类内部)或 asmonster.CurrentLevel
(从类外部)。您能够在特点的 getter 或 setter 办法中界说自界说行为,而且经过仅供给 getter、setter 或两者,您能够操控特点是只读、只写还是读/写。 - 在 getter 中,回来
currentLevel
的值。 - 在资源库中,将新值分配给currentLevel 。接下来,您将取得当时等级的索引。最终,循环拜访一切_等级_,并将可视化设置为活动或非活动,详细取决于currentLevelIndex .这很棒,由于这意味着每逢有人设置currentLevel 时,子画面都会自动更新。特点肯定会派上用场!
增加以下 实现OnEnable
:
void OnEnable()
{
CurrentLevel = levels[0];
}
这CurrentLevel将在放置时设置,保证它只显现正确的精灵。
留意:在 OnEnable
中初始化特点而不是OnStart
十分重要,由于您能够在实例化预制件时调用 order 办法。
创立预制件时,将当即调用 OnEnable
(假如预制件以启用状态保存),但直到目标开端作为场景的一部分运转后才会调用 OnStart
。
在放置怪物之前,您需求查看此数据,因而您能够在 OnEnable
中对其进行初始化。
保存文件并切换到 Unity。运转项目并放置怪物;现在,它们显现正确和最低等级的精灵。
晋级那些怪物
切换回您的 IDE,并将以下办法增加到 MonsterData
:
public MonsterLevel GetNextLevel()
{
int currentLevelIndex = levels.IndexOf (currentLevel);
int maxLevelIndex = levels.Count - 1;
if (currentLevelIndex < maxLevelIndex)
{
return levels[currentLevelIndex+1];
}
else
{
return null;
}
}
在GetNextLevel
中,您能够取得当时等级的索引和最高等级的
索引,前提是怪物没有抵达最大等级以回来下一个等级。否则,回来 null
。
您能够运用此办法来确定是否能够晋级怪物。
增加以下办法以提高怪物的等级:
public void IncreaseLevel()
{
int currentLevelIndex = levels.IndexOf(currentLevel);
if (currentLevelIndex < levels.Count - 1)
{
CurrentLevel = levels[currentLevelIndex + 1];
}
}
在这儿,您能够取得当时等级的索引,然后经过查看它是否小于 来保证它不是最大等级。假如是这样,请设置为下一个等级。levels.Count - 1``CurrentLevel
测验晋级才能
保存该文件,然后在 IDE 中切换到 PlaceMonster.cs 并增加以下新办法:
private bool CanUpgradeMonster()
{
if (monster != null)
{
MonsterData monsterData = monster.GetComponent<MonsterData>();
MonsterLevel nextLevel = monsterData.GetNextLevel();
if (nextLevel != null)
{
return true;
}
}
return false;
}
首要查看是否有能够晋级的怪物,办法是查看怪物
变量是否为 null
。假如是这种状况,您能够从怪物数据
中获取怪物的当时等级。
然后测验是否有更高等级可用,即 GetNextLevel()
不回来 null
时。假如能够晋级,则回来 true
,否则回来 false
。
启用金牌晋级
要启用晋级选项,请将 else if
分支增加到 OnMouseUp
:
if (CanPlaceMonster())
{
}
else if (CanUpgradeMonster())
{
monster.GetComponent<MonsterData>().IncreaseLevel();
AudioSource audioSource = gameObject.GetComponent<AudioSource>();
audioSource.PlayOneShot(audioSource.clip);
}
查看是否能够运用 CanUpgradeMonster()
进行晋级。假如是,您能够运用 GetComponent()
拜访 MonsterData
组件并调用 IncreaseLevel(),
这将增加怪物的等级。最终,你触发怪物的_音频源_。
保存文件并切换回 Unity。运转游戏,放置和晋级_恣意数量的_怪物…现在。
一切怪物晋级
付出金币 – 游戏管理器
现在,能够当即制作和晋级一切怪物,但这其间的应战在哪里?
让我们深入了解黄金问题。盯梢它的问题在于您需求在不同的游戏目标之间同享信息。
下图显现了一切要参加其间的目标。
杰出显现的游戏目标都需求知道,玩家具有多少金币。
您将运用其他目标可拜访的同享目标来存储此数据。
在_层次结构_中单击鼠标右键,然后挑选“创立空”。将新游戏目标命名_为游戏管理器_。
将一个名为 GameManagerBehavior 的 C# 脚本增加到 GameManager,然后在 IDE 中翻开新脚本。您将在标签中显现玩家的总金币,因而请在文件顶部增加以下行:
using UnityEngine.UI;
这使您能够拜访特定于 UI 的类,例如 ,项目将其用于标签。现在将以下变量增加到类中:Text
public Text goldLabel;
Text
这将存储对用于显现玩家具有多少金币的组件的引证。
既然GameManager
知道了标签,您怎么保证变量中存储的黄金数量与标签上显现的数量同步?您将创立一个特点。
将以下代码增加到GameManagerBehavior
:
private int gold;
public int Gold {
get
{
return gold;
}
set
{
gold = value;
goldLabel.GetComponent<Text>().text = "GOLD: " + gold;
}
}
好像很熟悉?它类似于您在CurrentLevel 中界说的Monster
。首要,创立一个私有变量gold
,来存储当时的黄金总量。然后你界说一个名为Gold
— creative的特点,对吧?– — 并实现一个 getter 和 setter。
getter 只需回来 gold
的值。setter更有趣。除了设置变量的值外,它还将字段text
设置为“翻开”以goldLabel
显现新的黄金数量。
你觉得有多慷慨?增加以下行以Start()
给玩家 1000 金币,假如您感到小气,则分配更少:
Gold = 1000;
将标签目标分配给脚本
保存文件并切换到 Unity。
在Hierarchy中,挑选GameManager。在Inspector 中,单击Gold Label右侧的圆圈。在Select Text对话框中,挑选Scene选项卡并挑选GoldLabel。
运转场景,标签显现_Gold:1000_。
查看玩家的“钱包”
在 IDE 中_.cs翻开 PlaceMonster_,然后增加以下实例变量:
private GameManagerBehavior gameManager;
您将用于gameManager
拜访GameManagerBehavior
场景的GameManager的组件。要分配它,请将以下内容增加到Start()
:
gameManager = GameObject.Find("GameManager").GetComponent<GameManagerBehavior>();
您能够运用 获取名为 GameManager 的游戏目标,该游戏目标回来它找到的第一个具有给定称号的游戏目标。然后,检索其组件并将其存储以供今后运用。GameObject.Find()``GameManagerBehavior
留意:您能够经过在 Unity 修改器中设置字段或向
GameManager
增加静态办法来实现此意图,该办法回来一个单一实例,您能够从中获取游戏管理器行为
。可是,上面的块中有一个黑马办法:,它在运转时速度较慢,但方便且能够慎重运用。
Find
拿钱!
你还没有扣除金币,所以在 OnMouseUp()
中增加_了两次_这一行,替换每个注释 // TODO: 扣除金币:
gameManager.Gold -= monster.GetComponent<MonsterData>().CurrentLevel.cost;
保存文件并切换到 Unity,晋级一些怪物并观看黄金读数更新。现在你扣除金币,但玩家只需有空间就能够制作怪物。
无限才能?棒!但你不能答应这种状况产生。只有当玩家有足够的金币时,才应该放置怪物。
怪物需求金币
在 IDE 中切换到 PlaceMonster.cs,并将 CanPlaceMonster()
的内容替换为以下内容:
int cost = monsterPrefab.GetComponent<MonsterData>().levels[0].cost;
return monster == null && gameManager.Gold >= cost;
levels
从其 中检索放置怪物的本钱MonsterData
。然后你查看它monster
不是null
而且它gameManager.Gold
大于本钱。
CanUpgradeMonster()
应战:增加自己查看玩家是否有足够的金币。替换此行:
return true;
有了这个:
return gameManager.Gold >= nextLevel.cost;
这将查看玩家具有的_金币_是否超过晋级本钱。
在 Unity 中保存并运转场景。来吧,试着放置无限的怪物!
现在你只能制作有限数量的怪物。
塔政治:敌人、波涛和航点
是时分为你的敌人“铺路”了。敌人出现在第一个航点,向下一个航点移动并重复,直到他们抵达你的饼干。
你会让敌人行进:
- 为敌人确定一条路途
- 沿路移动敌人
- 旋转敌人使其向前看
创立带有航点的路途
在_层次结构_中单击鼠标右键,然后挑选创立空以创立新的_空_游戏目标。将其命名为 Road,并保证它位于方位 (0, 0, 0)。
现在,右键单击_层次结构_中的 Road,并创立另一个空游戏目标作为 Road 的子目标。将其命名为_Waypoint0_并将其方位设置为_(-12,2,0)_ – 这是敌人开端攻击的当地。
运用以下称号和方位以相同的方式再创立五个航点:
- 航点 1: (X:7, Y:2, Z:0)
- 航点 2: (X:7, Y:-1, Z:0)
- 航点3: (X:-7.3, Y:-1, Z:0)
- 航点4: (X:-7.3, Y:-4.5, Z:0)
- 航点5: (X:7, Y:-4.5, Z:0)
以下屏幕截图杰出显现了航点方位和生成的途径。
生成敌人
现在要制作一些敌人来跟路。_预制件_文件夹包括_敌人_预制件。它的方位是 _(-20, 0, 0),_因而新实例将在屏幕外生成。
否则,它的设置很像Monster预制件,有一个AudioSource
和一个 childSprite
,而且它是一个精灵,所以你能够稍后旋转它,而无需旋转即将需求的血量条。
### 把怪物移到路上
将一个名为 MoveEnemy 的新 C# 脚本增加到预制件_\Enemy 预制件_。在 IDE 中翻开脚本,然后增加以下变量:
[HideInInspector]
public GameObject[] waypoints;
private int currentWaypoint = 0;
private float lastWaypointSwitchTime;
public float speed = 1.0f;
waypoints
将航点的副本存储在一个数组中,而上述内容可保证您不会意外更改__查看器__中的字段,但您仍然能够从其他脚本拜访它。[HideIn_inspector_]``waypoints
currentWaypoint
盯梢敌人当时正在脱离的路点,lastWaypointSwitchTime
存储敌人经过它的时刻。最终,存储敌人的speed
。
在 Start()
中增加此行:
lastWaypointSwitchTime = Time.time;
这将初始化lastWaypointSwitchTime
为当时时刻。
要使敌人沿着途径移动,请将以下代码增加到 Update()
中:
Vector3 startPosition = waypoints [currentWaypoint].transform.position;
Vector3 endPosition = waypoints [currentWaypoint + 1].transform.position;
float pathLength = Vector3.Distance (startPosition, endPosition);
float totalTimeForPath = pathLength / speed;
float currentTimeOnPath = Time.time - lastWaypointSwitchTime;
gameObject.transform.position = Vector2.Lerp (startPosition, endPosition, currentTimeOnPath / totalTimeForPath);
if (gameObject.transform.position.Equals(endPosition))
{
if (currentWaypoint < waypoints.Length - 2)
{
currentWaypoint++;
lastWaypointSwitchTime = Time.time;
}
else
{
Destroy(gameObject);
AudioSource audioSource = gameObject.GetComponent<AudioSource>();
AudioSource.PlayClipAtPoint(audioSource.clip, transform.position);
}
}
按部就班:
- 从路点数组中,您能够检索当时途径段的开端和完毕方位。
-
用公式时刻=距离/速度计算全程所需时刻,然后确定途径上的当时时刻。运用
Vector2.Lerp
,您能够在段的开端方位和完毕方位之间刺进敌人的当时方位。 - 查看敌人是否现已抵达
endPosition
。假如是,请处理这两种可能的状况:- 敌人还没有在最终一个航路点,所以增加
currentWaypoint
和更新lastWaypointSwitchTime
。稍后,您将增加代码来旋转敌人,使其也指向它移动的方向。 - 敌人抵达了最终一个路点,所以这会摧毁它并触发声响作用。稍后您还将增加代码来削减玩家的
health
。
- 敌人还没有在最终一个航路点,所以增加
保存文件并切换到 Unity。
给敌人一个方向感
在现在的状态下,敌人不知路途标的顺序。
在Hierarchy中挑选Road ,并增加一个名为 SpawnEnemy的新C# 脚本。然后在您的 IDE 中翻开它,并增加以下变量:
public GameObject[] waypoints;
您将运用它waypoints
以正确的顺序存储场景中的路点。
保存文件并切换到 Unity。在_层次结构_中挑选_路途_,并将_航点_数组_的巨细_设置为 6。
将 Road 的每个子项拖到字段中,将 Waypoint0 放入元素 0,将 Waypoint1 放入_元素 1_,依此类推。
现在你有一个数组,其间包括整齐有序的航点,所以有一条途径——请留意,它们永久不会撤退;他们会死在企图得到糖的路上。
查看一切是否正常
前往 IDE 中的 SpawnEnemy,并增加以下变量:
public GameObject testEnemyPrefab;
这会在 testEnemyPrefab
中保留对 Enemy 预制件的引证。
要在脚本启动时创立敌人,请将以下代码增加到 Start():
Instantiate(testEnemyPrefab).GetComponent<MoveEnemy>().waypoints = waypoints;
这将实例化存储在 testEnemy
中的预制件的新副本,并为其分配要遵从的航点。
保存文件并切换到 Unity。在Hierarchy中挑选Road并将其Test Enemy设置为Enemy预制件。
运转项目,看到敌人沿着这条路走。
运转项目以查看敌人沿着路途行进。
你有没有留意到他们并不总是在寻找他们要去的当地?有趣!假如你想在这个项目上更进一步,持续第二部分并完善这个项目,学习怎么令他们体现得更出色。
后续
你现已做了许多作业,而且正在具有自己的塔防游戏。
玩家能够制作怪物,但不是无限数量,而且有一个敌人跑向你的饼干。玩家具有金币,还能够晋级怪物。
在第二部分中,你将介绍生成大量敌人并将它们炸飞。第二部分见
【Unity小游戏】游戏开发案例,轻松打造一款塔防游戏!(下) – 掘金 ()