このブログは不定期に更新されるゲーム製作アラカルトのメモ帳です。
記事は以下のカテゴリーに分類されてます。(クリックで記事一覧表示)
- 雑記:思いついたことアレコレです。
- ゲームデザイン:ゲームの仕組みやシステムについての考察です。
- ゲーム寸感:ゲームの感想です。
- ゲームの作り方:ゲームの作り方です。
- プログラミング:ソースコードなど、プログラマー向けの記事です。
書いてる人→@eiki_okuma
意外と無かったので作りました
Shader "Custom/Sprite - BlurBack" { Properties { [PerRendererData] _MainTex("Sprite Texture", 2D) = "white" {} _BlurAmount("Blur Amount", Float) = 0.1 } SubShader { Tags { "Queue" = "Transparent" } GrabPass { "_BGTexture" } // GrabPass を使用。シンプル! Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc" struct appdata { float4 vertex : POSITION; float2 uv : TEXCOORD0; }; struct v2f { float4 vertex : SV_POSITION; float2 uv : TEXCOORD0; }; sampler2D _BGTexture; float _BlurAmount; v2f vert( appdata v ) { v2f o; o.vertex = UnityObjectToClipPos( v.vertex ); o.uv = v.uv; return o; } half4 frag( v2f i ) : SV_Target { // ガウシアンぼかし float4 col = tex2D(_BGTexture, i.uv); float2 blurSize = float2(_BlurAmount / _ScreenParams.x, _BlurAmount / _ScreenParams.y); for (int x = -1; x <= 1; x++) { for (int y = -1; y <= 1; y++) col += tex2D( _BGTexture, i.uv + float2(x, y) * blurSize ); } return col / 9.0f; } ENDCG } } }
GrabPass 便利ですね。(コンソールとかでも問題なく使えるんかな……?)
日本語のタイピングゲーム作りたいな~と思った時に、ネット上にチラホラ資料は見つかるものの決定版がなかったので自分で作りました。
アセットストアのコンプリートプロジェクトやスクリプト系アセットって、たしかにデモの画面は再現できるんだけどいざ開いてみると複雑怪奇な構造をしていて、無駄にスクリプトファイルが分断してたりデモに必須機能が埋め込まれていたりして自分のプロジェクトへの導入に手間取ることが結構あるんですよね。
その点このアセットはシンプルに作りましたので、秒であなたのプロジェクトにタイピングゲーム機能をぶち込めます。(いるか?)
ファイル構造はこんな感じ。必要なものは Core に入っている二つの .cs だけです。
問題データは表示する文字+読み仮名の (string,string) を TypingEngine に渡せばOKで、 Demo では Questions.cs に入ってますが、適当に整備すればOKです。(後述する自作ゲームでは Google Spread Sheet からデータを下ろしてきています)
タイピングゲームチュートリアルとかだと「とりあえず入力受け取れればOK」みたいな感じで、現在入力中の文字がどこなのか表示されなかったり、柔軟な入力に対応してなかったり、柔軟な入力をした時に例文のローマ字が変化しなかったりと割と不親切な仕上がりになることも多いですが、デモ動画を見ると分かるようにこのアセットではそういったあたりまである程度対応しています。
文字入力とかもちゃんとフックできるようになっているので、改造次第で割とリッチな演出もいけます。こういうのって大事ですよね。
で、何を隠そうこのエンジンは去年 unity1week で出したタイピングゲームのエンジンをそのまま分離したものなので、そのままこのゲームが応用例になります。
ためてまわす鳥さんタイピング | フリーゲーム投稿サイト unityroom
他にもあらゆるタイピングゲームが作れると思いますが、例えばタイピングオブザデッドのように複数の目標が出てきてどこからでも倒すことができるようなゲームの場合、TypingEngine を複数作るのがコツです。取り回しも大変楽。
*OnGUI をエンジンに入れ込みたかったので MonoBehaviour になっていますが、OnGUI さえ別の場所から呼んでしまえば MonoBehaviour を外すことも可能です。
というわけで、雑にタイピングゲーム作りたくなった人はぜひ焼肉定食をぼくにおごってください。よろしくネ!
Unity の New Input System、使ってますか。
長らく Unity 製ゲームでまともにゲームパッドやキーコンフィグに対応しようとすると Rewired というアセットを使わざるを得なかったんですが、Unity が公式にこれら機能に対応してくれました。ありがとう Unity。ちょっと遅いぞ Unity。
早速飛びついて一本ゲームを作るまで一応使い込んだので、解説します。
さて、New Input System のコア部分には二つの使い方があります。
で、ネットを検索するとどうも前者の情報しか出てこないんですよね。しかし、自分的には PlayerInput を Component でいちいち追加しないといけないのは GameObject の取り回しが面倒なので避けたい。ので、自分は後者の方法でやってます。後者の方法で New Input System を使うには、新規作成した inputactions アセットで Generate C# Class にチェックを入れるだけ。あ、この前段階のパッケージインポート云々は適当なチュートリアルサイト漁ってください。
この記事ではクラス名は GeneralInput とします。
アリスではこんな感じ。Gamepad と Keyboard 両対応です。
public enum EInputName {/*略*/} GeneralInput mInput = null; InputAction[] mActions = new InputAction[(int)EInputName.Max]; private void Start() { mInput = new GeneralInput(); mActions[(int)EInputName.Decide] = mInput.Game.Decide; mActions[(int)EInputName.Cancel] = mInput.Game.Cancel; mActions[(int)EInputName.Jump] = mInput.Game.Jump; mActions[(int)EInputName.Dash] = mInput.Game.Dash; mActions[(int)EInputName.Menu] = mInput.Game.Menu; mActions[(int)EInputName.Attack] = mInput.Game.Attack; mActions[(int)EInputName.Change] = mInput.Game.Change; mActions[(int)EInputName.Map] = mInput.Game.Map; mInput.Enable(); } public bool isHold( EInputName in ) => mActions[(int)in].IsPressed(); public bool isTrig( EInputName in ) => mActions[(int)in].WasPressedThisFrame(); public bool isReleased( EInputName in ) => mActions[(int)in].WasReleasedThisFrame();
はい。簡単ですね。ここまでは解説なしで大丈夫だとおもいます。
static public bool HasPad() => Gamepad.current != null;
基本的にはこれでOKです。ただし、ゲームパッドを差しているけどキーボードを使いたい人に対処したいなら、今入力しているものがパッドなのかキーボードなのかを検知し、その都度切り替えてあげましょう。
RebindingOperation というものを使います。
パラメータを色々とセットして Start() すると、キーを押すかキャンセルするかするまで待機します。onComplete まで来たらすでにキーバインドは変更されているので、わざわざセットする必要はありません。
ついでに、キーバインドは SaveBindingOverridesAsJson で Json 取得可能なので、そのまま保存してしまいましょう。
InputActionRebindingExtensions.RebindingOperation mRebindingOperation = null; public void rebindButton( int index = 0 ) { mInput.Disable(); mRebindingOperation = mActions[mCursor].PerformInteractiveRebinding( index ) .WithBindingGroup( "Gamepad" ) .WithControlsHavingToMatchPath("<gamepad>") .OnMatchWaitForAnother( 0.2f ) .OnCancel( op => onCancelKeyBinding() ) .OnComplete( op => onFinishKeyBinding() ) .Start(); } } void onFinishKeyBinding() { disposeOperation(); // セーブする PlayerPrefs.SetString( "KeyBinding", mInput.Game.Get().SaveBindingOverridesAsJson() ); PlayerPrefs.Save(); } void onCancelKeyBinding() => disposeOperation(); void disposeOperation() { mRebindingOperation?.Dispose(); mRebindingOperation = null; mInput.Enable(); }
index は何番目のキーコンフィグを変更するか指定します。
上のコードでは Input を Disable / Enable していますが、別のスキームに差し替える方法もあるようです。Enable() 直後は謎の入力が入っていたりするので、ウェイトをかけるなどして二連続でキーコンフィグに突入しないよう注意してください。
特定の行動にスティックなどを Bind したくない場合は
.WithControlsExcluding( "leftStick" )
などをパラメータとして追加してください。
キーボードを検知したい場合は、Gamepad の部分を二箇所 Keyboard にし、index を
mActions[mCursor].GetBindingIndex( InputBinding.MaskByGroup( "Keyboard" ) )
のように取得してください。
ロードの場合は保存した String を LoadBindingOverridesFromJson で読みます。
public void loadKeyBinding() { if ( PlayerPrefs.HasKey( "KeyBinding" ) ) { try { mInput.LoadBindingOverridesFromJson( PlayerPrefs.GetString( "KeyBinding" ) ); } catch { Debug.Log( $"[Keybinding] error" ); } } }
ここで一点注意。General Input は new するだけでどのクラスでも使えますが、LoadBinding したキー設定は、そのインスタンスにしか適用されません*1。LoadBinding したインスタンスを使いまわしたい場合は、どこかに static な GeneralInput を持っておいて、それを Get() して使いましょう。
mInput.FindAction( "Decide" ).ApplyBindingOverride( 0, "<Gamepad>/buttonEast" ); mInput.FindAction( "Cancel" ).ApplyBindingOverride( 0, "<Gamepad>/buttonSouth" );
これだけ。例えば Switch 環境だけ決定ボタンとキャンセルボタンを逆にしたい、みたいな設定は簡単。
public IEnumerator seq_vibrate( float power = 1f, float duration = 0.15f ) { Gamepad.current.SetMotorSpeeds( power, power ); yield return new WaitForSeconds( duration ); Gamepad.current.SetMotorSpeeds( power, power ); }
これも簡単ですね。SetMotorSpeeds は二つの周波数の振動をコントロールできるようで、具体的にどういうモノかはお手持ちのコントローラで試してみてください。
これは少しめんどいです。でも、最近のゲームは大体やってるので根性で頑張りましょう。
まず、TextMeshPro にスプライトアイコンを表示する……あたりのくだりは省略します。いい感じに全ボタン分のアイコンを設定してください。
//-------------------------------------------------------------------------- static public string GetSpriteName( string device_layout_name, string controlPath ) { return GetSpriteName( GetPadType( device_layout_name ), controlPath ); } static public string GetSpriteName( EPadType pad_type, string controlPath ) { var prefix = pad_type == EPadType.XBox ? "XB" : pad_type == EPadType.DualShock ? "PS" : "GP"; switch( controlPath ) { case "buttonSouth": return prefix + "_S"; case "buttonNorth": return prefix + "_N"; case "buttonEast": return prefix + "_E"; case "buttonWest": return prefix + "_W"; case "start": return prefix + "_ST"; case "select": return prefix + "_SE"; case "leftTrigger": return prefix + "_L"; case "rightTrigger": return prefix + "_R"; case "leftShoulder": return prefix + "_L2"; case "rightShoulder": return prefix + "_R2"; case "leftStickPress": return prefix + "_L3"; case "rightStickPress": return prefix + "_R3"; case "dpad": return "LStick"; case "dpad/up": return "Up"; case "dpad/down": return "Down"; case "dpad/left": return "Left"; case "dpad/right": return "Right"; case "leftStick": return "LStick"; case "rightStick": return "RStick"; } return "INVALID"; } //-------------------------------------------------------------------------- static EPadType GetPadType( string device_layout_name ) { if ( InputSystem.IsFirstLayoutBasedOnSecond( device_layout_name, "DualShockGamepad" ) ) { return EPadType.DualShock; } else if ( InputSystem.IsFirstLayoutBasedOnSecond( device_layout_name, "XInputController" ) ) { return EPadType.XBox; } else return EPadType.Gamepad; } static public string GetSpriteNameOfKeyboard() => "KB_KEY";
なんとなく分かったでしょうか。pad_type と controlPath によって適切なボタンの Sprite アイコンを呼んでいます。ただ、キーコンフィグを想定するとどのアクションにどの controlPath が設定されているのかわからないので、 GetSpriteName の呼び方にコツがあります。
public string getSpriteName( string action_name ) { var act = mInput.FindAction( action_name ); if ( act == null ) return ""; if ( !HasPad() ) { var b = act.GetBindingIndex( InputBinding.MaskByGroup( "Keyboard" ) ); act.GetBindingDisplayString( b, out var deviceLayoutName2, out var controlPath2 ); return $"<sprite name=\"{GetSpriteNameOfKeyboard()}\">{controlPath2.ToUpper()}"; } else { act.GetBindingDisplayString( 0, out var deviceLayoutName, out var controlPath ); return $"<sprite name=\"{GetSpriteName( deviceLayoutName, controlPath )}\">"; } }
特定のアクションに割り振られているキーは GetBindingDisplayString で取得します。キーボードの場合はキーが返ってくるのでよしなにしてください。一番目の引数は例によって index なので、メインキー、サブキーを併記したい場合はそんな感じに改変してみてください。
大体ゲームに必要な機能は網羅できたとおもいます。よき new input system ライフを*2。
え?よく分からなかった? そうかもしれません……でも大丈夫! そんな人のためにサンプルプロジェクト&UnityPackage も作りました。コインいっこでね。
こちらからどうぞ↓
いつものように前口上書いているうちにテンション下がるので本題から。
以上!
一日一時間ずつ遊んだエルデンリングをようやくクリアしました。
クリアタイムは74時間なので、ほぼ丸二ヶ月。いやー長かったですね。特にソウルシリーズは初めてだったので、最初の方は大分苦労しましたし、マルギットの洗礼も漏れなく受けました。終盤はほぼ雫先生のお世話になったので、大分イージーゲーマーと言っていいでしょう。一番苦労したのはマレニア。
ゲームデザインとしても大いに参考になるゲームだったので色々書きたいことはあったんですが、クリアするまで批評というものは書けないもので、ようやく話せます。ネタバレ全開でいくので未クリアの人は注意してください。あと、結構厳しいことも書きます。
と、このゲームが傑作であることは枚挙に暇がないのですが、この辺にして。
配信後にまた気づくやつ!
— Sonic Bird (@sonicbird19) 2022年4月29日
多くの人は2体の巨人に目がいって横にいるラーヤに気づかないってやつやんw #ELDENRING #エルデンリング pic.twitter.com/MG5JZrT24a
*1:前半部分を厳密に書くと、8人産んだ結果まともな神人が生まれない=黄金律に欠陥があることに気付いたマリカは黄金律に死の回帰が無いことが原因と考え、寵愛するゴッドウィンを死のルーンで殺害して死王子のルーンを得ることを考えたが協力者であるラニが肉体の死のために死のルーンの片割れを使ってしまったので失敗。仕方ないので黄金律そのものであるエルデンリングをぶっ壊そうとしたものの半身であるラダゴンが抗って修復したためそれも失敗(OPムービーの冒頭が壊して直しての2コマ漫画になってることに気づくと笑えます)。破砕戦争を経て狭間の地はにっちもさっちもいかない状況になりましたとさ。という感じです。皆さんの解釈も同じでしたか?
*2:というか、もう直接腐敗の苔薬をフィールド上に置けばよくて、アイテム製作とか必要ないのでは