23
2021
06

器材的拼装-卡槽模型

问题描述

实验开发中,我们经常要用到将两个器材(或者器材的部件)绑定到一起,并且能够解绑。比如电学中,导线的端点绑定到接线柱;家庭电路中,插头插到插座上;光学中,透镜放到光具座上;热学或者化学中,瓶盖绑定瓶子,玻璃导管放到试管中,等等。

方案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)。

无论是代码结构,还是代码实现,都有很成熟的方案可供参考。这样,我们出错的可能性就大大降低了。

模型很简单,也很容易理解。这个简单的模型,迄今为止,可以满足我们所有的拼装需求。



« 上一篇下一篇 »

发表评论:

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。