问题描述
实验开发中,我们经常要用到将两个器材(或者器材的部件)绑定到一起,并且能够解绑。比如电学中,导线的端点绑定到接线柱;家庭电路中,插头插到插座上;光学中,透镜放到光具座上;热学或者化学中,瓶盖绑定瓶子,玻璃导管放到试管中,等等。
方案1
不考虑通用模型,每一组需要绑定关系的器材单独维护一套数据。
比如对于导线与接线柱的绑定,导线的一端同一时刻只能绑定一个器材,器材的接线柱可以绑定多个导线,所以,器材的接线柱应该创建一个数组,来存储绑定的导线端点,导线的端点应该创建一个变量,来存储绑定的接线柱。
实验开发初期,我们确实是这么做的。
缺点
只适用于这一种情况,当开发其他学科,其他模型的时候,又要创建一组变量,重新写一遍逻辑。
方案2
使用力学引擎中的关节(joint)。
在需要使用力学引擎的实验中,确实是用的关节,也很方便。使用关节连接之后,还有力学特性。
缺点
在不需要力学引擎的地方,为了实现绑定关系,引入一个力学引擎,太重了。即便是在使用了力学引擎的实验中,如果不需要力学特性,使用关节,也人为的把问题复杂化了。
方案3
卡槽模型。
结合以上两种方案,以及实际需求,我们自己创建一个抽象的模型。
Slot(槽):一个槽可以插入一个或多个卡。
Card(卡):一个卡只能插入到一个槽。
卡和槽的型号(分组,其实用掩码更合适)要匹配才能插上去。
卡插入槽之后,跟随槽移动。
实现代码
基类。
export class AssembleBase { public assembleData: IAssembleAble; public disabled: boolean = false; public group: number = -1; protected engine: AssembleEngine; constructor(engine: AssembleEngine, assembleData: IAssembleAble){ this.assembleData = assembleData; this.engine = engine; } public destroy(): void{ this.assembleData = null; this.engine = null; } public canAdd(assemble: AssembleBase): boolean { return true; } } export interface IAssembleAble{ }
卡。
export class Card extends AssembleBase{ public slot: Slot; public userData: ICardUserData = {}; constructor(engine: AssembleEngine, assembleData: IAssembleAble, group: number = -1){ super(engine, assembleData); this.group = group; this.engine.addCard(this); } public destroy(): void{ this.free(); this.slot = null; this.engine.removeCard(this); this.userData = null; super.destroy(); } public isFree(): boolean{ return !this.slot; } public free(): void{ if (this.slot) { this.slot.removeCard(this); } } public canAdd(slot: Slot): boolean { return !this.disabled && this.isFree(); } } export interface ICardUserData { }
槽。
export class Slot extends AssembleBase{ public userData: ISlotUserData = {}; protected cards: Card[] = []; protected maxCards: number = 1; constructor(engine: AssembleEngine, assembleData: IAssembleAble, group: number = -1, maxCards: number = 1){ super(engine, assembleData); this.group = group; this.maxCards = maxCards; this.engine.addSlot(this); } public destroy(): void { this.engine.removeSlot(this); this.cards.forEach((card: Card) => { card.slot = null; }); this.cards = null; this.userData = null; super.destroy(); } /** * 是否是空的 * @returns {boolean} */ public isEmpty(): boolean{ return this.cards.length === 0; } /** * 是否已满 * @returns {boolean} */ public isFull(): boolean{ return this.cards.length >= this.maxCards; } /** * 是否能添加指定的卡 * @param card * @returns {boolean} */ public canAdd(card: Card): boolean{ return !this.disabled && !this.hasCard(card) && !this.isFull() && (this.group & card.group) !== 0; } /** * 是否包含卡 * @param card * @returns {boolean} */ public hasCard(card: Card): boolean{ return card.slot === this; } /** * 添加卡 * @param card */ public addCard(card: Card): void{ if (card.slot) { card.slot.removeCard(card); } this.cards.push(card); card.slot = this; } /** * 移除卡 * @param card */ public removeCard(card: Card): void{ const ind: number = this.cards.indexOf(card); if (ind !== -1) { this.cards.splice(ind, 1); card.slot = null; } } } export interface ISlotUserData { }
AssembleEngine。
export class AssembleEngine{ protected cardArr: Card[] = []; protected slotArr: Slot[] = []; constructor(){ } public destroy(): void{ this.cardArr = null; this.slotArr = null; } public addCard(card: Card): void{ ArrayUtil.add(this.cardArr, card); } public removeCard(card: Card): void{ ArrayUtil.remove(this.cardArr, card); } public addSlot(slot: Slot): void{ ArrayUtil.add(this.slotArr, slot); } public removeSlot(slot: Slot): void{ ArrayUtil.remove(this.slotArr, slot); } public update(dt: number): void { this.cardArr.forEach((card: Card) => { // 卡跟随槽 }); } }
总结
从整体结构来看,我们有:Card、Slot、Engine,如果加上碰撞检测和卡跟随槽的代码,应该还有一个Calculater。
力学引擎我们都比较熟悉,力学引擎是Shape、Body、World,再加上约束、碰撞检测、碰撞反应。
我们的电学引擎是Vertex、Edge、Graph,再加一个求解算法。
还会有其它好多引擎,都是一样的结构。完全符合:程序 = 数据结构 + 算法。
从Card和Slot的具体实现来看,很像显示列表的实现(Container和DisplayObjet)。
无论是代码结构,还是代码实现,都有很成熟的方案可供参考。这样,我们出错的可能性就大大降低了。
模型很简单,也很容易理解。这个简单的模型,迄今为止,可以满足我们所有的拼装需求。
发表评论:
◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。