はじめに
前回の記事でキャラクターをその場で足踏みさせることができたので、今度は十字キーで動かせるようにします
動きの仕様
- キーを押している間にそのキーの方向へ移動
- 斜め移動の時の向きは、先に押したキーの向きとする(左上への斜め移動の時、左→上と押したら左向とする)
- キーを離した時、最後に向いていた方向でニュートラルの姿勢になる
- 斜め移動と縦や横の移動の速さは同じとする
1. キーを押している間にそのキーの方向へ移動
これは十字キーを押している間に、キャラクターの表示位置を押したキーに沿って動かしてやれば良いです。
2. 斜め移動の時の向きは、先に押したキーの向きとする
保持している矢印キー情報がない場合で、最初に押されたキー情報を保持しておき、そのキーが離されたら保持していたキー情報をリリースする。
そうすることで、上→左 と押して左上に移動するときは上向きという情報が保持され、左→上通して左上に移動するときは左向きという情報が保持される。
3. キーを離したとき、最後に向いていた方向でニュートラルの姿勢になる
2番の仕様に関連して、キーを離して保持していたキー情報をリリースするときに「リリースされたキー情報」を保持することで、キーを全て離してキャラクターの動きが止まった瞬間の向き情報を保存することができる。
4. 斜め移動と縦や横の移動の速さは同じとする
今回の実装の肝。
キャラクターの速度を1としたとき、単純に十字キーに連動して縦横の移動速度を1にしてしまうと、斜め入力の時の速度が√2(=1.41421356)となり、斜め移動が縦横のみの移動に比べて早くなってしまう。
なので、キャラクターの速度ベクトルと移動方向回転行列から移動ベクトルを算出することにする。
こうすることで、将来的にゲームパッドなどでスティックからキャラクターの移動方向の命令が来たときに簡単に移動方向の計算ができるようになる。
速度ベクトル
速度のスカラーをvとして、以下のベクトルを定義
\begin{pmatrix}
v \\
0
\end{pmatrix}
移動方向回転行列
X軸に+方向を0度として、そこからの回転角度をθとして以下の2次元回転行列を定義
\begin{pmatrix}
cosθ & -sinθ \\
sinθ & cosθ
\end{pmatrix}
移動ベクトル
移動方向回転行列と速度ベクトルの内積を取ることで、「速度ベクトルをθだけ回転させたベクトル、すなわち移動方向の成分をもった移動ベクトル」が生成される
\begin{pmatrix}
cosθ & -sinθ \\
sinθ & cosθ
\end{pmatrix}
\begin{pmatrix}
v \\
0
\end{pmatrix}
=
\begin{pmatrix}
v \times cosθ\\
v \times sinθ
\end{pmatrix}
完成したコード
過去の自分の記事で作成した画像を簡単に表示するパッケージ使ってます
package main
import (
"fmt"
"math"
"log"
"image"
_ "image/png"
"github.com/hajimehoshi/ebiten/v2"
"github.com/hajimehoshi/ebiten/v2/ebitenutil"
"github.com/hajimehoshi/ebiten/v2/inpututil"
"github.com/username/projectname/src/picture" // 画像を簡単に表示させるオリジナルパッケージ
// 行列を使うライブラリ
"gonum.org/v1/gonum/mat"
)
const (
frameOX = 0 // フレーム開始時点のX座標
frameOY = 0 // フレーム開始時点のY座標
frameWidth = 32 // 1フレームで表示する横幅
frameHeight = 32 // 1フレームで表示する縦幅
frameNum = 4 // 表示させる画像の数
)
// 基準速度
var basic_velocity float64 = 2
var velocity_array = []float64{basic_velocity,0} // 速度ベクトル生成用の行列を定義
var velocity_vector = mat.NewDense(2, 1, velocity_array) // 速度ベクトル(2行1列の行列)を生成 X軸の正方向にbasic_velocityの大きさを持つベクトル
// 移動方向回転行列 デフォルトでは下向き
var basic_angle float64 = -math.Pi /2
var basic_posture_array = []float64{math.Cos(basic_angle), - math.Sin(basic_angle), math.Sin(basic_angle), math.Cos(basic_angle)}
var posture_rotate_matrix = mat.NewDense(2,2,basic_posture_array) // 移動方向回転行列(2行2列)を生成
// キャラクターの初期位置
var positionX float64 = 100
var positionY float64 = 100
// キャラクターの向き 0:下 1:左 2:右 3:上 これは読み込む画像による
var character_direction int = 0
// 斜めの時キャラクターの向きを決める変数 0:下 1:左 2:右 3:上 4:未入力
var pushed_arrow_key_num = 4
var released_arrow_key_num = 0
var pushed_arrow_key ebiten.Key
// 向きを保存した配列
var arrow_key_array[4] ebiten.Key = [4]ebiten.Key {ebiten.KeyDown, ebiten.KeyLeft, ebiten.KeyRight, ebiten.KeyUp}
// キャラクター用の変数宣言
var character *ebiten.Image
// 最初に画像を読み込む
func init() {
var err error
character, _, err = ebitenutil.NewImageFromFile("character.png")
if err != nil {
log.Fatal(err)
}
}
type Game struct{
count int
}
func (g *Game) Update() error{
// 毎フレーム毎にgを増加させていく
g.count++
return nil
}
func(g *Game) Draw(screen *ebiten.Image){
// 斜め移動時の向きの優先度を保存
for i, v := range arrow_key_array{
if pressedKey(v) {
savePressedArrowKey(v, i)
}
}
// どの方向を向くか決定する
if(pushed_arrow_key_num != 4){
character_direction = pushed_arrow_key_num
} else {
character_direction = released_arrow_key_num
}
// 優先度が高いキーが離されたかを監視
checkReleasedArrowKey(pushed_arrow_key, pushed_arrow_key_num)
// 基準速度を現在の速度に代入
current_velocity := basic_velocity
// 入力したキーで移動方向の角度と速度を決定
if pressedKey(ebiten.KeyArrowUp){
if pressedKey(ebiten.KeyArrowRight){
// 右上の時 -45度
basic_angle = -math.Pi /4
} else if pressedKey(ebiten.KeyArrowLeft){
// 左上の時 -135度
basic_angle = -math.Pi / 4 * 3
} else{
// 上の時 -90度
basic_angle = -math.Pi /2
}
if pressedKey(ebiten.KeyArrowDown){
// 上下キーを同時押しした時、その場にとどまらせる
current_velocity = 0
}
} else if pressedKey(ebiten.KeyArrowDown){
if pressedKey(ebiten.KeyArrowRight){
// 右下の時 45度
basic_angle = math.Pi /4
} else if pressedKey(ebiten.KeyArrowLeft){
// 左下の時 135度
basic_angle = math.Pi / 4 * 3
} else {
// 下の時 90度
basic_angle = math.Pi /2
}
} else if pressedKey(ebiten.KeyArrowRight){
// 右の時 0度
basic_angle = 0
if pressedKey(ebiten.KeyArrowLeft){
// 左右キーを同時押しした時、その場に留まらせる
current_velocity = 0
}
} else if pressedKey(ebiten.KeyArrowLeft){
// 左の時 180度
basic_angle = math.Pi
} else {
// 十字キーの入力がない時に、現在の速度を0にする
current_velocity = 0
}
// 現在の入力に合わせた移動方向回転行列を更新
posture_rotate_matrix.Set(0,0,math.Cos(basic_angle))
posture_rotate_matrix.Set(0,1,-math.Sin(basic_angle))
posture_rotate_matrix.Set(1,0,math.Sin(basic_angle))
posture_rotate_matrix.Set(1,1,math.Cos(basic_angle))
// 速度ベクトルの成分を更新
velocity_vector.Set(0,0,current_velocity)
// 移動ベクトルを移動方向回転行列と速度ベクトルから計算
move_vector := mat.NewDense(2, 1, nil)
move_vector.Product(posture_rotate_matrix, velocity_vector)
// 13フレームに一度画像を更新する
i := (g.count / 13) % frameNum
// i が3 すなわち右手→ニュートラル→左手→ここ の時にニュートラルを表示させる
if i == 3 {
i = 1
}
// 現在の速度が0の時、ニュートラルの姿勢にする
if current_velocity == 0 {
i = 1
}
sx, sy := frameOX + i * frameWidth, frameOY + 32 * character_direction
// 表示する画像を切り出す
var this_frame_img *ebiten.Image = character.SubImage(image.Rect(sx, sy, sx + frameWidth, sy + frameHeight)).(*ebiten.Image)
// キャラクターの表示位置をmove_vectorの分だけ移動させる
positionX += move_vector.At(0,0)
positionY += move_vector.At(1,0)
// 画像を表示させる
picture.Show(screen, this_frame_img, 1, positionX, positionY,0)
}
func (g *Game) Layout(outsideWidth, outsideHeight int) (screenWidth, screenHeight int) {
return 320, 240
}
func main() {
ebiten.SetWindowSize(640, 480)
ebiten.SetWindowTitle("walk")
if err := ebiten.RunGame(&Game{}); err != nil {
log.Fatal(err)
}
}
// 行列を標準出力する
func matPrint(X mat.Matrix) {
fa := mat.Formatted(X, mat.Prefix(""), mat.Squeeze())
fmt.Printf("%v\n", fa)
}
// 特定のキーが押されているかをチェックする
func pressedKey(str ebiten.Key) bool{
inputArray := inpututil.PressedKeys()
for _, v := range inputArray{
if v == str{
return true
}
}
return false
}
// 優先的に押されたキーを保持する
func savePressedArrowKey(key ebiten.Key, key_num int) {
if pushed_arrow_key_num == 4 {
pushed_arrow_key = key
pushed_arrow_key_num = key_num
}
}
// 優先的に押されていたキーが離されたかどうかをチェックする
func checkReleasedArrowKey(key ebiten.Key, key_num int) {
if inpututil.IsKeyJustReleased(key) {
pushed_arrow_key_num = 4
released_arrow_key_num = key_num
}
}
完成した動き
gyazoがフリープランなので長い動画撮れないですが、仕様を全て満たせました
最後に
もっとリファクタリングできる気がする(特に角度決めるところ)