架构设计之从OOP到ECS架构演进

目录

背景规则

OOP实现:

分析OOP代码的设计缺陷

Entity-Component-System(ECS)架构

ECS介绍

ECS架构分析

ECS架构改造

背景规则

现在公司用户中心提出一个需求,需要根据用户的会员等级实行不同的程度的打折,会员等级越高打折力度越大。其具体规则如下:

  • 青铜会员,折扣是9.9折;
  • 黄金会员,折扣是8.8折;
  • 铂金会员,折扣是6.6折;
  • 钻石会员,折扣是5折。

OOP实现:

对于熟悉Object-Oriented Programming的同学,一个比较简单的实现是通过类的继承关系(此处省略部分非核心代码):

public abstract class Member {
	public abstract double discount(double sourcePrice);
}

public class BronzeMember extends Member {
	@Override
	public double discount(double sourcePrice) {
		return sourcePrice * 9.9 / 10;
	}
}

public class SilverMember extends Member {
	@Override
	public double discount(double sourcePrice) {
		return sourcePrice * 8.8 / 10;
	}
}

public class GoldMember extends Member {
	@Override
	public double discount(double sourcePrice) {
		return sourcePrice * 6.6 / 10;
	}
}

public class DiamondMember extends Member {
	@Override
	public double discount(double sourcePrice) {
		return sourcePrice * 5 / 10;
	}
}

然后是简单的单元测试:

	public void discountTest() {
		Member bronzeMember = new BronzeMember();
		Member silverMember = new SilverMember();
		Member goldMember = new GoldMember();
		Member diamondMember = new DiamondMember();
		double sourcePrice = 10D;
		Assert.assertEquals (bronzeMember.discount(sourcePrice), 9.9, 0);
		Assert.assertEquals (silverMember.discount(sourcePrice), 8.8, 0);
		Assert.assertEquals (goldMember.discount(sourcePrice), 6.6, 0);
		Assert.assertEquals (diamondMember.discount(sourcePrice), 5, 0);
	}

上述代码比较简单,就不做过多的阐述了。

分析OOP代码的设计缺陷

知识级和操作级对象混用

知识级对象定义了对操作级对象的合法配置,根据相应的规则进行不同的约束;

操作级对象定义了模型常变化的部分。

如上述的例子中,抽取知识级公共对象rule作为规则父类,具体规则:

  • 折扣规则,抽取为DiscountRule;
  • 积分规则,抽取为PointRule;

构建模型的规则:抽离可变因素,减少模型的变动,尽量将变化集中在更少的地方。

对象继承导致代码强依赖父类逻辑,违反开闭原则Open-Closed Principle(OCP)

开闭原则(OCP)规定“对象应该对于扩展开放,对于修改封闭“,继承虽然可以通过子类扩展新的行为,但因为子类可能直接依赖父类的实现,导致一个变更可能会影响所有对象。在这个例子里,我们需要给我们的会员增加一个积分的属性,根据不同的等级制度,在不同等级的人在购买商品之后赠送额外的积分。

然后我们需要修改代码,包括:

  • Member中添加积分(point)属性;

  • 然后修改所有会员实现类,在实现中添加赠送积分的方法;
public class BronzeMember extends Member {
	@Override
	public double discount(double sourcePrice) {
		return sourcePrice * 9.9 / 10;
	}

	@Override
	public int addPoint() {
		return this.point + 10;
	}
}

在一个复杂的软件中为什么会建议“尽量”不要违背OCP?最核心的原因就是一个现有逻辑的变更可能会影响一些原有的代码,导致一些无法预见的影响。这个风险只能通过完整的单元测试覆盖来保障,但在实际开发中很难保障单测的覆盖率。OCP的原则能尽可能的规避这种风险,当新的行为只能通过新的字段/方法来实现时,老代码的行为自然不会变。

继承虽然能Open for extension,但很难做到Closed for modification。所以今天解决OCP的主要方法是通过Composition-over-inheritance,即通过组合来做到扩展性,而不是通过继承。

多对象行为类似,导致代码重复

当我们有不同的对象,但又有相同或类似的行为时,OOP会不可避免的导致代码的重复。就像上面例子中,我们每增加一个统一的行为,都需要在所有继承了Member类的实现类中重写这个方法,导致每个类都需要去改动。

问题总结

在这个案例里虽然从直觉来看OOP的逻辑很简单,但如果你的业务比较复杂,未来会有大量的业务规则变更时,简单的OOP代码会在后期变成复杂的一团浆糊,逻辑分散在各地,缺少全局视角,各种规则的叠加会触发bug。有没有感觉似曾相识?对的,电商体系里的优惠、交易等链路经常会碰到类似的坑。而这类问题的核心本质在于:

  • 业务规则的归属到底是对象的“行为”还是独立的”规则对象“?
  • 业务规则之间的关系如何处理?
  • 通用“行为”应该如何复用和维护?

Entity-Component-System(ECS)架构

ECS介绍

ECS架构模式是其实是一个很老的游戏架构设计,最早应该能追溯到《地牢围攻》的组件化设计,但最近因为Unity的加入而开始变得流行(比如《守望先锋》就是用的ECS)。要很快的理解ECS架构的价值,我们需要理解一个游戏代码的核心问题:

  • 性能:游戏必须要实现一个高的渲染率(60FPS),也就是说整个游戏世界需要在1/60s(大概16ms)内完整更新一次(包括物理引擎、游戏状态、渲染、AI等)。而在一个游戏中,通常有大量的(万级、十万级)游戏对象需要更新状态,除了渲染可以依赖GPU之外,其他的逻辑都需要由CPU完成,甚至绝大部分只能由单线程完成,导致绝大部分时间复杂场景下CPU(主要是内存到CPU的带宽)会成为瓶颈。在CPU单核速度几乎不再增加的时代,如何能让CPU处理的效率提升,是提升游戏性能的核心。
  • 代码组织:如同第一章讲的案例一样,当我们用传统OOP的模式进行游戏开发时,很容易就会陷入代码组织上的问题,最终导致代码难以阅读,维护和优化。
  • 可扩展性:这个跟上一条类似,但更多的是游戏的特性导致:需要快速更新,加入新的元素。一个游戏的架构需要能通过低代码、甚至0代码的方式增加游戏元素,从而通过快速更新而留住用户。如果每次变更都需要开发新的代码,测试,然后让用户重新下载客户端,可想而知这种游戏很难在现在的竞争环境下活下来。

而ECS架构能很好的解决上面的几个问题,ECS架构主要分为:

  • Entity:用来代表任何一个游戏对象,但是在ECS里一个Entity最重要的仅仅是他的EntityID,一个Entity里包含多个Component
  • Component:是真正的数据,ECS架构把一个个的实体对象拆分为更加细化的组件,比如位置、素材、状态等,也就是说一个Entity实际上只是一个Bag of Components。
  • System(或者ComponentSystem,组件系统):是真正的行为,一个游戏里可以有很多个不同的组件系统,每个组件系统都只负责一件事,可以依次处理大量的相同组件,而不需要去理解具体的Entity。所以一个ComponentSystem理论上可以有更加高效的组件处理效率,甚至可以实现并行处理,从而提升CPU利用率。

ECS的一些核心性能优化包括将同类型组件放在同一个Array中,然后Entity仅保留到各自组件的pointer,这样能更好的利用CPU的缓存,减少数据的加载成本,以及SIMD的优化等。

ECS架构分析

组件化

在软件系统里,我们通常将复杂的大系统拆分为独立的组件,来降低复杂度。比如网页里通过前端组件化降低重复开发成本,微服务架构通过服务和数据库的拆分降低服务复杂度和系统影响面等。但是ECS架构把这个走到了极致,即每个对象内部都实现了组件化。通过将一个游戏对象的数据和行为拆分为多个组件和组件系统,能实现组件的高度复用性,降低重复开发成本。

行为抽离

这个在游戏系统里有个比较明显的优势。如果按照OOP的方式,一个游戏对象里可能会包括移动代码、战斗代码、渲染代码、AI代码等,如果都放在一个类里会很长,且很难去维护。通过将通用逻辑抽离出来为单独的System类,可以明显提升代码的可读性。另一个好处则是抽离了一些和对象代码无关的依赖,比如上文的delta,这个delta如果是放在Entity的update方法,则需要作为入参注入,而放在System里则可以统一管理。在第一章的有个问题,到底是应该Player.attack(monster) 还是 Monster.receiveDamage(Weapon, Player)。在ECS里这个问题就变的很简单,放在CombatSystem里就可以了。

数据驱动

即一个对象的行为不是写死的而是通过其参数决定,通过参数的动态修改,就可以快速改变一个对象的具体行为。在ECS的游戏架构里,通过给Entity注册相应的Component,以及改变Component的具体参数的组合,就可以改变一个对象的行为和玩法,比如创建一个水壶+爆炸属性就变成了“爆炸水壶”、给一个自行车加上风魔法就变成了飞车等。在有些Rougelike游戏中,可能有超过1万件不同类型、不同功能的物品,如果这些不同功能的物品都去单独写代码,可能永远都写不完,但是通过数据驱动+组件化架构,所有物品的配置最终就是一张表,修改也极其简单。这个也是组合胜于继承原则的一次体现。

ECS架构改造

定义规则

public class Rule {
	private String name;
}

public class DiscountRule extends Rule {
	private double discount;

	public double getDiscount() {
		return discount;
	}
}

定义entity

public class Member {
	private Map<String, Handler> components = new HashMap<>(16); //存储实体规则对应的具体处理器
	private MemberId memberId;

	private class MemberId {

	}

	public Map<String, Handler> getComponents() {
		return components;
	}
}

处理器

public interface Handler {
	double discountHandle(double sourcePrice, Rule rule);
}


//折扣处理器
public class DiscountHandler implements Handler {
	@Override
	public double discountHandle(double sourcePrice, Rule rule) {
		if (rule instanceof DiscountRule) {
			return sourcePrice * ((DiscountRule) rule).getDiscount();
		}
		return sourcePrice;
	}
}
     

装配规则器

public class Assember {
	public List<Handler> matchHandler(Rule rule, Map<String, Handler> handlerMap) {
		List<Handler> handlers = new ArrayList<>();
		rule.getRuleKeys().forEach(e -> {
			if (handlerMap.get(e) != null) {
				handlers.add(handlerMap.get(e));
			}
		});
		return handlers;
	}
}

打折系统

public class DiscountSystem {
	List<Handler> handlers;

	public double discount(double sourcePrice, Rule rule) {
		double targetPrice = sourcePrice;

		for (Handler h : handlers) {
			targetPrice = h.discountHandle(targetPrice, rule);
		}
		return targetPrice;
	}
}

采用这种方式的架构,我们需要增加积分赠送的需求,现在就只需要增加积分规则和增加积分处理的类就行了。无需改动原来的类,将模型可变都集中在一起,最小化模型的可变部分

  • 5
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

知始行末

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值