시뮬레이터 구현

2020년 12월 19일
Guidance Simulator

생각보다 남은 작업이 크지 않아서 세 번째 글에 이어 바로 네 번째 글을 작성하였습니다. 이번 글에서는 전체적인 차량 생성 및 설정, 삭제를 처리할 Simulator에 대해 작성하고 합니다.

작동 방식

Simulator의 작동 방식은 이전 글에서 보여드렸던 TaskVehicle과 크게 다르지 않습니다. 지도 컴포넌트가 생성될 때, 시뮬레이터도 생성하게 되며 초깃값으로 설정한 만큼의 차량을 생성하게 됩니다. 이후 설정한 값의 변경에 따라 차량을 늘리거나 줄이는 작업 혹은 차량의 설정을 변경하는 작업을 진행하게 됩니다. 최종적으로 지도 컴포넌트가 제거될 때 시뮬레이터를 멈추는 메소드인 destroy()를 호출하게 되면 Worker 및 차량, 작업 모두 제거하게 됩니다.

export default class Simulator extends EventEmitter {
  private size: number = DEFAULT_VEHICLE_SIZE;
  private acceleration: number = DEFAULT_VEHICLE_ACCELERATION;
  private workers: any[] = [];
  private taskMap: Map<string, Task> = new Map();
  private movingMap: Map<string, VehicleMoving> = new Map();
  private destroyed: boolean = false;

  public async initialize(size: number): Promise<void> {
    this.size = size ?? DEFAULT_VEHICLE_SIZE;

    for (let i = 0; i < size; i++) {
      await this.generateWorker();
    }
  }

  public async destroy(): Promise<void> {
    this.destroyed = true;
    const size = this.workers.length;
    for (let i = 0; i < size; i++) {
      await this.terminateWorker(this.workers[i]);
    }
  }

  public async setSize(size: number): Promise<void> {
    if (typeof size !== 'number') {
      return;
    }

    let diff = this.size - size;

    if (diff === 0) {
      return;
    }

    if (diff > 0) {
      const removed = this.workers.splice(size, diff);
      for (let i = 0; i < diff; i++) {
        await this.terminateWorker(removed[i]);
      }
    } else {
      diff = Math.abs(diff);
      for (let i = 0; i < diff; i++) {
        await this.generateWorker();
      }
    }

    this.size = size ?? DEFAULT_VEHICLE_SIZE;
    this.onChange();
  }

  public setAcceleration(acceleration: number): void {
    if (typeof acceleration !== 'number') {
      return;
    }

    if (this.acceleration === acceleration) {
      return;
    }

    this.acceleration = acceleration ?? DEFAULT_VEHICLE_ACCELERATION;
    this.workers.forEach(worker => worker.acceleration(acceleration));
  }

  private async generateWorker(): Promise<any> {
    let worker = null;

    try {
      worker = await spawn(new Worker('../modules/worker.ts', { type: 'module' }));
      this.workers.push(worker)

      worker.initialize();
      worker.moving().subscribe((moving: VehicleMoving) => {
        this.movingMap.set(moving.id, moving);
        this.onChange();
      });
      worker.statuses().subscribe((vehicle: Vehicle) => {
        const task: Task = vehicle.getTask();
        if (vehicle.getStatus() === VehicleStatus.WORKING) {
          this.taskMap.set(task.getId(), task);
        } else if (vehicle.getStatus() === VehicleStatus.DONE) {
          this.taskMap.delete(task.getId());
        }
        this.onChange();
      });
    } catch (e) {
      console.error(e);
    }

    return worker;
  }

  private async terminateWorker(worker: any): Promise<void> {
    if (!worker) {
      return;
    }

    try {
      const { vehicleId, taskId } = await worker?.terminate();
      this.movingMap.delete(vehicleId);
      this.taskMap.delete(taskId);
      await Thread.terminate(worker);
    } catch (e) {
      console.error(e);
    }
  }

  private onChange(): void {
    if (this.destroyed) {
      return;
    }

    this.emit(SimulatorEvent.UPDATE, this.taskMap, this.movingMap);
  }
}

export enum SimulatorEvent {
  UPDATE = 'update',
}

앞서 설명해 드린 것과 같이 별다른 추가 기능 없이 Worker의 생성 및 삭제를 통해 차량을 관리하게 됩니다.

const PlaygroundMapGuidanceSimulatorMapTemplate: FC<PropsType> = ({ size, acceleration }) => {
  //...

  useEffect(() => {
    baseLayer.current = new GeoJsonLayer({
      id: 'base-layer',
      data: [AREA],
      filled: true,
      getFillColor: [160, 160, 180, 80],
    });

    simulator.current = new Simulator();
    void simulator.current.initialize(size);

    simulator.current.on(
      SimulatorEvent.UPDATE,
      (taskMap: Map<string, Task>, movingMap: Map<string, VehicleMoving>) => setLayers(getLayers(baseLayer.current, taskMap, movingMap))
    );

    return async () => {
      await simulator.current?.destroy();
    };
  }, []);

  useEffect(() => {
    void simulator.current?.setSize(size);
  }, [size]);

  useEffect(() => {
    void simulator.current?.setAcceleration(acceleration);
  }, [acceleration]);

  //...
};

지도를 사용하는 컴포넌트에서는 시뮬레이터의 이벤트에 따라 지도에 올라갈 레이어를 조정하게 되며, 컴포넌트가 삭제될 시 시뮬레이터에 대한 해제 작업을 수행하게 됩니다.

마치며

앞선 글들에서 대부분의 기능이 이미 구현되어 상대적으로 손쉽게 작업할 수 있었습니다. 간단하게 시뮬레이터를 추가하고 전반적으로 동작을 확인하며 버그를 수정하는 정도로 작업이 마무리하게 되었습니다. 처음 작업을 시작할 때는 많은 시간이 걸리겠다고 생각했는데, 막상 작업을 진행해보니 생각보다 빨리 끝난 것 같습니다. 이제는 여유가 생길 때 마다 좀 더 개선할 수 있는 점들에 대해 수정을 해가고자 합니다. 이로써 Guidance Simulator에 대한 글을 마치고자 합니다. 읽어주셔서 감사합니다.

본 글의 예제는 Guidance Simulator에서 확인하실 수 있습니다.

Recently posts
© 2016-2023 smilecat.dev