这个 QA 有点乱,但在电子游戏中,简单地...
"...检查到每个字符的距离..."
private void GrenadeTimer()
{
rb.isKinematic = true;
// here is our small explosion...
Gp.explosions.MakeExplosion("explosionA",transform.position);
float radius = splashMeasuredInEnemyHeight *
Gp.markers.GeneralExampleEnemyWidth();
List<Enemy> hits = new List<Enemy>();
foreach(Enemy e in Gp.enemies.all)
{
if (e.gameObject.layer != Grid.layerEnemies) continue;
if ( transform.DistanceTo(e) < radius ) hits.Add(e);
}
hits.SortByDistanceFrom( this.transform );
boss.SequentialHits(hits,damage);
boss.Done(this);
}
很难想象还有什么比这更简单的。
请注意,我们决定一个
radius
以米为单位,比方说“4.2 米”,我们要在其中对敌人造成伤害。 (或者,对它们进行抛光,或者无论如何。)
这东西
Gp.enemies.all
是一个List<Enemy> ...它现在拥有游戏中的所有敌人。简单吧?
如果您实际上没有所有敌人(或玩家,NPC - 任何相关的)的List<> - 你是###ed。重新开始您的学习项目。一旦你有一个经过单元测试的实时列表,请回到这个。
这行代码
Grid.layerEnemies
与 Unity 中的 layer 系统相关。这通常会给新的爱好者带来问题...
实际上,您可以在 Unity 中什么都不做,而不需要对每一件事都使用层系统。
让您开始使用图层超出了本文的范围,因此我们将把它放在一边。如果您愿意,只需在您的学习项目中省略这行代码即可。
接下来。所以 - 我们通过并找到我们想要影响的所有敌人。假设有十五个。
请注意...
代码收集它们在循环中。它们最终出现在“命中”列表中。
无论如何,当你刚刚学习时,你可以简单地在循环中应用 buff/damage/etc:
foreach(Enemy e in Gp.enemies.all)
{
if (e.gameObject.layer != Grid.layerEnemies) continue;
if ( transform.DistanceTo(e) < radius )
e.Slow(3f, 0f);
}
但是,在任何真正的游戏中,您必须首先列出物品清单,然后最常见的是有一个经理(比如说,您的“爆炸经理!”-随便)处理这些命中/增益/损坏/任何东西.
原因是你很少能把所有发生的事情都放在同一个框架中。想象一下当我快速爆炸十五个敌人时的声音/视觉效果。几乎可以肯定你的创意总监/谁会希望他们发生“老鼠屎”你知道吗?一种或另一种方式将远比仅仅“触发它们”复杂得多。 (此外,在性能方面,您可能不得不错开它们 - 显然这可能是一个涉及大量代码库的大问题;甚至不要提及游戏是否联网。)请注意,在给出的实际示例中,它们最终是交错排列,实际上与手榴弹有一定距离,看起来很棒。
(出于好奇,该特定代码已被用于爆炸大约十亿颗手榴弹!)
下一期:
查看您的代码,您只是“GetComponent”。其他对象是“哑”的。实际上,您永远不会这样做。注意这里的示例代码中,有一个实际的c#类Enemy
我将在底部粘贴一些Enemy 以增加风味。
事实上,您几乎总是保留一个“附加到玩家/敌人/等的主要 c# 类”的列表。您通常不会对 GameObject 这样的问题感到困扰。
(如果您确实需要访问 GameObject,请告诉 Destroy 它,您只需 enemy.gameObject。)
所以在这里,由于我们只是检查距离,因此您立即拥有Enemy 类。 (如果你使用物理,你必须“GetComponent”才能进入 Enemy 类;当然你也经常这样做。)
话虽如此 - 请记住 Unity 的组件行为性质:
话虽如此。我的讨论有点滑,有一个“Enemy”类(确实有针对敌人的特定类,例如“Dinosaur”、“KillerRobot”、“AttackParrot”等)。
但请记住,您确实需要在 Unity 中“行为明智”。
真的不应该有一个“AttackParrot”类。真的,应该只有组件 - 行为 - 例如
- 苍蝇
- 扔石头
- 有明亮的颜色
- 承受伤害
- 激光眼球
- LandOnTrees
从概念上讲,“AttackParrot”只是一个游戏对象,它恰好具有所有这六种行为。相反,它不会说“BreathesFire”和“CanHyperjump”。
这一切都在这里详细讨论:
https://stackoverflow.com/a/37243035/294884
说“哦,不应该有 'Enemy' 类,只有行为”有点“纯粹主义”——但要牢记。
接下来,
您必须在 Unity 游戏中拥有“通用”组件,这些组件在任何地方都可以访问。诸如音效、配乐等。
Unity 只是忘记了这样做(他们将来会添加它)。
幸运的是,这非常容易做到。注意上面有一个“boss”通用组件和一个“soundEffects”通用组件被调用。
在您项目中任何需要使用通用“boss”组件或通用“声音”组件的脚本中,它只是...
Boss boss = Object.FindObjectOfType<Boss>();
Sound sound = Object.FindObjectOfType<Sound>();
这就是它的全部内容......
老板老板 = Object.FindObjectOfType();
这已经解释了很多次了,我们只需要链接到它:
https://stackoverflow.com/a/35891919/294884
请注意,如果您愿意,使用 PhysX 的另一种方法是:
如果您想使用内置物理:Physics2D.CircleCastNonAlloc
如果你愿意,花几天时间来掌握它。
请注意,这里的示例是针对 2D 游戏的,在 3D 中是相同的。
(当您在 3D 中测量“距离”时,如果您的游戏只发生在平面上,您可能只想费心测量这两个轴上的距离 - 但老实说,这无关紧要。)
你可能会问,SortByDistanceFrom是什么?
SortByDistanceFrom 实际上 ........ 按距离排序
为了节省您的输入,这里是扩展名:
public static void SortByDistanceFrom<T>(
this List<T> things, Transform t) where T:Component
{
Vector3 p = t.position;
things.Sort(delegate(T a, T b)
{
return Vector2.Distance(p, a.transform.position)
.CompareTo(Vector2.Distance(p, b.transform.position));
});
}
这给新爱好者带来了另一个问题。
示例敌人类
示例 - 上面提到的 Enemy 类...包括添加背景。
因此,所有实际的敌人组件(恐龙、袋熊、XFighters 等)都将源自这个组件,并酌情覆盖(如运动等概念)。
using UnityEngine;
using System.Collections;
public class Enemy:BaseFrite
{
public tk2dSpriteAnimator animMain;
public string usualAnimName;
[System.NonSerialized] public Enemies boss;
[Header("For this particular enemy class...")]
public float typeSpeedFactor;
public int typeStrength;
public int value;
// could be changed at any time during existence of an item
[System.NonSerialized] public FourLimits offscreen; // must be set by our boss
[System.NonSerialized] public int hitCount; // that's ATOMIC through all integers
[System.NonSerialized] public int strength; // just as atomic!
[System.NonSerialized] public float beginsOnRight;
private bool inPlay; // ie, not still in runup
void Awake()
{
boss = Gp.enemies;
}
void Start()
{
}
public void ChangeClipTo(string clipName)
{
if (animMain == null)
{
return;
}
animMain.StopAndResetFrame();
animMain.Play(clipName);
}
public virtual void ResetAndBegin() // call from the boss, to kick-off sprite
{
hitCount = 0;
strength = typeStrength;
beginsOnRight = Gp.markers.HitsBeginOnRight();
Prepare();
Gp.run.runLevel.EnemyAvailable();
}
protected virtual void Prepare() // write it for this type of sprite
{
ChangeClipTo(bn);
// so, for the most basic enemy, you just do that
// for other enemy, that will be custom (example, swap damage sprites, etc)
}
void OnTriggerEnter2D(Collider2D c)
{
GameObject cgo = c.gameObject;
// huge amount of code like this .......
if (cgo.layer == Grid.layerPeeps) // we ran in to a "Peep"
{
Projectile p = c.GetComponent<Projectile>();
if (p == null)
{
Debug.Log("WOE!!! " +cgo.name);
return;
}
int damageNow = p.damage;
Hit(damageNow);
return;
}
}
public void _stepHit()
{
if ( transform.position.x > beginsOnRight ) return;
++hitCount;
--strength;
ChangeAnimationsBasedOnHitCountIncrease();
// derived classes write that one.
// todo, actually should the next passage only be after all the steps?
// is after all value is deducted? (just as with the _bashSound)...
if (strength==0) // enemy done for!
{
Gp.coins.CreateCoinBunch(value, transform.position);
FinalEffect();
if ( Gp.skillsTest.on )
{
Gp.skillsTest.EnemyGottedInSkillsTest(gameObject);
boss.Done(this);
return;
}
Grid.pops.GotEnemy(Gp.run.RunDistance); // basically re meters/achvmts
EnemyDestroyedTypeSpecificStatsEtc(); // basically re achvments
Gp.run.runLevel.EnemyGotted(); // basically run/level stats
boss.Done(this); // basically removes it
}
}
protected virtual void EnemyDestroyedTypeSpecificStatsEtc()
{
// you would use this in derives, to mark/etc class specifics
// most typically to alert achievements system if the enemy type needs to.
}
private void _bashSound()
{
if (Gp.bil.ExplodishWeapon)
Grid.sfx.Play("Hit_Enemy_Explosive_A", "Hit_Enemy_Explosive_B");
else
Grid.sfx.Play("Hit_Enemy_Non_Explosive_A", "Hit_Enemy_Non_Explosive_B");
}
public void Hit(int n) // note that hitCount is atomic - hence strength, too
{
for (int i=1; i<=n; ++i) _stepHit();
if (strength > 0) // bil hit the enemy, but enemy is still going.
_bashSound();
}
protected virtual void ChangeAnimationsBasedOnHitCountIncrease()
{
// you may prefer to look at either "strength" or "hitCount"
}
protected virtual void FinalEffect()
{
// so, for most derived it is this standard explosion...
Gp.explosions.MakeExplosion("explosionC", transform.position);
}
public void Update()
{
if (!holdMovement) Movement();
// note don't forget Translate is in Space.Self,
// so you are already heading transform.right - cool.
if (offscreen.Outside(transform))
{
if (inPlay)
{
boss.Done(this);
return;
}
}
else
{
inPlay = true;
}
}
protected virtual void Movement() // override for parabolas, etc etc
{
transform.Translate( -Time.deltaTime * mpsNow * typeSpeedFactor, 0f, 0f, Space.Self );
}
}
所以这是一个普通的敌人类别。然后你有派生,例如 Ufo、Dinosaur、Tank、XWingFighter 等等。这里是 Ufo ...
请注意,它覆盖很多东西。它似乎覆盖了“准备”(cmets 建议它“开始更高”,您可以看到它覆盖了其他东西。
using UnityEngine;
using System.Collections;
public class Ufo:Enemy
{
public Transform projectilePosition;
protected override void Prepare()
{
// ufo always start up high (and then zip up and down)
transform.ForceY(Gp.markers.StartHeightHighArea());
animMain.StopAndResetFrame();
animMain.Play(bn + "A");
animMain.StopAndResetFrame();
Invoke("ZipDown", Random.Range(0.6f,0.8f));
}
protected override void OnGamePause()
{
CancelInvoke();
StopAllCoroutines();
}
protected override void OnGameUnpause()
{
Attack();
if(transform.position.y<0f)
ZipUp();
else
ZipDown();
}
private float fZip = 3.3f;
private void ZipDown() { StartCoroutine(_zipdown()); }
private void ZipUp() { StartCoroutine(_zipup()); }
private IEnumerator _zipdown()
{
Grid.sfx.Play("Enemy_UFO_Move_Down");
float tLow = Gp.markers.StartHeightLowArea();
while (transform.position.y > tLow)
{
transform.Translate(0f,
fZip * -Time.deltaTime * mpsNow, 0f,Space.Self );
yield return null;
}
Attack();
Invoke("ZipUp", Random.Range(0.7f,1.4f));
}
private IEnumerator _zipup()
{
Grid.sfx.Play("Enemy_UFO_Move_Up");
float tHigh = Gp.markers.StartHeightHighArea();
while (transform.position.y < tHigh)
{
transform.Translate(0f,
fZip * Time.deltaTime * mpsNow, 0f,Space.Self );
yield return null;
}
Attack();
Invoke("ZipDown", Random.Range(0.7f,1.4f));
}
private void Attack()
{
Grid.sfx.Play("Enemy_UFO_Shoot");
animMain.Play();
Invoke("_syncShoot", .1f);
}
private void _syncShoot()
{
Gp.eeps.MakeEepUfo(projectilePosition.position);
}
protected override void ChangeAnimationsBasedOnHitCountIncrease()
{
// ufo just goes 4,2,out
if (strength == 2)
{
// if any attack, cancel it
CancelInvoke("ShootGreenPea");
CancelInvoke("Attack");
// on the ufo, anim only plays with attack
animMain.StopAndResetFrame();
animMain.Play(bn + "B");
animMain.StopAndResetFrame();
Invoke("Attack", 1.5f.Jiggle());
}
}
protected override void EnemyDestroyedTypeSpecificStatsEtc()
{
Grid.pops.AddToEnemyCount("ufo");
}
}
让我们考虑一下“在 Enemy 类中覆盖”的想法。
在示例 Enemy 类中覆盖......
许多敌人都有不同类型的移动,对吧?游戏中的一般范例是物体在 2D 中运动学地移动(即,我们“每帧将它们移动一定距离”——这里不使用 PhysX)。所以不同的敌人以完全不同的方式行动。
这是一个以某种方式移动的......(cmets解释它)
protected override void Movement()
{
// it enters at about 2x normal speed
// the slow crossing of the stage is then about 1/2 normal speed
float mpsUse = transform.position.x < changeoverX ? mpsNow*.5f : mpsNow * 2.5f;
transform.Translate( -Time.deltaTime * mpsUse * typeSpeedFactor, 0f, 0f, Space.Self );
// recall that mpsNow was set by enemies when this was created, indeed
// nu.mpsNow = ordinaryMps * widthSpeedFactor;
}
这是一个持续的过程,但有时会“向下漂移......”
protected override void Movement()
{
float mm = mpsNow * typeSpeedFactor;
if ( fallingMotion )
transform.Translate(
-Time.deltaTime * mm,
-Time.deltaTime * mm * fallingness, 0f,
Space.Self );
else
transform.Translate(
-Time.deltaTime * mm, 0f, 0f,
Space.Self );
}
这是一个似乎遵循窦...
protected override void Movement()
{
transform.Translate( -Time.deltaTime * mpsNow * typeSpeedFactor, 0f, 0f, Space.Self );
float y = Mathf.Sin( basis-transform.position.x * (2f/length) );
y *= height;
transform.transform.ForceY( y );
}
这是一个复杂的速度变化,放大
protected override void Movement()
{
// it enters at about 2x normal speed
// it appears to then slow crossing of the stage about 1/2 normal speed
// however it then zooms to about 3x normal speed
float mpsUse = mpsNow;
float angled = 0f;
if ( transform.position.x > changeoverX) //enter quickly
mpsUse = mpsNow * 3f;
if ( transform.position.x < thenAngled) // for bead, angled section
{
mpsUse = mpsNow * 1.5f;
angled = leanVariation;
}
transform.Translate(
-Time.deltaTime * mpsUse * typeSpeedFactor,
-Time.deltaTime * mpsUse * typeSpeedFactor * angled,
0f, Space.Self );
}
你可以做任何运动——飞行、跑步、弹跳等等。
这一切都在 c# 中由 protected override 概念处理。
静态“全局”类...比通用组件更简单,用于简单地“保存”某些变量
这是一个静态类的简单示例,它包含您不妨称之为“全局”的东西,在游戏工程环境中,将某些东西作为“全局”是明智的。
using UnityEngine;
using Shex;
using System.Collections;
using System.Collections.Generic;
static class Gp
{
public static Enemies enemies;
public static Pickups pickups;
public static Coins coins;
public static Peeps peeps;
public static Eeps eeps;
}
因此,TBC 复杂的“通用”系统,如上述的 SoundEffects、Boss、Scores、AI、Networking、Social、InAppPurchase 等,确实会通过解释的“预加载”类型对象。 (即,您在需要它们的任何脚本、任何场景等的顶部使用Boss boss = Object.FindObjectOfType();。)
但是对于只需要在任何地方访问的简单变量、事物,您可以使用像这样的普通静态类。通常只有一个静态类(称为“Gameplay”或“Gp”)完成整个项目的工作。
{无论如何,有些团队会说“去他妈的,不要使用静态类,把它放在像 Boss 这样的“通用”(“preload-style”)组件中......”}
请注意,静态类当然不是真正的 MonoBehavior - 你 “实际上不能在 Unity 中“做”任何事情” 。它仅适用于您想在任何地方轻松访问的“保存变量”(最常见的是列表)。
再一次,请务必记住,静态类根本不是 Unity 游戏对象或组件 - 因此它确实是 不是游戏的一部分;在静态类中,您实际上无法“做”任何事情。要“做”任何事情,在 Unity 中,它必须是一个实际的组件,字面意思是在特定游戏对象上的某个位置。
因此,例如,尝试将“分数”保存在一个简单的静态类中是完全没用的。不可避免地,关于“分数”,你会想要做各种各样的事情(改变屏幕上的显示,奖励积分,保存加密的偏好,触发级别......无论如何,有很多事情要做)。你绝对不能在静态中做到这一点——你根本不能在静态中“做”任何事情——它必须是一个实际的 Unity 游戏对象。 (即,使用“预加载系统”。)静态再次只是为了从字面上跟踪一些基本的“全局”变量,通常是事物列表。 (诸如“屏幕标记”之类的东西就是很好的例子。)
顺便说一句,在游戏开发中,“eep”是敌人射弹,“peep”是玩家射弹,嘿!