ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

[Unity] ECS中的重复多态性

2021-07-01 20:59:52  阅读:208  来源: 互联网

标签:多态性 void float readonly Unity ECS typeof query public


英文原文:
https://coffeebraingames.wordpress.com/2019/09/15/replicating-polymorphism-in-ecs/

  多态性是那些难以摆脱的OOP基础之一。它很容易做到,而且非常直观。我们用它来重构代码,使其更有条理。我们用它来管理不同的行为,同时只保持一个单一的接口。我们还用它来制作全面的创作编辑器,通过与反射相辅相成,影响运行时的行为。

  然而,在一个不允许引用类型的环境中,它是无法做到的(Unity的HPC#)。它可以在ECS中以另一种方式进行复制,这就是本帖的内容。

OOP版本

  比方说,我们有一个投射物的框架。我们还可以说,我们的游戏世界是带有魔法的蒸汽朋克。我们希望能够同时支持子弹和魔法弹丸,如火球。我们的OOP代码可能看起来像这样:

public abstract class Projectile {
    // Common projectile properties
    protected Vector2 position;
    private int damage;
 
    // Each projectile type may implement its own movement
    public abstract void Move();
 
    // Each projectile might have different effects on impact
    public abstract void OnImpact();
     
    public int Damage {
        get {
            return this.damage;
        }
    }
}
 
public class Bullet : Projectile {
    private readonly Vector2 direction;
    private readonly float speed;
 
    public Bullet(Vector2 direction, float speed) {
        this.direction = direction.normalized;
        this.speed = speed;
    }
 
    public override void Move() {
        // Move by speed in straight line
        this.position += this.speed * Time.deltaTime * this.direction;
    }
 
    public override void OnImpact() {
        // Maybe just destroy the bullet here
    }
}
 
public class Fireball : Projectile {
    private readonly float initialVelocity;
    private readonly float angle;
    private readonly float gravity;
 
    private readonly float vX;
    private readonly float vYPart;
     
    private float polledTime;
 
    public Fireball(float initialVelocity, float angle, float gravity) {
        this.initialVelocity = initialVelocity;
        this.angle = angle;
        this.gravity = gravity;
         
        // Cache
        this.vX = this.initialVelocity * Mathf.Cos(this.angle);
        this.vYPart = this.initialVelocity * Mathf.Sin(this.angle);
    }
 
    public override void Move() {
        // Move by projectile motion
        // There are better ways to do this but just bare with me
        this.polledTime += Time.deltaTime;
         
        // Update X
        this.position.x += this.vX * Time.deltaTime;
 
        // Update Y
        float vY = this.vYPart - this.gravity * this.polledTime;
        this.position.y += vY * Time.deltaTime;
    }
 
    public override void OnImpact() {
        // Destroy the projectile then send a request to show a fireball impact particle effect
        // at the current position
    }
}

然后我们可以这样实现处理抛射物的类:

public class ProjectileManager {
    private readonly List<Projectile> projectiles = new List<Projectile>();
 
    public void Add(Projectile projectile) {
        this.projectiles.Add(projectile);
    }
 
    public void Update() {
        for (int i = 0; i < this.projectiles.Count; ++i) {
            this.projectiles[i].Move();
        }
         
        CheckForCollisions();
    }
 
    private void CheckForCollisions() {
        // Let's just say a list of collisions exists
        foreach(Collision c in this.collisions) {
            // Apply damage if health component exists
            if(c.Health != null) {
                c.Health.Value -= c.Projectile.Damage;
            }
 
            // Execute custom on impact routines
            c.Projectile.OnImpact();
        }
    }
}

  这种臆造的弹射系统应该很容易理解。有在一定方向上直线运动的子弹和以弹射运动的火球。它们都可以由ProjectileManager来处理,因为它们继承了Projectile基类。

ECS Version

  在Unity的纯ECS中,不能使用类,当然也不能使用继承。但我认为这是一件好事,因为在ECS中需要一种不同的思维方式。在使用ECS对游戏元素进行建模时,我们必须忘记OOP。

  让我们从我们的投射物组件开始:

public struct Projectile : IComponentData {
    public float2 position;
    public readonly int damage;
 
    public Projectile(float2 position, int damage) {
        this.position = position;
        this.damage = damage;
    }
}

  我们对这个组件的意图是,任何拥有这个组件的实体都被认为是一个射弹。这个组件可以被系统用来只过滤具有这种组件的实体,然后执行可以应用于所有射弹的一般或通用逻辑。可以把它看作是基类中的代码。

接下来是代表子类的组件:

public struct Bullet : IComponentData {
    public readonly float2 direction;
    public readonly float speed;
 
    public Bullet(float2 direction, float speed) {
        this.direction = math.normalize(direction);
        this.speed = speed;
    }
}
 
public struct Fireball : IComponentData {
    public readonly float initialVelocity;
    public readonly float angle;
    public readonly float gravity;
 
    public readonly float vX;
    public readonly float vYPart;
     
    public float polledTime;
     
    public Fireball(float initialVelocity, float angle, float gravity) {
        this.initialVelocity = initialVelocity;
        this.angle = angle;
        this.gravity = gravity;
         
        // Cache
        this.vX = this.initialVelocity * math.cos(this.angle);
        this.vYPart = this.initialVelocity * math.sin(this.angle);
 
        this.polledTime = 0;
    }
}

  我们只是把它们的数据移到它们自己的组件中。为了给一个子弹射出物建模,我们创建了一个具有射出物和子弹组件的实体。同样的情况也适用于火球弹。

// Create a bullet
Entity bullet = entityManager.CreateEntity(typeof(Projectile), typeof(Bullet));
 
// Create a fireball
Entity fireball = entityManager.CreateEntity(typeof(Projectile), typeof(Fireball));

  从这里,我们就可以定义不同的运动系统:

// Using ComponentSystem here instead of JobComponentSystem so that it's
// easier to understand
public class BulletMoveSystem : ComponentSystem {
    private EntityQuery query;
 
    protected override void OnCreate() {
        this.query = GetEntityQuery(typeof(Projectile), typeof(Bullet));
    }
 
    protected override void OnUpdate() {
        this.Entities.With(this.query).ForEach(delegate(ref Projectile projectile, ref Bullet bullet) {
            projectile.position = projectile.position + (bullet.speed * Time.deltaTime * bullet.direction);
        });
    }
}
 
public class FireballMoveSystem : ComponentSystem {
    private EntityQuery query;
     
    protected override void OnCreate() {
        this.query = GetEntityQuery(typeof(Projectile), typeof(Fireball));
    }
         
    protected override void OnUpdate() {
        this.Entities.With(this.query).ForEach(delegate(ref Projectile projectile, ref Fireball fireball) {
            // Move by projectile motion
            fireball.polledTime += Time.deltaTime;
 
            float2 newPosition = projectile.position;
            newPosition.x += fireball.vX * Time.deltaTime;
             
            float vY = fireball.vYPart - fireball.gravity * fireball.polledTime;
            newPosition.y += vY * Time.deltaTime;
 
            projectile.position = newPosition;
        });
    }
}

  为了处理不同的 "撞击 "逻辑,一个单独的系统可以处理检查碰撞检测,然后给发生碰撞的实体添加碰撞标签组件。然后,独立的系统将处理伤害处理和 "撞击 "程序。这里是处理碰撞检测的系统:

// Component that is added to entities that have collided
public struct Collided : IComponentData {
    public readonly Entity other; // The other entity that we collided with
}
 
public class ProjectileCollisionDetectionSystem : ComponentSystem {
    private EntityQuery query;
 
    protected override void OnCreate() {
        this.query = GetEntityQuery(typeof(Projectile), ComponentType.Exclude<Collided>());
    }
     
    protected override void OnUpdate() {
        // Adds Collided component to entities that have collided
    }
}

应用伤害的系统可以是这样的:

// Component representing health
public struct Health : IComponentData {
    public int amount;
}
 
public class ProjectileDamageSystem : ComponentSystem {
    private EntityQuery query;
 
    protected override void OnCreate() {
        this.query = GetEntityQuery(typeof(Projectile), typeof(Collided));
    }
 
    protected override void OnUpdate() {
        ComponentDataFromEntity<Health> allHealth = GetComponentDataFromEntity<Health>();
         
        this.Entities.With(this.query).ForEach(delegate(ref Projectile projectile, ref Collided collided) {
            // Apply damage
            Health health = allHealth[collided.other];
            health.amount -= projectile.damage;
            allHealth[collided.other] = health; // Modify
        });
    }
}

  也可以在自己的系统中实现 "受影响 "的程序。

public class BulletOnImpactSystem : ComponentSystem {
    private EntityQuery query;
 
    protected override void OnCreate() {
        this.query = GetEntityQuery(typeof(Projectile), typeof(Collided), typeof(Bullet));
    }
 
    protected override void OnUpdate() {
        // Just destroy them
        this.EntityManager.DestroyEntity(this.query);
    }
}
 
public class FireballOnImpactSystem : ComponentSystem {
    private EntityQuery query;
 
    protected override void OnCreate() {
        this.query = GetEntityQuery(typeof(Projectile), typeof(Collided), typeof(Fireball));
    }
 
    protected override void OnUpdate() {
        this.Entities.With(this.query).ForEach(delegate(ref Projectile projectile) {
            // Request fireball particle effect at projectile.position
        });
         
        // Then destroy them
        this.EntityManager.DestroyEntity(this.query);
    }
}

  在这一点上,我们已经将OOP的逻辑复制到其ECS版本。

更理想的ECS解决方案

  从我们最初的重构中,我们可以对一些组件进行修改,使它们更容易被重用。

  我们可以改进的一个方面是运动。与其使用Bullet组件进行直线运动,为什么不把它定义为自己的组件,如StraightDirectionMovement。这样,我们就可以在游戏中的其他需要这种运动的元素上重复使用它。在这样做之前,我们还需要从Projectile中移除位置属性,并使用一个单独的组件代表它。这就是新的运动系统的模样:

// Holds the projectile's position
public struct Position : IComponentData {
    public float2 value;
}
 
public struct StraightDirectionMovement : IComponentData {
    public readonly float2 direction;
    public readonly float speed;
 
    public StraightDirectionMovement(float2 direction, float speed) {
        this.direction = math.normalize(direction);
        this.speed = speed;
    }
}
 
public class StraightDirectionMovementSystem : ComponentSystem {
    private EntityQuery query;
 
    protected override void OnCreate() {
        this.query = GetEntityQuery(typeof(Position), typeof(StraightDirectionMovement));
    }
 
    protected override void OnUpdate() {
        this.Entities.With(this.query).ForEach(delegate(ref Position position, ref StraightDirectionMovement movement) {
            position.value = position.value + (movement.speed * Time.deltaTime * movement.direction);
        });
    }
}

  以同样的方式,火球的弹射运动也可以变成自己的组件。假设我们把它叫做ProjectileMotionMovement。

public struct ProjectileMotionMovement : IComponentData {
    public readonly float initialVelocity;
    public readonly float angle;
    public readonly float gravity;
 
    public readonly float vX;
    public readonly float vYPart;
 
    public float polledTime;
 
    public ProjectileMotionMovement(float initialVelocity, float angle, float gravity) {
        this.initialVelocity = initialVelocity;
        this.angle = angle;
        this.gravity = gravity;
     
        // Cache
        this.vX = this.initialVelocity * math.cos(this.angle);
        this.vYPart = this.initialVelocity * math.sin(this.angle);
 
        this.polledTime = 0;
    }
}
 
public class ProjectileMotionMovementSystem : ComponentSystem {
    private EntityQuery query;
     
    protected override void OnCreate() {
        this.query = GetEntityQuery(typeof(Position), typeof(ProjectileMotionMovement));
    }
         
    protected override void OnUpdate() {
        this.Entities.With(this.query).ForEach(delegate(ref Position position, ref ProjectileMotionMovement movement) {
            // Move by projectile motion
            movement.polledTime += Time.deltaTime;
 
            float2 newPosition = position.value;
            newPosition.x += movement.vX * Time.deltaTime;
             
            float vY = movement.vYPart - movement.gravity * movement.polledTime;
            newPosition.y += vY * Time.deltaTime;
 
            position.value = newPosition;
        });
    }
}

  我们可以改进的另一个方面是撞击时的程序。与其让BulletOnImpactSystem只对带有Bullet组件的实体起作用,不如让它更容易重复使用。让我们用一个名为DestroyOnCollision的组件来代替:

// A tag component that identifies an entity to be removed on collision
public struct DestroyOnCollision : IComponentData {
}
 
public class DestroyOnCollisionSystem : ComponentSystem {
    private EntityQuery query;
 
    protected override void OnCreate() {
        this.query = GetEntityQuery(typeof(Collided), typeof(DestroyOnCollision));
    }
 
    protected override void OnUpdate() {
        // Just destroy them
        this.EntityManager.DestroyEntity(this.query);
    }
}

  请求像火球撞击那样的粒子效果也可以是它自己的组件和系统。比方说,我们有一个名为RequestParticleEffectOnCollision的组件:

public struct RequestParticleEffectOnCollision : IComponentData {
    // Used to identify what particle effect to deploy
    public readonly int effectId;
 
    public RequestParticleEffectOnCollision(int effectId) {
        this.effectId = effectId;
    }
}
 
[UpdateBefore(typeof(DestroyOnCollisionSystem))]
public class RequestParticleEffectOnCollisionSystem : ComponentSystem {
    private EntityQuery query;
 
    protected override void OnCreate() {
        this.query = GetEntityQuery(typeof(Position), typeof(Collided), typeof(RequestParticleEffectOnCollision));
    }
 
    protected override void OnUpdate() {
        this.Entities.With(this.query).ForEach(delegate(ref Position position, ref RequestParticleEffectOnCollision effectRequest) {
            // Request the particle effect at position.value
        });
         
        // Destruction of the entity will now be handled by DestroyOnCollisionSystem
    }
}

  有了上面的系统,在游戏中为子弹物体建模,现在看起来是这样的:

Entity bullet = entityManager.CreateEntity(typeof(Position), 
typeof(Projectile), 
typeof(StraightDirectionMovement), 
typeof(DestroyOnCollision));

  请注意,我们已经完全删除了 "子弹 "的概念。现在,一颗子弹是由构成其行为的部件组成的。这对火球也是一样的:

Entity fireball = entityManager.CreateEntity(typeof(Position), 
typeof(Projectile), 
typeof(ProjectileMotionMovement), 
typeof(RequestParticleEffectOnCollision), 
typeof(DestroyOnCollision));

最后的想法

  很明显,把OOP变成ECS需要更多的代码。对此,我只能说…它就是它。不幸的是,我们只是用C#结构来模拟ECS。目前还没有这样一种意识到ECS的编程语言(还没有)可以大大减少这些代码。我认为这是一种交易。我得到了高度模块化和高效代码的好处,但却牺牲了冗长的语言。

  老实说,冗长的代码并不是一个沉重的代价。我可以拥有尽可能快的代码,而不需要切换到另一个更复杂的代码,如C++,它本身就很冗长。

标签:多态性,void,float,readonly,Unity,ECS,typeof,query,public
来源: https://blog.csdn.net/u013716859/article/details/118396256

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有