๐Ÿ•น Canvas StarCraft

kimbyungchanยท2020๋…„ 11์›” 13์ผ
87

canvas

๋ชฉ๋ก ๋ณด๊ธฐ
4/4
post-thumbnail

๐Ÿค” ์บ”๋ฒ„์Šค๋ฅผ ์ด์šฉํ•ด ์Šคํƒ€ํฌ๋ž˜ํ”„ํŠธ๋ฅผ ๋งŒ๋“ค์–ด๋ด…์‹œ๋‹ค.

https://velog.io/@kimbyungchan/canvas-animation
https://velog.io/@kimbyungchan/canvas-mouse-interaction
https://velog.io/@kimbyungchan/canvas-fireworks
์ด์ „ ๊ธ€์„ ์ฐธ๊ณ ํ•˜์‹œ๋ฉด ๊ธฐ๋ณธ์ ์ธ ๊ตฌ์กฐ๋ฅผ ์ดํ•ดํ•˜๋Š”๋ฐ ๋„์›€์ด๋ฉ๋‹ˆ๋‹ค.

1. ๋จผ์ € ๋“œ๋ž˜๊ทธ ๊ธฐ๋Šฅ๋ถ€ํ„ฐ

์Šคํƒ€ํฌ๋ž˜ํ”„ํŠธ์—์„œ ์œ ๋‹›์„ ์„ ํƒํ•˜๋Š” ๋ฐฉ๋ฒ•์ค‘ ํ•˜๋‚˜์ธ ๋“œ๋ž˜๊ทธ๋ฅผ ๋จผ์ € ๊ตฌํ˜„ํ•ด๋ด…์‹œ๋‹ค.

// App.ts
import Time from './Time';
import EntityManager from './EntityManager';
import Vector from './Vector';

export default class App {
  static instance: App;

  ref: HTMLElement;
  canvas: HTMLCanvasElement;
  context: CanvasRenderingContext2D;
  handleRequestFrame: number | null = null;
  entityManager: EntityManager;

  isPressed: boolean = false;
  mouseDownPosition: Vector = new Vector(0, 0);
  mousePosition: Vector = new Vector(0, 0);
  mouseUpPosition: Vector = new Vector(0, 0);

  constructor(ref: HTMLElement) {
    App.instance = this;

    this.ref = ref;
    this.canvas = document.createElement('canvas');
    this.canvas.width = window.innerWidth;
    this.canvas.height = window.innerHeight;
    this.context = this.canvas.getContext('2d')!;
    this.entityManager = new EntityManager();
    this.ref.appendChild(this.canvas);
  }

  onMouseDown = (e: MouseEvent) => {
    this.isPressed = true;
    this.mouseDownPosition = new Vector(e.clientX, e.clientY);
  }

  onMouseMove = (e: MouseEvent) => {
    this.mousePosition = new Vector(e.clientX,e.clientY);
  }

  onMouseUp = (e: MouseEvent) => {
    this.isPressed = false;
    this.mouseUpPosition = new Vector(e.clientX, e.clientY);
  }

  play = () => {
    Time.start();
    window.addEventListener('mousedown', this.onMouseDown);
    window.addEventListener('mousemove', this.onMouseMove);
    window.addEventListener('mouseup', this.onMouseUp);
    this.handleRequestFrame = window.requestAnimationFrame(this.onEnterFrame);
  }

  pause = () => {
    if (this.handleRequestFrame === null) {
      return;
    }

    window.cancelAnimationFrame(this.handleRequestFrame);
  }

  onEnterFrame = () => {
    Time.update();
    this.entityManager.update();
    this.entityManager.render(this.context);
    this.handleRequestFrame = window.requestAnimationFrame(this.onEnterFrame);
  }
}

๋งˆ์šฐ์Šค ์‹œ์ž‘, ํ˜„์žฌ, ๋ ์ขŒํ‘œ๋ฅผ ์ €์žฅํ•  ๋ณ€์ˆ˜๋ฅผ ๋งŒ๋“ค๊ณ 
App ํด๋ž˜์Šค์— ๊ฐ„ํŽธํ•˜๊ฒŒ ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ๋„๋ก ์‹ฑ๊ธ€ํ†ค ํŒจํ„ด์œผ๋กœ ๋งŒ๋“ค์—ˆ์Šต๋‹ˆ๋‹ค.

2. ์ด์ œ ๋“œ๋ž˜๊ทธ ์˜์—ญ์„ ๋ณด์—ฌ์ค์‹œ๋‹ค.๐Ÿ–ฑ

// DragArea.ts
import Entity from './Entity';
import Vector from './Vector';
import App from './App';

export default class DragArea extends Entity {
  constructor(position: Vector) {
    super(position);
  }

  update() {
    this.position = App.instance.mouseDownPosition;
  }

  render(context: CanvasRenderingContext2D) {
    if (!App.instance.isPressed) {
      return;
    }

    context.beginPath();
    context.strokeStyle = '#00ff00';
    context.lineWidth = 2;
    context.rect(this.position.x, this.position.y, App.instance.mousePosition.x - this.position.x, App.instance.mousePosition.y - this.position.y);
    context.stroke();
  }
}

App์˜ ๋งˆ์šฐ์Šค ์ขŒํ‘œ๋ฅผ ๊ฐ€์ ธ์™€ ์‚ฌ๊ฐํ˜•์„ ๊ทธ๋ ธ์Šต๋‹ˆ๋‹ค. ๊ทธ๋ฆฌ๊ณ  ํ•ด๋‹น ์—”ํ‹ฐํ‹ฐ๋ฅผ ์ถ”๊ฐ€ํ•ด์ฃผ์–ด์•ผํ•ฉ๋‹ˆ๋‹ค.

// index.ts
import App from './App';
import DragArea from './DragArea';
import Vector from './Vector';

window.addEventListener('load', () => {
  const app = new App(document.body);
  const dragArea = new DragArea(new Vector(0, 0))
  app.entityManager.addEntity(dragArea);
  app.play();
});

์ด์ œ ๋“œ๋ž˜๊ทธ๋ฅผ ํ•ด๋ณด์‹œ๋ฉด

์„ฑ๊ณต์ ์œผ๋กœ ๋“œ๋ž˜๊ทธ์˜์—ญ์ด ๋ณด์ด๋Š”๊ฑธ ํ™•์ธํ•˜์‹ค์ˆ˜์žˆ์Šต๋‹ˆ๋‹ค.

3. ์ด์ œ ์œ ๋‹›์„ ๋งŒ๋“ค์–ด ๋ด…์‹œ๋‹ค. ๐Ÿคทโ€โ™€๏ธ

// Unit.ts
import Entity from "./Entity";
import Vector from "./Vector";

function drawEllipse(
  context: CanvasRenderingContext2D,
  x: number,
  y: number,
  w: number,
  h: number
) {
  const kappa = 0.5522848,
    ox = (w / 2) * kappa,
    oy = (h / 2) * kappa,
    xe = x + w,
    ye = y + h,
    xm = x + w / 2,
    ym = y + h / 2;
  context.beginPath();
  context.moveTo(x, ym);
  context.bezierCurveTo(x, ym - oy, xm - ox, y, xm, y);
  context.bezierCurveTo(xm + ox, y, xe, ym - oy, xe, ym);
  context.bezierCurveTo(xe, ym + oy, xm + ox, ye, xm, ye);
  context.bezierCurveTo(xm - ox, ye, x, ym + oy, x, ym);
  context.closePath();
  context.stroke();
}

export default class Unit extends Entity {
  radius: number;
  speed: number;
  isSelected: boolean = true;

  constructor(position: Vector) {
    super(position);

    this.radius = 15;
    this.speed = 100;
  }

  update() {}

  render(context: CanvasRenderingContext2D) {
    if (this.isSelected) {
      context.beginPath();
      context.lineWidth = 2;
      context.strokeStyle = "#0f0";
      drawEllipse(
        context,
        this.position.x - this.radius,
        this.position.y + this.radius * 0.75,
        this.radius * 2,
        this.radius * 0.75
      );
    }

    context.beginPath();
    context.fillStyle = "#000";
    context.arc(this.position.x, this.position.y, this.radius, 0, Math.PI * 2);
    context.fill();
  }
}

๊ธฐ๋ณธ์ ์ธ ์Šคํƒฏ speed, radius ๋ฅผ ์ถ”๊ฐ€ํ•˜๊ณ 
์„ ํƒ๋˜์—ˆ๋Š”์ง€ ํ™•์ธํ•  isSelected๋„ ์ถ”๊ฐ€ํ•ด์ฃผ์—ˆ์Šต๋‹ˆ๋‹ค.
๋‚˜์ค‘์— ๋‹ค๋ฅธ ์œ ๋‹›์„ ๋งŒ๋“ค๋•Œ ํ•ด๋‹นํด๋ž˜์Šค๋ฅผ ์ƒ์†๋ฐ›์•„์„œ ๋งŒ๋“ค๋ฉด ๋˜๊ฒ ์ฃ ?

4. ์ด์ œ ์œ ๋‹›์„ ์ถ”๊ฐ€ํ•˜๊ณ  ๋“œ๋ž˜๊ทธํ•ด์„œ ์„ ํƒํ•ด๋ด…์‹œ๋‹ค.

// index.ts
import App from './App';
import DragArea from './DragArea';
import Vector from './Vector';
import Unit from './Unit';

window.addEventListener('load', () => {
  const app = new App(document.body);
  const dragArea = new DragArea(new Vector(0, 0))
  const unit = new Unit(new Vector(app.canvas.width * 0.5, app.canvas.height * 0.5));
  app.entityManager.addEntity(dragArea);
  app.entityManager.addEntity(unit);
  app.play();
});

๋Œ€์ถฉ ์•„๋ฌด๊ณณ์—์„œ๋‚˜ ์œ ๋‹›์„ ์ƒ์„ฑํ•ด์„œ ์ถ”๊ฐ€ํ•ด์ค๋‹ˆ๋‹ค.

// App.ts
import Time from "./Time";
import EntityManager from "./EntityManager";
import Vector from "./Vector";
import Unit from "src/Unit";

export default class App {
  static instance: App;

  ref: HTMLElement;
  canvas: HTMLCanvasElement;
  context: CanvasRenderingContext2D;
  handleRequestFrame: number | null = null;
  entityManager: EntityManager;

  isPressed: boolean = false;
  mouseDownPosition: Vector = new Vector(0, 0);
  mousePosition: Vector = new Vector(0, 0);
  mouseUpPosition: Vector = new Vector(0, 0);
  selectedUnits: Array<Unit> = [];

  constructor(ref: HTMLElement) {
    App.instance = this;

    this.ref = ref;
    this.canvas = document.createElement("canvas");
    this.canvas.width = window.innerWidth;
    this.canvas.height = window.innerHeight;
    this.context = this.canvas.getContext("2d")!;
    this.entityManager = new EntityManager();
    this.ref.appendChild(this.canvas);
  }

  onMouseDown = (e: MouseEvent) => {
    this.isPressed = true;
    this.mouseDownPosition = new Vector(e.clientX, e.clientY);
  };

  onMouseMove = (e: MouseEvent) => {
    this.mousePosition = new Vector(e.clientX, e.clientY);
  };

  onMouseUp = (e: MouseEvent) => {
    this.isPressed = false;
    this.mouseUpPosition = new Vector(e.clientX, e.clientY);

    const startX = Math.min(this.mouseDownPosition.x, this.mouseUpPosition.x);
    const endX = Math.max(this.mouseDownPosition.x, this.mouseUpPosition.x);

    const startY = Math.min(this.mouseDownPosition.y, this.mouseUpPosition.y);
    const endY = Math.max(this.mouseDownPosition.y, this.mouseUpPosition.y);
    const units = this.entityManager.entities.filter(
      (entity) => entity instanceof Unit
    ) as Unit[];

    this.selectedUnits = units.filter((entity: Unit) => {
      return (
        entity.position.x >= startX &&
        entity.position.x <= endX &&
        entity.position.y >= startY &&
        entity.position.y <= endY
      );
    });

    for (let i = 0; i < units.length; i++) {
      units[i].isSelected = false;
    }

    for (let i = 0; i < this.selectedUnits.length; i++) {
      this.selectedUnits[i].isSelected = true;
    }
  };

  play = () => {
    Time.start();
    window.addEventListener("mousedown", this.onMouseDown);
    window.addEventListener("mousemove", this.onMouseMove);
    window.addEventListener("mouseup", this.onMouseUp);
    this.handleRequestFrame = window.requestAnimationFrame(this.onEnterFrame);
  };

  pause = () => {
    if (this.handleRequestFrame === null) {
      return;
    }

    window.cancelAnimationFrame(this.handleRequestFrame);
  };

  onEnterFrame = () => {
    Time.update();
    this.entityManager.update();
    this.entityManager.render(this.context);
    this.handleRequestFrame = window.requestAnimationFrame(this.onEnterFrame);
  };
}

onMouseUp ํ•จ์ˆ˜์—์„œ ์œ ๋‹› ์„ ํƒ ๋กœ์ง์„ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค.

๋”ฐ๋ž€. ์ด์ œ ์„ ํƒ์ด๋ฉ๋‹ˆ๋‹ค.

5. ์ด์ œ ์›€์ง์ด๊ฒŒ ํ•˜๊ธฐ์ „์—..

๋งˆ์šฐ์Šค ์™ผ์ชฝํด๋ฆญ์œผ๋กœ ๋“œ๋ž˜๊ทธ๋ฅผ ํ•˜๊ณ  ์˜ค๋ฅธ์ชฝ ํด๋ฆญ์œผ๋กœ ์›€์ง์ด๊ฒŒ ํ•˜๋ ค๋ฉด ์ผ๋‹จ onMouse* ํ•จ์ˆ˜์—์„œ
์–ด๋–ค ๋ฒ„ํŠผ์„ ๋ˆŒ๋ €๋Š”์ง€ ๋‚˜๋ˆ„์–ด์ค์‹œ๋‹ค.

// App.ts
import Time from "./Time";
import EntityManager from "./EntityManager";
import Vector from "./Vector";
import Unit from "src/Unit";

export default class App {
  static instance: App;

  ref: HTMLElement;
  canvas: HTMLCanvasElement;
  context: CanvasRenderingContext2D;
  handleRequestFrame: number | null = null;
  entityManager: EntityManager;

  isPressed: boolean = false;
  mouseDownPosition: Vector = new Vector(0, 0);
  mousePosition: Vector = new Vector(0, 0);
  mouseUpPosition: Vector = new Vector(0, 0);
  selectedUnits: Array<Unit> = [];

  constructor(ref: HTMLElement) {
    App.instance = this;

    this.ref = ref;
    this.canvas = document.createElement("canvas");
    this.canvas.width = window.innerWidth;
    this.canvas.height = window.innerHeight;
    this.context = this.canvas.getContext("2d")!;
    this.entityManager = new EntityManager();
    this.ref.appendChild(this.canvas);
  }

  onMouseDown = (e: MouseEvent) => {
    e.preventDefault();
    e.stopPropagation();

    if (e.button === 0) {
      this.isPressed = true;
      this.mouseDownPosition = new Vector(e.clientX, e.clientY);
    } else if (e.button === 2) {

    }
  };

  onMouseMove = (e: MouseEvent) => {
    this.mousePosition = new Vector(e.clientX, e.clientY);
  };

  onMouseUp = (e: MouseEvent) => {
    if (e.button !== 0) {
      return;
    }

    this.isPressed = false;
    this.mouseUpPosition = new Vector(e.clientX, e.clientY);

    const startX = Math.min(this.mouseDownPosition.x, this.mouseUpPosition.x);
    const endX = Math.max(this.mouseDownPosition.x, this.mouseUpPosition.x);

    const startY = Math.min(this.mouseDownPosition.y, this.mouseUpPosition.y);
    const endY = Math.max(this.mouseDownPosition.y, this.mouseUpPosition.y);
    const units = this.entityManager.entities.filter(
      (entity) => entity instanceof Unit
    ) as Unit[];

    this.selectedUnits = units.filter((entity: Unit) => {
      return (
        entity.position.x >= startX &&
        entity.position.x <= endX &&
        entity.position.y >= startY &&
        entity.position.y <= endY
      );
    });

    for (let i = 0; i < units.length; i++) {
      units[i].isSelected = false;
    }

    for (let i = 0; i < this.selectedUnits.length; i++) {
      this.selectedUnits[i].isSelected = true;
    }
  };

  play = () => {
    Time.start();
    window.addEventListener(
      "contextmenu",
      (e) => {
        e.preventDefault();
      },
      false
    );
    window.addEventListener("mousedown", this.onMouseDown);
    window.addEventListener("mousemove", this.onMouseMove);
    window.addEventListener("mouseup", this.onMouseUp);
    this.handleRequestFrame = window.requestAnimationFrame(this.onEnterFrame);
  };

  pause = () => {
    if (this.handleRequestFrame === null) {
      return;
    }

    window.cancelAnimationFrame(this.handleRequestFrame);
  };

  onEnterFrame = () => {
    Time.update();
    this.entityManager.update();
    this.entityManager.render(this.context);
    this.handleRequestFrame = window.requestAnimationFrame(this.onEnterFrame);
  };
}

e.button์œผ๋กœ ๋งˆ์šฐ์Šค ์™ผ์ชฝ ์˜ค๋ฅธ์ชฝ ๋ฒ„ํŠผ์„ ๊ตฌ๋ถ„ํ• ์ˆ˜์žˆ๊ณ 
๊ทธ๋ฆฌ๊ณ  contextmenu ์ด๋ฒคํŠธ๋Š” ๋น„ํ™œ์„ฑํ™” ํ•ด์ค˜์•ผํ•ฉ๋‹ˆ๋‹ค

6. ๋“œ๋””์–ด ์œ ๋‹›์„ ์›€์ง์—ฌ๋ด…์‹œ๋‹ค.

// Vector.ts
export default class Vector {
  x: number;
  y: number;

  constructor(x: number, y: number) {
    this.x = x;
    this.y = y;
  }

  public angleBetween(other: Vector): number {
    return Math.atan2(other.y - this.y, other.x - this.x);
  }

  public distance(other: Vector): number {
    return Math.sqrt(Math.pow(this.x - other.x, 2) + Math.pow(this.y - other.y, 2));
  }
}

๋ชฉํ‘œ ์ง€์ ๊ณผ ์œ ๋‹›์˜ ๋‘ ์ขŒํ‘œ์‚ฌ์ด์˜ ๊ฐ๋„๋ฅผ ๊ตฌํ•  ํ•จ์ˆ˜์™€ ๊ฑฐ๋ฆฌ๋ฅผ ์ธก์ •ํ•  ์ˆ˜ ์žˆ๋Š” ํ•จ์ˆ˜๋ฅผ ์ถ”๊ฐ€ํ•˜๊ณ 

// Unit.ts
import Entity from "./Entity";
import Vector from "./Vector";
import Time from './Time';

function drawEllipse(
  context: CanvasRenderingContext2D,
  x: number,
  y: number,
  w: number,
  h: number
) {
  const kappa = 0.5522848,
    ox = (w / 2) * kappa,
    oy = (h / 2) * kappa,
    xe = x + w,
    ye = y + h,
    xm = x + w / 2,
    ym = y + h / 2;
  context.beginPath();
  context.moveTo(x, ym);
  context.bezierCurveTo(x, ym - oy, xm - ox, y, xm, y);
  context.bezierCurveTo(xm + ox, y, xe, ym - oy, xe, ym);
  context.bezierCurveTo(xe, ym + oy, xm + ox, ye, xm, ye);
  context.bezierCurveTo(xm - ox, ye, x, ym + oy, x, ym);
  context.closePath();
  context.stroke();
}

export default class Unit extends Entity {
  radius: number;
  speed: number;
  movement: boolean = false;
  targetPosition: Vector = new Vector(0, 0);
  isSelected: boolean = false;

  constructor(position: Vector) {
    super(position);

    this.radius = 15;
    this.speed = 100;
  }

  update() {
    if (this.movement) {
      const angle = this.position.angleBetween(this.targetPosition);
      this.position.x += Math.cos(angle) * this.speed * Time.delta;
      this.position.y += Math.sin(angle) * this.speed * Time.delta;

      if (this.position.distance(this.targetPosition) <= 1) {
        this.movement = false;
      }
    }
  }

  render(context: CanvasRenderingContext2D) {
    if (this.isSelected) {
      context.beginPath();
      context.lineWidth = 2;
      context.strokeStyle = "#0f0";
      drawEllipse(
        context,
        this.position.x - this.radius,
        this.position.y + this.radius * 0.75,
        this.radius * 2,
        this.radius * 0.75
      );
    }

    context.beginPath();
    context.fillStyle = "#000";
    context.arc(this.position.x, this.position.y, this.radius, 0, Math.PI * 2);
    context.fill();
  }
}

ํ•ด๋‹น ํ•จ์ˆ˜๋“ค์„ ์ด์šฉํ•ด ์›€์ง์ด๋ผ๋Š” ๋ช…๋ น์„ ๋ฐ›์œผ๋ฉด ํ•ด๋‹น ์ขŒํ‘œ๊นŒ์ง€ ์›€์ง์ด๊ณ  ๋„์ฐฉํ•˜๋ฉด ๋ฉˆ์ถ”๋Š” ๋กœ์ง์„ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค.

// App.ts
import Time from "./Time";
import EntityManager from "./EntityManager";
import Vector from "./Vector";
import Unit from "src/Unit";

export default class App {
  static instance: App;

  ref: HTMLElement;
  canvas: HTMLCanvasElement;
  context: CanvasRenderingContext2D;
  handleRequestFrame: number | null = null;
  entityManager: EntityManager;

  isPressed: boolean = false;
  mouseDownPosition: Vector = new Vector(0, 0);
  mousePosition: Vector = new Vector(0, 0);
  mouseUpPosition: Vector = new Vector(0, 0);
  selectedUnits: Array<Unit> = [];

  constructor(ref: HTMLElement) {
    App.instance = this;

    this.ref = ref;
    this.canvas = document.createElement("canvas");
    this.canvas.width = window.innerWidth;
    this.canvas.height = window.innerHeight;
    this.context = this.canvas.getContext("2d")!;
    this.entityManager = new EntityManager();
    this.ref.appendChild(this.canvas);
  }

  onMouseDown = (e: MouseEvent) => {
    e.preventDefault();
    e.stopPropagation();

    if (e.button === 0) {
      this.isPressed = true;
      this.mouseDownPosition = new Vector(e.clientX, e.clientY);
    } else if (e.button === 2) {
      for (let i = 0; i < this.selectedUnits.length; i++) {
        this.selectedUnits[i].movement = true;
        this.selectedUnits[i].targetPosition = new Vector(e.clientX, e.clientY);
      }
    }
  };

  onMouseMove = (e: MouseEvent) => {
    this.mousePosition = new Vector(e.clientX, e.clientY);
  };

  onMouseUp = (e: MouseEvent) => {
    if (e.button !== 0) {
      return;
    }

    this.isPressed = false;
    this.mouseUpPosition = new Vector(e.clientX, e.clientY);

    const startX = Math.min(this.mouseDownPosition.x, this.mouseUpPosition.x);
    const endX = Math.max(this.mouseDownPosition.x, this.mouseUpPosition.x);

    const startY = Math.min(this.mouseDownPosition.y, this.mouseUpPosition.y);
    const endY = Math.max(this.mouseDownPosition.y, this.mouseUpPosition.y);
    const units = this.entityManager.entities.filter(
      (entity) => entity instanceof Unit
    ) as Unit[];

    this.selectedUnits = units.filter((entity: Unit) => {
      return (
        entity.position.x >= startX &&
        entity.position.x <= endX &&
        entity.position.y >= startY &&
        entity.position.y <= endY
      );
    });

    for (let i = 0; i < units.length; i++) {
      units[i].isSelected = false;
    }

    for (let i = 0; i < this.selectedUnits.length; i++) {
      this.selectedUnits[i].isSelected = true;
    }
  };

  play = () => {
    Time.start();
    window.addEventListener(
      "contextmenu",
      (e) => {
        e.preventDefault();
      },
      false
    );
    window.addEventListener("mousedown", this.onMouseDown);
    window.addEventListener("mousemove", this.onMouseMove);
    window.addEventListener("mouseup", this.onMouseUp);
    this.handleRequestFrame = window.requestAnimationFrame(this.onEnterFrame);
  };

  pause = () => {
    if (this.handleRequestFrame === null) {
      return;
    }

    window.cancelAnimationFrame(this.handleRequestFrame);
  };

  onEnterFrame = () => {
    Time.update();
    this.entityManager.update();
    this.entityManager.render(this.context);
    this.handleRequestFrame = window.requestAnimationFrame(this.onEnterFrame);
  };
}

๋งˆ์šฐ์Šค ์˜ค๋ฅธ์ชฝ ํด๋ฆญ์ด ๋ˆŒ๋ ธ์„๋•Œ ์„ ํƒ๋œ ์œ ๋‹› ์ „์ฒด์—๊ฒŒ ํด๋ฆญํ•œ ์ขŒํ‘œ๋กœ ์ด๋™ํ•˜๋ผ๊ณ  ๊ฐ’์„ ๋„ฃ์–ด์ฃผ์—ˆ์Šต๋‹ˆ๋‹ค.

๋“œ๋””์–ด ์›€์ง์ž…๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ ์œ ๋‹›์„ ์—ฌ๋Ÿฌ๊ฐœ ์ถ”๊ฐ€ํ•ด๋ณด๋„๋ก ํ•˜์ฃ .

์œ ๋‹›๋ผ๋ฆฌ ์„œ๋กœ ๊ฒน์น˜๊ฒŒ๋ฉ๋‹ˆ๋‹ค (ํžˆ๋“œ๋ผ ๊ฒน์น˜๊ธฐ).
ํ•ด๋‹น ์ด์Šˆ๋ฅผ ๊ณ ์ณ๋ณด๋„๋ก ํ•ฉ์‹œ๋‹ค.

7. ์ถฉ๋Œ์ฒ˜๋ฆฌ

// Unit.ts
import Entity from "./Entity";
import Vector from "./Vector";
import Time from './Time';
import EntityManager from 'src/EntityManager';

function drawEllipse(
  context: CanvasRenderingContext2D,
  x: number,
  y: number,
  w: number,
  h: number
) {
  const kappa = 0.5522848,
    ox = (w / 2) * kappa,
    oy = (h / 2) * kappa,
    xe = x + w,
    ye = y + h,
    xm = x + w / 2,
    ym = y + h / 2;
  context.beginPath();
  context.moveTo(x, ym);
  context.bezierCurveTo(x, ym - oy, xm - ox, y, xm, y);
  context.bezierCurveTo(xm + ox, y, xe, ym - oy, xe, ym);
  context.bezierCurveTo(xe, ym + oy, xm + ox, ye, xm, ye);
  context.bezierCurveTo(xm - ox, ye, x, ym + oy, x, ym);
  context.closePath();
  context.stroke();
}

export default class Unit extends Entity {
  radius: number;
  speed: number;
  movement: boolean = false;
  targetPosition: Vector = new Vector(0, 0);
  isSelected: boolean = false;

  constructor(position: Vector) {
    super(position);

    this.radius = 15;
    this.speed = 100;
  }

  update() {
    const units = EntityManager.instance.entities.filter((entity) => entity instanceof Unit) as Unit[];

    for (let i = 0; i < units.length; i++) {
      const unit = units[i];
      if (this !== unit) {
        const distance = this.position.distance(unit.position);
        const length = this.radius + unit.radius;
        if (distance <= length) {
          const force = length - distance;
          const angle = this.position.angleBetween(unit.position);

          this.position.x -= Math.cos(angle) * force;
          this.position.y -= Math.sin(angle) * force;
        }
      }
    }

    if (this.movement) {
      const angle = this.position.angleBetween(this.targetPosition);
      this.position.x += Math.cos(angle) * this.speed * Time.delta;
      this.position.y += Math.sin(angle) * this.speed * Time.delta;

      if (this.position.distance(this.targetPosition) <= 1) {
        this.movement = false;
      }
    }
  }

  render(context: CanvasRenderingContext2D) {
    if (this.isSelected) {
      context.beginPath();
      context.lineWidth = 2;
      context.strokeStyle = "#0f0";
      drawEllipse(
        context,
        this.position.x - this.radius,
        this.position.y + this.radius * 0.75,
        this.radius * 2,
        this.radius * 0.75
      );
    }

    context.beginPath();
    context.fillStyle = "#000";
    context.arc(this.position.x, this.position.y, this.radius, 0, Math.PI * 2);
    context.fill();
  }
}

EntityManager์—์„œ Unit์„ ๋ชจ๋‘ ๊ฐ€์ ธ์™€ ์ž๊ธฐ ์ž์‹ ์ด ์•„๋‹Œ ๋‹ค๋ฅธ ์œ ๋‹›๊ณผ ์ถฉ๋Œํ• ๊ฒฝ์šฐ
์ถฉ๋Œํ•œ๋งŒํผ ๋ฐ€๋ ค๋‚˜๊ฒŒ ์ž‘์„ฑํ–ˆ์Šต๋‹ˆ๋‹ค.

์œ ๋‹›๋ผ๋ฆฌ ์ด์ œ ๊ฒน์น˜์ง€๋Š” ์•Š๋Š”๋ฐ ์„œ๋กœ ์ถฉ๋Œํ•˜๋‹ˆ๊นŒ ๋ชฉ์ ์ง€๊ฐ€ ๊ฐ™์•„์„œ ๊ณ„์† ๋ชฉ์ ์ง€์— ๋„๋‹ฌํ•˜๋ ค๊ณ  ํ•˜๋‹ˆ๊นŒ
์›€์ง์ž„์ด ๋ฉˆ์ถ”์ง€์•Š๋Š” ์ด์Šˆ๊ฐ€ ์žˆ๋„ค์š” ํ•ด๋‹น ์ด์Šˆ๋Š” ๋‹ค์ŒํŽธ์—์„œ ํ•ด๊ฒฐํ•˜๋„๋ก ํ•˜์‹œ์ฃ .

๋„ต ์—ฌ๊ธฐ๊นŒ์ง€ Canvas๋กœ StarCraft๋งŒ๋“ค์–ด๋ณด๊ธฐ์˜€์Šต๋‹ˆ๋‹ค.


WE-AR์—์„œ ์ฑ„์šฉ์„ ์ง„ํ–‰ํ•˜๊ณ ์žˆ์Šต๋‹ˆ๋‹ค.
https://www.notion.so/WE-AR-b8fc4563ede64311896e032aaada52d4

profile
๊ฐ„๋‹จํ•œ๊ฑธ ์ข‹์•„ํ•˜๋Š” ๊ฐœ๋ฐœ์ž์ž…๋‹ˆ๋‹ค

5๊ฐœ์˜ ๋Œ“๊ธ€

comment-user-thumbnail
2020๋…„ 11์›” 15์ผ

์˜ค ํฅ๋ฏธ๋กญ๊ฒŒ ์ž˜๋ดค์Šต๋‹ˆ๋‹ค!

๋‹ต๊ธ€ ๋‹ฌ๊ธฐ
comment-user-thumbnail
2020๋…„ 11์›” 16์ผ

์ž˜๋ณด๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค~~

๋‹ต๊ธ€ ๋‹ฌ๊ธฐ
comment-user-thumbnail
2020๋…„ 11์›” 19์ผ

์™€ ์ €๋„ canvas ์— ๊ด€์‹ฌ์ด ๋งŽ์•„์„œ ์ด๊ฒƒ์ €๊ฒƒ ํ•ด๋ดค๋Š”๋ฐ ๋Œ€๋‹จํ•˜๋„ค์š”

๋‹ต๊ธ€ ๋‹ฌ๊ธฐ
comment-user-thumbnail
2020๋…„ 11์›” 25์ผ

์˜ค์˜ค ์žฌ๋ฐŒ์–ด๋ณด์ด๋„ค์šฉ ํผ๊ฐ‘๋‹ˆ๋‹ค์š”~

๋‹ต๊ธ€ ๋‹ฌ๊ธฐ
comment-user-thumbnail
4์ผ ์ „

์šฐ์™€.. ๋„ˆ๋ฌด ํฅ๋ฏธ๋กญ๊ณ  ์‹ ๊ธฐํ•˜๋„ค์š”!!

๋‹ต๊ธ€ ๋‹ฌ๊ธฐ