实验开发中,我们经常要用到将两个器材(或者器材的部件)绑定到一起,并且能够解绑。比如电学中,导线的端点绑定到接线柱;家庭电路中,插头插到插座上;光学中,透镜放到光具座上;热学或者化学中,瓶盖绑定瓶子,玻璃导管放到试管中,等等。不考虑通用模型,每一组需要绑定关系的器材单独维护一套数据。比如对于导线与接线柱的绑定,导线的一端同一时刻只能绑定一个器材,器材的接线柱可以绑定多个导线,所以,器材的接线柱应该创建一个数组,来存储绑定的导线端点,导线的端点应该创建一个变量,来存储绑定的接线柱。缺点
只适用于这一种情况,当开发其他学科,其他模型的时候,又要创建一组变量,重新写一遍逻辑。在需要使用力学引擎的实验中,确实是用的关节,也很方便。使用关节连接之后,还有力学特性。缺点
在不需要力学引擎的地方,为了实现绑定关系,引入一个力学引擎,太重了。即便是在使用了力学引擎的实验中,如果不需要力学特性,使用关节,也人为的把问题复杂化了。结合以上两种方案,以及实际需求,我们自己创建一个抽象的模型。卡和槽的型号(分组,其实用掩码更合适)要匹配才能插上去。实现代码
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 {
}
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)。无论是代码结构,还是代码实现,都有很成熟的方案可供参考。这样,我们出错的可能性就大大降低了。模型很简单,也很容易理解。这个简单的模型,迄今为止,可以满足我们所有的拼装需求。