スプライン曲線(Rounded Nonuniform Spline)を描く

公開:2017-10-15 18:16
更新:2020-02-15 04:37
カテゴリ:シューティングゲーム,ゲーム製作,HTML5,ES6,JS

ここのところ仕事が忙しかったり、風邪をこじらせて家で寝まくっていたりであまりゲーム作りに時間をかけることができなかった。 が、ようやく体調が戻ってきたのでコードの見直しとか、お題のスプライン曲線を描いたりしていた。

スプライン曲線とは、いくつか座標を与えるとその点を通る曲線をコンピューターがいい具合に補完して描いてくれるというものである。 これを使えば、敵の移動軌跡を簡単に作ることができるので何とか実装しようと考えたのである。

しかしそうはいっても理屈はなんとなくわかる程度で、コードをサクッと書けるわけではない。のでググって情報を漁り、あわよくばコードを拝借して時間短縮することにした。

ググるとこの辺りの情報が参考になりそうだ。

その34 スプライン曲線上をおおよそ等速で移動する(丸み不均一スプライン)

上記は理屈のみでコードはない。もうちょっとググるとRNS(Rounded Nonuniform Spline)のコードを見つけることができた。ただしC++であるが。

レーザーについて考える その3 | ゆるふぁ~む

それでまあ、そのC++コードをまんま移植して利用することにした。それが以下のコードである。

// geometry.js
'use strict';

export class Vec2 {
  constructor(x = 0,y = 0){
    this.x = x;
    this.y = y;
  }
  static sub(v1,v2,out = new Vec2()){
    out.x = v1.x - v2.x;
    out.y = v1.y - v2.y;
    return out;
  }

  static add (v1,v2,out = new Vec2()){
    out.x = v1.x + v2.x;
    out.y = v1.y + v2.y;
    return out;
  }

  static mul (v,num,out = new Vec2()){
    out.x = v.x * num;
    out.y = v.y * num;
    return out;
  }

  static div (v,num,out = new Vec2()){
    out.x = v.x / num;
    out.y = v.y / num;
    return out;
  }

  static dot(v1,v2){
    return v1.x * v2.x + v1.y * v2.y; 
  }

  static cross(v1,v2){
    return v1.x * v2.y - v2.x * v1.y;
  }

  static normalize(v,out = new Vec2()){
    let l = Vec2.length(v);
    out.x = v.x / l;
    out.y = v.y / l;
    return out;
  }

  static length(v){
    return Math.sqrt(v.x*v.x + v.y*v.y);
  }

  static hermite_(v1, a1, v2, a2,t)
  {
    const a = 2. * (v1 - v2) + (a1 + a2);
    const b = 3. * (v2 - v1) - (2. * a1) - a2;
    let r = a;
    r *= t;
    r += b;
    r *= t;
    r += a1;
    r *= t;
    return r + v1;
  }

  static hermite(v1,a1,v2,a2,t,out = new Vec2()){
    out.x = Vec2.hermite_(v1.x,a1.x,v2.x,a2.x,t);
    out.y = Vec2.hermite_(v1.y,a1.y,v2.y,a2.y,t);
    return out; 
  }
}

export class NodePoint
{
  constructor(p = new Vec2(),t = new Vec2(),speed = 0){
    this.p = p;
    this.t = t;
    this.speed = speed;
  }
}

export class Section {
  constructor(accel = 0,dist = 0){
    this.accel = accel;
    this.dist = dist;
  }
}

export class CurveStatus {
  constructor(secNo = 0,dist = 0,speed = 0.5){
    this.secNo = secNo;
    this.dist = dist;
    this.speed = speed;
  }
}

export class Curve {
  constructor(){
    this.points = [];
    this.sections = [];
    this.buildNum = 1;
  }

  calcStartVelocity(){
    let t = new Vec2();
    const points = this.points;

    Vec2.div(Vec2.mul(Vec2.sub(points[1].p, points[0].p,t),3.0,t),this.sections[0].dist,t);
    points[0].t = Vec2.mul(Vec2.sub(t,points[1].t,t),0.5,t);

  }

  calcEndVelocity(){
    let t = new Vec2();
    const i = this.buildNum - 1;
    const points = this.points;
    Vec2.div(Vec2.mul(Vec2.sub(points[i].p, points[i - 1].p,t),3.0,t),this.sections[i - 1].dist,t);
    points[i].t = Vec2.mul(Vec2.sub(t,points[i - 1].t,t),0.5,t);
  }

  getPoint(index){
    return this.points[index];
  }

  getSection(index){
    return this.sections[index];
  }

}

export class UniCurve extends Curve {

  constructor(points){
    super();
    if(points){
      this.points = points;
      this.build();
    }
  }

  addPoint(v){
    this.points.push(new NodePoint(v));
    this.build();
  }

  build(){
    if(this.points.length < 3){
      return;
    }
    let v1 = new Vec2(),v2 = new Vec2(),
    vT = new Vec2(),vT1 = new Vec2(),
    vT2 = new Vec2();

    const points = this.points;
    const sections = this.sections;
    for(let i = this.buildNum,e = points.length;i < e;++i)
    {
      Vec2.sub(points[i-1].p,points[i].p,v1);
      if(i < (e - 1)){
        Vec2.sub(points[i+1].p,points[i].p,v2);
        Vec2.normalize(v1,vT1);
        Vec2.normalize(v2,vT2);
        Vec2.sub(vT2,vT1,vT);
        Vec2.normalize(vT,points[i].t);
      }
      !sections[i-1] && (sections[i-1] = new Section()); 
      sections[i-1].dist = Vec2.length(v1);
    }

    if(this.buildNum == 1){
      this.calcStartVelocity();
    }
    this.buildNum = points.length;
    this.calcEndVelocity();
  }

  getPosition(status,elapsedTime,out = new Vec2()){
    let dist = 0,i = status.secNo;
    const points = this.points;
    if(i > points.length || points.length < 3){
      return false;
    }
    const sections = this.sections;
    const section1 = sections[i];
    const nodePoint1 = points[i] ;
    const nodePoint2 = points[i + 1];
    dist = status.speed * elapsedTime;

    if((status.dist + dist) > section1.dist){
      if(i < (points.length - 2)){
        const section2 = sections[i + 1];
        const nodePoint3 = points[i + 2];
        let time = 0,dist2 = 0;
        time = elapsedTime - (section1.dist - status.dist) / status.speed;
        dist2 = status.speed * time;
        status.dist = dist2;
        status.secNo++;
        const u = status.dist / section2.dist;
        const vel1 = Vec2.mul(nodePoint2.t,section2.dist);
        const vel2 = Vec2.mul(nodePoint3.t,section2.dist);
        Vec2.hermite(nodePoint2.p,vel1,nodePoint3.p,vel2,u);
      } else {
        out = points[points.length - 1].p;
        status.dist = 0;
        status.secNo = 0;
        return false;
      }
    } else {
      status.dist += dist;
      const u = status.dist / section1.dist;
      const vel1 = Vec2.mul(nodePoint1.t,section1.dist);
      const vel2 = Vec2.mul(nodePoint2.t,section1.dist);
      Vec2.hermite(nodePoint1.p,vel1,nodePoint2.p,vel2,u,out);
    }
    return true;
  }
}

なんとなく車輪の再発明感ただようコードになっているが。。

これを使って、スプライン曲線を描くデモを作った。

https://sfpgmr.github.io/images/2017/10/2017101501.png

下で実際に動かして試すことができる。Alt+左クリックで点を追加、Ctrl+左クリックで点と点の間に点を追加、点にマウスカーソルを合わせて右クリックで点の削除、点をドラッグすると移動ができる。

これを使って敵の動きのバリエーションを増やしていこうと思っている。

動作サンプル

新しいウィンドウで開く

ソースコード・リソース

/dev/2dshooting/devver/20171015/css/style.css

/dev/2dshooting/devver/20171015/index.html

/dev/2dshooting/devver/20171015/js/bundle.js

/dev/2dshooting/devver/20171015/js/dsp.js