Goでゲーム制作_準備
ゲームを作ろうと思ったきっかけ
Paizaでプログラミング問題を解いたこと
よし、ゲーム作ろ
— ふじた@週末プログラマ (@weekend_fuji) 2020年4月23日
時代はゲームだよ pic.twitter.com/cSyPOHRMVR
がきっかけです。 もともとゲームは好きで制作にも興味があったのですが、改めて作りたくなった感じです。
まずは準備
ゲームを作るのは初めてなので、どのように作るのが良いのか少し調べてみました。 すると以下の3点を主に注意したほうがいいことがわかりました。
気を付けるべきこと3点
いきなり大作を作ろうとしない!!
ゲーム作りの初心者が陥るポイントNo.1だそうです。
自分のアイディアを形にしてゲームを作ろうというくらいですから、もちろんゲームへの愛は人一倍。そんな人だからこと「こんなゲームを作りたい!」「このアイディアはいける!!」という熱い情熱をもって開発を始めるのですが、、、個人でのゲーム作りは情熱だけでは乗り越えられない「工数の限界」という壁があります。
一説では、ゲームを製作するためにかかる時間は
必要製作時間 = 予想プレイ時間 × 100
だそうです!
これはある程度、自分のアイディアの大きさと突っ込める工数の間で調整が必要ですね。
可能な限りフリー素材を使う!!(もちろん権利には注意しよう!!)
これは一つ目の注意点と強く関連するのですが、ゲームを作るために勉強が必要なことは非常にたくさんあります。
プログラミング(フラグ管理、レベルシステムなど含む)、シナリオ作成、ゲーム音楽作成、イラスト作成などなど。これらすべてを自前で揃えようとすると、おそらく個人では作るのは難しいのではないでしょうか。すべて自分の手作りが、個人製作のゲームとしては理想の形なのかもしれませんが、万能の天才にしか作れない難易度になってしまいます。
そのため、ゲームの個人製作では権利関係に十分に注意を払いつつ積極的にフリー素材やライブラリを採用する姿勢が、必要となります。
完璧を目指さない!!
何百人体制で開発された販売されているゲームでさえバグの一つや二つは必ずあります。いわんや個人製作をや、ですね。
人に迷惑をかけない範囲であれば、個人のゲーム制作では、まずは動くものを作って政策自体を楽しむのもありだと思います。
そのほうがモチベーションが保ちやすいようですね。
今回作ったもの
今回はfyne.ioライブラリ(pure golangのGUIアプリ製作用ライブラリ)の調査もかねて簡単なピンポンゲームを作りました。はてぶに挙げることができるGIF画像のサイズを超えないようにするためにフレームレートを落としたのでカクカクですが、、、
goroutineの挙動の勉強も兼ねています。
反省点は以下の二つです。
反省点
当たり判定
当たり判定ムズイ!!当たり判定は、細かく見ていかないといけないですね。
得点の表示がおかしい
画面左下に現在取得しているポイント数を表示しています。しかし、原因はまだ分かっていませんが、数値の表示が不安定になっています。
ログを見る限りでは、ポイント数の計算自体は間違っておらず正常に行われているように見えますが、、、さらなる調査が必要な部分です。
ソースコード
package main import ( "fmt" "image/color" "log" "time" "fyne.io/fyne/widget" "fyne.io/fyne/layout" "fyne.io/fyne" "fyne.io/fyne/app" "fyne.io/fyne/canvas" ) func main() { myApp := app.New() w := myApp.NewWindow("Ball and Board is not Bored!!") w.Resize(fyne.NewSize(800, 600)) // ボールの生成 circle := canvas.NewCircle(color.White) circle.StrokeWidth = 5 // 板の生成 board := canvas.NewLine(color.White) board.StrokeWidth = 5 cont := fyne.NewContainer(board) // ポイント表示枠の生成 pointBox := widget.NewLabel("00 pts") pboxCon := fyne.NewContainer(pointBox) content := fyne.NewContainerWithLayout(layout.NewFixedGridLayout(fyne.NewSize(50, 50)), circle, cont, pboxCon) w.SetContent(content) log.Println(circle.Position1, circle.Position2) go func() { board.Position1 = fyne.NewPos(250, 550) board.Position2 = fyne.NewPos(350, 550) board.Refresh() pointBox.Move(fyne.NewPos(0, w.Canvas().Size().Height-50)) pointBox.Refresh() // キーボードからの入力に応じて反射板を移動させる go func() { for { w.Canvas().SetOnTypedKey(func(ev *fyne.KeyEvent) { log.Printf("key=%s\n", string(ev.Name)) if ev.Name == fyne.KeyRight { board.Move(fyne.NewPos(board.Position().X+15, board.Position().Y)) } if ev.Name == fyne.KeyLeft { board.Move(fyne.NewPos(board.Position().X-15, board.Position().Y)) } }) canvas.Refresh(board) } }() diffX := 1 diffY := 1 point := 0 // ボールを 1 count/msで移動する for { time.Sleep(time.Millisecond) x := circle.Position().X + diffX y := circle.Position().Y + diffY circle.Move(fyne.NewPos(x, y)) if x < 0 || w.Canvas().Size().Width < x+50 { diffX *= -1 } if y < 0 || w.Canvas().Size().Height < y+50 { diffY *= -1 } centerX := (circle.Position1.X + circle.Position2.X) / 2 if diffY > 0 && board.Position1.X <= centerX && centerX <= board.Position2.X && board.Position().Y <= circle.Position2.Y { diffY *= -1 point++ pointBox.SetText(fmt.Sprintf("%d pts", point)) canvas.Refresh(pointBox) log.Printf("Ball (x1, y1)=(%d, %d), (x2, y2)=(%d, %d)\n", circle.Position1.X, circle.Position1.Y, circle.Position2.X, circle.Position2.Y) log.Printf("Ball Center (x, y)=(%d, %d)\n", circle.Position().X+25, circle.Position().Y+25) log.Printf("Board (x1, y1)=(%d, %d), (x2, y2)=(%d, %d)\n", board.Position1.X, board.Position1.Y, board.Position2.X, board.Position2.Y) log.Printf("Point:%0d\n", point) } canvas.Refresh(circle) } }() w.ShowAndRun() }
まとめ
今回簡単なゲームを作ってみました。いつものプログラミングが上手くいった時の高揚感とは、また違った達成感がありました。非常に楽しかったです。
次回以降の記事では、プログラミングのことだけでなくゲーム用に制作した画像なども挙げていきたいと考えています。
JavaでHello World
どうも皆さんこんにちは!週末プログラマのふじたです。
今回はこのブログの初投稿でございますので、Hello Worldを書いてみました。
動いているところ
ソース
HelloWorldMain.java
public class HelloWorldMain { public static void main(String[] args) { Logo logo = new Logo("../data/HelloWorld.txt"); logo.printLogo(); } }
Logo.java
import java.io.BufferedReader; import java.io.FileReader; import java.util.ArrayList; import java.util.List; import java.util.Random; public class Logo { /**ロゴの各行の文字列を保持する配列 */ private List<LogoLine> logoLines; /** * 引数としてロゴファイルパスを渡し、ロゴを初期化する * @param path */ public Logo(String path){ try (FileReader fis = new FileReader(path); BufferedReader bis = new BufferedReader(fis)){ this.logoLines = new ArrayList<>(); String line = bis.readLine(); int i = 1; while (line != null) { this.logoLines.add(new LogoLine(i, line)); line = bis.readLine(); i++; } } catch (Exception e) { e.printStackTrace(); } } /** * ロゴをコンソール画面に出力する */ public void printLogo(){ // コンソール画面を初期化 System.out.print("\033[2J"); int lineSize = this.logoLines.size(); long sleepTime = 1000; Random rnd = new Random(); // ロゴ出力処理 while (!isAllLineWrited()) { // ランダム行選択 int lineNum = Math.abs(rnd.nextInt(lineSize)); LogoLine line = this.logoLines.get(lineNum); if(line.lineLength() == 0){ continue; // 選択した行の長さが0の場合は、次の行選択処理へ移る } // ランダムカラム選択 int col = Math.abs(rnd.nextInt(line.lineLength())) + 1; // 文字出力 line.printColChar(col); // スレッドスリープ try { Thread.sleep(sleepTime); } catch (Exception e) { e.printStackTrace(); } // スリープ時間減少(最低50ms) if (sleepTime - 30 >= 0) { sleepTime -= 30; } } } /** * 保持するロゴの各行がすべて出力されたか判定する * @return */ public boolean isAllLineWrited(){ for (LogoLine logoLine : logoLines) { if (!logoLine.isAllWrited()) { return false; } } return true; } }
LogoLine.java
import java.util.Arrays; /** 出力ロゴ構成行 * ロゴ文字列のある行の文字列とその行数を保持する */ public class LogoLine extends Thread { /**割り当て行数 */ private int line; /**行文字列 */ private String lineString; /**カラム書き込み済みフラグ配列 */ private boolean[] writedCol; /** * 割り当て行数とその行が保持する文字列を渡して初期化する * 列の位置とカラム書き込み済みフラグ配列は、それぞれ1とfalseで初期化する * @param line * @param lineString */ public LogoLine (int line, String lineString){ this.line = line; this.lineString = lineString; this.writedCol = new boolean[lineString.length()]; Arrays.fill(writedCol, false); } /** * 保持する文字列のうち指定された列の文字列を出力する * 指定された列の文字列が出力済みだった場合と全ての文字列が出力済みだった場合は、呼び出し元に何もせずに戻る * @param col */ public void printColChar(int col){ // 配列長超過判定 if(col > lineString.length()) { return; } // 行文字列出力済み判定 if (writedCol[col - 1] || isAllWrited()){ return; } // 保持する行の指定された列にカーソルを移動する System.out.printf("\033[%d;%df", this.line, col); // 保持する文字列の指定された列に対応する文字を出力する System.out.print(this.lineString.charAt(col - 1)); // 指定された列に対応するカラム書き込み済みフラグをtrue(書き込み済み)に変更する this.writedCol[col - 1] = true; } /** * カラム書き込み済みフラグ配列がすべてtrueになっているか判定する * @return */ public boolean isAllWrited(){ for (boolean b : writedCol) { if(!b){ return false; } } return true; } /** * 保持する文字列の長さを返す。 * @return */ public int lineLength(){ return this.lineString.length(); } /** * 保持する行数を返す * @return */ public int getLine() { return this.line; } }
HelloWorld.txt
/// /// ////////// /// /// ////// /// /// /// /// /// /// /// /////////// ///////// /// /// /// /// /// /// /// /// /// /// /// /// /// ////////// ////////// ////////// ////// /// /// ////// //////// /// ////// /// /// /// /// /// /// /// /// /// /// /// ///// /// /// /// //////// /// /// /// ///// ///// /// /// /// /// /// /// /// /// /// ////// /// /// ////////// ////////
ソースの説明
掲載したソースの説明をさせていただきたいと思います、、、が、うぉいHelloWorld.txtがプレビューで見たら変なことになってる(笑) 私が使っているモニターのサイズが問題なんでしょうか?
概要
テキストファイル(HelloWorld.txt)に保存した文字列のロゴを、コンソールのセル単位 = 半角一文字ごとにランダムに表示するプログラムです。 テキストファイルに保存した情報を変えることで、別のロゴを出力することができます。
HelloWorld.txt
出力するロゴ = 文字列を保存しています。プログラムは起動すると、このテキストファイルの文字列を各行ごとに読み取り配列として保持します。 ちなみに、Java13からヒアドキュメントがサポートされております。ですので今回ヒアドキュメントでの実装も考えたのですが、ロゴのデータをjavaファイルから切り離したほうが今回作成するプログラムの趣旨に合うのかな、と思ってテキストファイルに取り出しました。
Logo.java, LogoLine.java
それぞれロゴの本体と各行を表しています。 Logo.java#printLogoで文字を出力する行と列をランダムに選択し、該当する行のオブジェクト(LogoLineインスタンス)のLogoLine#printColCharを実行します。その結果は、この記事の冒頭でお見せした通りです。 少しでもHello Worldの見た目が楽しくなるように心がけました(笑)
コンソール画面の初期化処理やカーソルの移動処理では、エスケープシーケンスを使用しております。 私の開発環境はWindows 10なのですが、Windowsでエスケープシーケンスを使用してちょっと凝ったコンソール画面の動きを実装しようとすると、期待通りに動かなかったり不安定な挙動になったりして割とうまくゆきません。今回は、Cygwin上で実行することで不安定な挙動になることを避けました。 歴史的にWindowsでは、ANSIエスケープコードをサポートしていなかったという背景があります。私もそこらへんはあまり詳しくはないのですが、いい記事を見つけましたらこの記事で共有させていただきます。
しれっとLogoLineクラスがThreadを継承しているのは、リファクタリングフェーズでマルチスレッドにしてみようかなぁ、と何となく考えているからです。今後気が向いたら実装することもあるかもしれません。
まとめ
そこそこ面白いHello Worldが書けたと思います。 コンソールアプリって作っていてシンプルに楽しいと感じるので好きです。
ここまで読んでくださった方、真にありがとうございます。 気が向いた方は、Twitterのフォローをお願いします。 twitter.com
今回の記事に関する質問や意見などは、この記事のコメント欄やTwitterにてどうぞご気軽に上げていただければと思います。