ラベル VectorRumble の投稿を表示しています。 すべての投稿を表示
ラベル VectorRumble の投稿を表示しています。 すべての投稿を表示

アイテムと武器の変更

0 コメント


難しいところが出てきて、ちょっとやる気が減ってたというのもちょこっとあるけど、仕事が忙しかったのでなかなか進まなかった。

PowerUp          アイテムのActor、これを継承する
DoubleLaserPowerUp  武器強化のアイテム
DoubleLaserWeapon   弾2個分 同時に生成する武器

単純に弾を2個同時につくっているのにびっくりした。 さらなる弾強化の3つ同時も同様にやる。
貫通レーザーとかも、当たった時に消さずにそのまま残すだけでできそう。

この武器変更の仕組みはシューティングで使いまわせそう。


ここまでのプロジェクト(205K)

アステロイド追加

0 コメント

アステロイドActorを追加。
新しい種類のActorの追加のやり方がわかる。これで、敵の種類が増えても大丈夫。
追加の際に、重ならないように出現場所をWorldクラスのFindSpawnPoint()であいてる場所を探す。
衝突判定後、AdjustVelocities()でActorの速度を調整する。
しかしこれが難しくてわからない。
円の衝突をモデル化すると、衝突時、ぶつかり合う円の中心同士を結んだベクトルがあり、衝突面は、このベクトルに直行したものになる。衝突面に対して、入射角度と反射角度は同じになるので、衝突面方向、中心同士を結んだ方向に速度の成分を分けて考える。というぐらいまでで、どうしてこういう計算になるんだろうか?

ここはとりあえずわからないので飛ばす。やっぱりわからんとこ出てきたなあ。

ここまでのプロジェクト(205KB)

private static void AdjustVelocities(Actor actor1, Actor actor2) {
 if(actor1.Mass <= 0f || actor2.Mass <= 0f) {
  return;
 }
 Vector2 collisionNormal = Vector2.Normalize(actor2.Position - actor1.Position);
 Vector2 collisionTangent = new Vector2(-collisionNormal.Y, collisionNormal.X);

 float velocityNormal1 = Vector2.Dot(actor1.Velocity, collisionNormal);
 float velocityTangent1 = Vector2.Dot(actor1.Velocity, collisionNormal);
 float velocityNormal2 = Vector2.Dot(actor2.Velocity, collisionNormal);
 float velocityTangent2 = Vector2.Dot(actor2.Velocity, collisionTangent);

 float velocityNormal1New = ((velocityNormal1 * (actor1.Mass - actor2.Mass))
  + (2f * actor2.Mass * velocityNormal2)) / (actor1.Mass + actor2.Mass);
 float velocityNormal2New = ((velocityNormal2 * (actor2.Mass - actor1.Mass))
  + (2f * actor1.Mass * velocityNormal1)) / (actor1.Mass + actor2.Mass);
 actor1.Velocity = (velocityNormal1New * collisionNormal) +(velocityTangent1 * collisionTangent);
 actor2.Velocity = (velocityNormal2New * collisionNormal) +(velocityTangent2 * collisionTangent);
}

当たり判定 線分と円

0 コメント
Actorの持っている円形の領域と線分との当たり判定

学校で習ったベクトルがようやく役にたつとき!



基本的な考え方は、線分ABと点Cを中心とした円を考える。点C線分ABにおろした垂線が、円の中心との最短距離になる。こいつと円の半径を比較して、半径の方が小さかったら当たってない、大きかったらあたっていることになる。また、垂線との交点Hが線分ABの間にないときは、最短距離が、ACになったりBCになったりする。
こんな感じ




この場合はHCよりもACの方が短い。

問題はHCの長さを求める計算だけど、まず点Hの場所をベクトルを使って求める。
ベクトルAHはベクトルABをt倍したものとできるので、tを求めれば場所がわかる。




 という式の変形でtが求められる。
public static bool CircleLineCollide(Vector2 center, float radius,Vector2 lineStart, Vector2 lineEnd, ref CircleLineCollisionResult result) {

 Vector2 AC = center - lineStart;
 Vector2 AB = lineEnd - lineStart;
 float ab2 = AB.LengthSquared();
 if(ab2 <= 0f) {
  return false;
 }
 float acab = Vector2.Dot(AC, AB);
 float t = acab / ab2;

tが負の場合、または1以上の場合は線分ABからはみ出てるので調整する
if(t < 0.0f) {
  t = 0.0f;//点Hが点Aの場所になる
 } else if(t > 1.0f) {
  t = 1.0f;//点Hが点Bの場所になる
 } 

交点は当たり判定通知用の構造体、CircleLineCollisionResultにしまう。

円の半径と、距離を比較して結果を出す。

result.Point = lineStart + t * AB;
 result.Normal = center - result.Point;

 float h2 = result.Normal.LengthSquared();
 float r2 = radius * radius;
 if(h2 > r2) {
  result.Collision = false;
 } else {
  result.Normal.Normalize();
  result.Distance = (radius - (center - result.Point).Length());
  result.Collision = true;
 }

 return result.Collision;
} 
 
きれいに書いてみたかったのでTEXを使ってみた。環境をつくるのがめんどい。
図はGRAPESを使ってみました。使い方が独特だけど、簡単に図形がかけてよい感じ。

弾の発射 Actorの動的な追加

0 コメント


Projectile 射出体(弾)のActor
LaserProjectile 通常弾の実装
Weapon 武器を表す Shipが持っている
LaserWeapon 通常弾の実装

このへんがちょっとわかりにくい。
ProjectileはActorなのでGamePlayScreenが自機と同じようにUpdate()とかDraw()を呼び出してくれる。他のActorと当たった時の処理もここ。
WeaponはUpdate()やDraw()にかかわらない部分、弾の発射間隔のカウントとかをする。 同時発射数が決まってるとかだったらここでやるのだろうか?

だんだん要素が増えてきてしんどい。

ここまでのプロジェクト(190KB)

当たり判定 2線分の交点

0 コメント
Collisionに実装された当たり判定
一生使いまわせる

2線分の交点。
厚みがない壁などは突き抜けてしまったりするので、移動しているそれぞれのフレームでの移動量(速度)のベクトルと、壁との当たり判定を調べる。

それぞれの線分の始点、終点をVector2で渡す。交差してる場合は交点を戻す。outはC#の参照渡し。
式を計算を進めて変形して実装している。
詳しい計算式はこちら

public static bool LineLineIntersect(Vector2 a, Vector2 b,Vector2 c, Vector2 d, out Vector2 point) {
 point = Vector2.Zero;

 double r, s;
 double denominator = (b.X - a.X) * (d.Y - c.Y) - (b.Y - a.Y)*(d.X - c.X);

 //分母が0の場合平行
 if(denominator == 0) {
  return false;
 }

 double numeratorR = (a.Y - c.Y) * (d.X - c.X) - (a.X - c.X)*(d.Y - c.Y);
 r = numeratorR / denominator;

 double numeratorS = (a.Y - c.Y)*(b.X - a.X) - (a.X - c.X) * (b.Y - a.Y);
 s = numeratorS / denominator;

 //交差しない
 if(r < 0 || r > 1 || s<0 || s > 1) {
  return false;
 }

 point.X = (float)(a.X + (r * (b.X - a.X)));
 point.Y = (float)(a.Y + (r * (b.Y - a.Y)));
 return true;
}

壁との当たり判定2

0 コメント
ちょっと考え方が違ってた。

Actorとの当たり判定と壁との当たり判定を分けているだけだった。
壁は線で定義されているので当たり判定をするとき、後で出てくる弾(Projectile)は動きがはやいので突き抜けてしまったりするのでLineLineIntersect()で、それ以外はCircleLineCollide()で計算する。
当たり判定が起きた時に何かしたいときは、Touch()が呼ばれるように書く。
その後、Touch()を継承する。当たった相手を引数にしてもよいが特に今は何にも使われていない。


private void MoveWorld(float elapsedTime) {
 Vector2 point = Vector2.Zero;

 for(int i = 0; i < actors.Count; ++i) {
  if(actors[i].Dead) {
   continue;
  }
 Vector2 movement = actors[i].Velocity * elapsedTime;
 if(actors[i].CollidedThisFrame == false) {
  //Actorと
  movement = MoveAndCollide(actors[i], movement);
 }
 actors[i].Position += movement;

 //壁と
 for(int w = 0; w < walls.Length / 2; ++w) {
  if(actors[i] is Projectile) {
   if(Collision.LineLineIntersect(actors[i].Position,
    actors[i].Position - movement, walls[w * 2],walls[w * 2 + 1], out point)) {
    actors[i].Touch(actors[0]);
   }
  } else {
   Collision.CircleLineCollisionResult result = new Collision.CircleLineCollisionResult();
   if(Collision.CircleLineCollide(actors[i].Position,actors[i].Radius, walls[w * 2], walls[w * 2 + 1],ref result)) {
    float vn = Vector2.Dot(actors[i].Velocity, result.Normal);
    actors[i].Velocity -= (2.0f * vn) * result.Normal;
    actors[i].Position += result.Normal * result.Distance;
   }
 }
  }
 }
}

壁との当たり判定

0 コメント

壁と、それに対しての当たり判定の追加。
中学生ぐらいのベクトルがようやく役に立った。というか忘れてた。
新しく追加したクラスは
Collision 当たり判定をする staticなのでオブジェクトを作るわけではない
CollisionResult 当たり判定の結果のオブジェクト

for(int w = 0; w < walls.Length / 2; ++w) {
 Collision.CircleLineCollisionResult result = new collision.CircleLineCollisionResult();
 if(Collision.CircleLineCollide(actors[i].Position,actors[i].Radius, walls[w * 2], walls[w * 2 + 1],ref result)) {
  float vn = Vector2.Dot(actors[i].Velocity, result.Normal);
  actors[i].Velocity -= (2.0f * vn) * result.Normal;
  actors[i].Position += result.Normal * result.Distance;
 }
}

resultに当たり判定の結果や、方向が入っているのでそれを使って位置を修正する。

ここまでのプロジェクト(170K)

Shipの動きの入力

0 コメント
ベクトルが入ってきて難しくなってきたけど2Dならなんとか。


Vector2 forward = new Vector2((float)Math.Sin(Rotation),-(float)Math.Cos(Rotation));
Vector2 right = new Vector2(-forward.Y, forward.X);
Actorオブジェクトが持っているプロパティーRotation(角度)を使って。
Y軸の正方向に向かって Vector2 forward
それに直行する右向きの Vector2 right

スティックを動かしていないときは処理をしない。


float angleDiff = (float)Math.Acos(Vector2.Dot(wantedForward, forward));
内積の式の変形より、スティックで入力された角度と、現在のforwardとのなす角を出す。
ベクトルが正規化されているので、計算が簡単になるところがミソ。


float facing = (Vector2.Dot(wantedForward, right) > 0f) ? 1f : -1f;
内積の符号を調べることで、なす角が鋭角か鈍角かを調べることができる。
(符号が正のとき 鋭角、符号が負のとき 鈍角)
この場合、現在の向きに対して直行する右向きのベクトルと、スティックで入力された向き(ベクトル)とを比較している。 よって鋭角の場合は右方向への変化、鈍角のときは左方向への変化になる。

if(angleDiff > 0f) {
    Rotation += Math.Min(angleDiff, facing * elapsedTime * rotationRadiansPerSecond);
   }
   Velocity += leftStick * (elapsedTime * speed);
   if(Velocity.Length() > velocityLengthMaximum) {
    Velocity = Vector2.Normalize(Velocity) * velocityLengthMaximum;
   } 
angleDiffの範囲は0からπまで、0は動いていないのでそれを省く 
 
1秒当たりの向きを変えられる量、速度など1秒当たりで決める。
elapsedTimeは経過時間なので掛けることで移動量などが計算できる。 

ゲーム画面

0 コメント

メニューばかりで飽きたので、ゲーム画面を作りだす。
ここでも基本構造を作らなくては、画面に表示されないので結構しんどかった。
新しく作ったクラスは
LineBatch    画面表示に関するもの
VectorPolygon 自機の形などの形の情報
GamePlayScreen  GameScreenの継承
Actor  動くものの
Ship   Actorを継承、自機
World  ゲームのステージ、動くものを管理する

前と同じでGamePlayScreenのUpdateでWorldをUpdate
WorldはすべてのActorをUpdateする。
MoveWorldと分けているのは今後当たり判定を入れた時に、現在の位置と速度などから当たり判定をして障害物などがあったら、変更するため。
ActorのUpdateはゲームパッドの入力から、速度を更新する。
その他、生きてる死んでるの状態から、自機を追加したりなどの細かいこと 。

構造は単純だけどやっぱり難しい。

これまでのプロジェクト(170KB)





MenuEntry描画を変更

0 コメント

より、オブジェクト指向を進め、MenuEntryの更新、描画のためのメソッドを作成、MenuScreen内で、それらを呼び出す形に変更した。これで、更新描画、ともに
for(int i = 0; i < menuEntries.Count; i++) {}
のかたちで書くことができる。
しかし、描画位置の計算が、MenuScreen側で大まかな位置を決定、MenuEntry側でscaleに合わせて位置を調整とちょっと冗長?な感じになった。
しかし、それぞれのMenuEntryで新たな変数selectionFadeを持てるので、選択状態から外れた瞬間徐々に大きさを戻すというような表現ができるのでよりリッチになった。

ここまでのプロジェクト(138KB)

GameScreenのUpdate

0 コメント

これまではGameSceenが動的に変化することがなかった。リッチなインターフェースを作るにはアニメーション効果が必要になる。Update()内で表示させる場所などを変化させる。

特定時間をかけて0から1まで変化するものをつくりアニメーションを作成する。





--
bool UpdateTransition(GameTime gameTime, TimeSpan time, int direction) {
 float transitionDelta;
 if(time == TimeSpan.Zero) {
  transitionDelta = 1;
 } else {
 transitionDelta = (float)(gameTime.ElapsedGameTime.TotalMilliseconds /  time.TotalMilliseconds);
 }

 transitionPosition += transitionDelta * direction;

 if((transitionPosition <= 0) || (transitionPosition >= 1)) {
  transitionPosition = MathHelper.Clamp(transitionPosition, 0, 1);
                
  return false;
 }
 return true;
}
これによって計算されたプロパティTransitionPositionを表示位置に適用する。
ScreenStateにTransitionOn(表示が現れる)とTransitionOff(表示が消えゆく)が追加された。GameScreenの最初の状態はTransitionOnで指定された長さのアニメーションが終わったら(上の関数がfalseを返したら)状態が変化する。
ScreenManager内のUpdateでScreenStateの状態を確認して、重ねて画面を表示させるかどうか確認する。
また、文字の透明度をTransitionPositionを利用して変化させる。

最初に追加したMainMenuScreenのオブジェクトはずっと残っているので注意。





ここまでのプロジェクト(135KB)

MenuScreenの継承

0 コメント


これまではabstractであるMenuScreenを継承してMainManuScreenを実装していた。
実装を変えれば、別のメニューが作れるということは想像できる。
別の実装のOptionsMenuScreenを作って、MainManuScreen内のOptionのMenuEntryを選択したイベント内で、ScreenManagerにaddScreen()した。
このままでは二つのGameScreenが重なって表示されてしまう。
GameScreenにIsPopupプロパティーを設けて、ポップアップタイプのGameScreenでないときは重なって表示されないように、 ScreenManagerのUpdate()を変更した。


while(screensToUpdate.Count > 0) {
//GameScreenを後ろから順番に参照する
 GameScreen screen = screensToUpdate[screensToUpdate.Count - 1];
 screensToUpdate.RemoveAt(screensToUpdate.Count - 1);

 screen.Update(gameTime, otherScreenHasFocus, coveredByOtherScreen);

 if(!otherScreenHasFocus) {
  screen.HandleInput(input);
  otherScreenHasFocus = true;
 }
//もしGameScreenがポップアップタイプでなかったら、次のスクリーンは coveredByOthierScreen=trueになる。(覆い隠されている)
 if(!screen.IsPopup) {
  coveredByOtherScreen = true;
 }
}



ここまでのプロジェクト(133KB)

VectorRumbleメニュー部分

0 コメント

ここまでのプロジェクト(129K)


ScreenManager
GameScreen
InputState
は、デザインパターン的な扱いになると思う。
これらは使いまわすことができそう。

VectorRumbleをよむ

0 コメント
ミニゲームなんだけど、これでも要素が多いし、難しいので、最小の要素まで削って、新規作成した自分のプロジェクトに内容を理解しながら移し替えていく(コピペにあらず)作業をすることにした。
すごく時間と手間がかかるが、実際自分で作る時も、一人だとすこしづつビルドして動いているのを確認できないと、モチベーションがもたない。