这个 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>
……它现在拥有游戏中的所有敌人。简单吧?
如果您实际上没有List<>
所有敌人(或玩家,NPC - 任何相关的) - 你是###ed。重新开始您的学习项目。一旦你有一个经过单元测试的实时列表,请回到这个。
这行代码
Grid.layerEnemies
与Unity中的图层系统有关。这通常会给新的爱好者带来问题......
实际上,如果不为每一件事都使用图层系统,您在 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);
}
然而,在任何真正的游戏中,您必须首先列出物品清单,然后最常见的情况是让一个经理(比如说,您的“爆炸经理!” - 随便)处理这些命中/增益/损坏/其他任何东西。
原因是你很少能把发生的事情都放在同一个框架中。想象一下当我快速爆炸十五个敌人时的声音/视觉效果。几乎可以肯定你的创意总监/谁会希望他们发生“老鼠屎”你知道吗?一种或另一种方式将远比仅仅“触发它们”复杂得多。(此外,在性能方面,您可能不得不错开它们 - 显然这可能是一个涉及大量代码库的大问题;甚至不要提及游戏是否联网。)请注意,在给出的实际示例中,它们最终是交错,实际上是从手榴弹向外的距离,看起来很棒。
(出于好奇,该特定代码已被用于爆炸10 亿颗手榴弹!)
下一期:
查看您的代码,您只需“GetComponent”。其他对象是“哑”的。实际上,您永远不会这样做。请注意,在此处的示例代码中,有一个实际的 c# 类Enemy
我会在Enemy
底部粘贴一些来增加味道。
实际上,您几乎总是保留“附加到玩家/敌人/等的主要 c# 类”的列表。您通常不会对 GameObject 这样的问题感到困扰。
(如果您确实需要到达 GameObject,请告诉Destroy
它,您只需enemy.gameObject
。)
所以在这里,因为我们只是检查距离,你马上就可以Enemy
上课了。(如果你使用物理,你必须“GetComponent”才能进入 Enemy 类;当然你也经常这样做。)
话虽如此 - 请记住 Unity 的组件行为性质:
话虽如此。我的讨论有点滑,有一个“Enemy”类(确实有针对敌人的特定类,例如“Dinosaur”、“KillerRobot”、“AttackParrot”等)。
不过请记住,您确实需要在 Unity 中进行“行为明智”。
真的不应该有一个“AttackParrot”类。真的,应该只有组件 - 行为 - 例如
- 苍蝇
- 扔石头
- 有明亮的颜色
- 受到伤害
- 激光眼球
- 陆地上的树
从概念上讲,“AttackParrot”只是一个游戏对象,它恰好具有所有这六个行为。相反,它不会说“BreathesFire”和“CanHyperjump”。
这一切都在这里详细讨论:
https://stackoverflow.com/a/37243035/294884
说“哦,不应该有‘敌人’类,只有行为”有点“纯粹主义”——但要记住一些事情。
接下来,
您必须在 Unity 游戏中拥有“通用”组件,这些组件在任何地方都可以访问。诸如音效、评分等。
Unity只是忘记了这样做(他们将来会添加它)。
幸运的是,这非常容易做到。请注意,上面有一个“boss”通用组件和一个“soundEffects”通用组件被调用。
在项目中需要使用通用“boss”组件或通用“声音”组件的任何脚本中,它只是...
Boss boss = Object.FindObjectOfType<Boss>();
Sound sound = Object.FindObjectOfType<Sound>();
这里的所有都是它的...
Boss boss = 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 ...
请注意,它会覆盖许多内容。它似乎覆盖了“准备”(评论表明它“开始更高”,你可以看到它覆盖了其他东西。
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)。所以不同的敌人以完全不同的方式行动。
这是一个以某种方式移动的……(评论解释了它)
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 );
}
你可以做任何运动——飞行、跑步、弹跳等等。
这一切都是由protected override
概念在c#中处理的。
静态“全局”类...比通用组件更简单,用于简单地“保存”某些变量
这是一个静态类的简单示例,它包含您不妨称之为“全局”的东西,在游戏工程环境中,将某些东西作为“全局”是明智的。
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 这样的“通用”(“预加载样式”)组件中......”}
请注意,静态类当然不是真正的 MonoBehavior - 您“实际上不能在 Unity 中“做”任何事情”。它仅适用于您想在任何地方轻松访问的“保存变量”(最常见的是列表)。
同样,请务必记住,静态类根本不是 Unity 游戏对象或组件——因此它实际上不是游戏的一部分;您实际上不能在静态类中“做”任何事情。要“做”任何事情,在 Unity 中,它必须是一个实际的组件,字面意思是在特定游戏对象上的某个位置。
因此,例如,尝试将您的“分数”保存在一个简单的静态类中是完全没有用的。不可避免地,关于“分数”,你会想要做各种各样的事情(改变屏幕上的显示,奖励积分,保存加密的偏好,触发级别......无论如何,有很多事情要做)。你绝对不能在静态中做到这一点——你根本不能在静态中“做”任何事情——它必须是一个实际的 Unity 游戏对象。(即,使用“预加载系统”。)静态再次只是为了从字面上跟踪一些基本的“全局”变量,通常是事物列表。(诸如“屏幕标记”之类的东西就是一个很好的例子。)
顺便说一句,在游戏开发中,“eep”是敌人的射弹,“peep”是玩家的射弹,呵呵!