前提・実現したいこと
Unityでカードゲームを作ろうとしています。
現在、デッキエディット画面がある程度完成し、今度は何から手を付けたらいいか分からなくなり、
カードの効果スクリプトを書いてみようと思い調べました。
しかしフェイズ移行や攻撃の仕方などを書いてあるものはあったりしたのですが、
求めているような結果が得られなかったため質問させていただきます。
現状、カードのステータス等はエクセルで管理し、Unityで読み取れるようにしてあります。
作ろうとしているカードゲームのシステムの雰囲気としては、シャドウバース等よりは遊戯王等に近く、
任意に起動させる効果や出された時の効果、永続的な効果や相手のターンにでも使える効果などがあります。
カード種類は大まかに
ユニット(効果を持つモンスターが大半)、
スペル(基本的に相手ターンにも使えるもの)、
アイテム(基本的に自分ターンに使えるもの)があります。
どのようにスクリプトを書いたらカード効果の実装を実現できるでしょうか。
ちなみにカードの枚数は数百種類あるので、それに適した書き方を教えてほしいです。
--2021/07/27追記
自分なりにカード効果の分析をして分けて考え、スクリプトに落とし込むにはこう処理するのでは、
というものを書きました。
カード効果の一例と分析結果を以下の画像に追記します。
カード効果
分析結果
気になる質問をクリップする
クリップした質問は、後からいつでもMYページで確認できます。
またクリップした質問に回答があった際、通知やメールを受け取ることができます。
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。
2021/07/27 09:20
2021/07/27 09:28
2021/07/28 11:19 編集
回答4件
0
ベストアンサー
とりあえず至極シンプルな物を作ってみる,というのをやってみました.
(コンソールアプリですが.まぁどこかがごく一部でも何かしらの参考になれば)
この例は「どこにも面白さがない謎の一人遊び」みたいなやつですが,
まずはこのくらいの内容から始めてみると良いのではないかと.
ルール
- 毎ターン,デックからカードを1枚ドローして手札に加え,手札から1枚選んで場に出します.
- 何やかんやのカード効果によってスコアが増減します.スコアが目標値に達したら終了です.
(1)カード効果の発動処理の流れを考える
「いろんなタイミングでカードの効果が発動する」という話を,
「カードから各タイミングで発動すべき効果群を取得し,それらを実行する」という形で考えた.
登場人物として{カード,カード効果,タイミング,カード効果の実装手段}くらいが必要そうなので,
ざっくりとそれらの間の関係性をコード化:
(行数がちょっと多いので空行を除去したりしてます.見難いかも)
CSharp
1//カード効果の処理側からゲームデータにアクセスする手段 2internal interface IGameContextForCardEffect 3{ //※この例ではこれら全部を使ってるわけじゃないけど,効果がやりそうなことを並べてみた 4 void AddToField( ICard Card ); //場にカードを出す 5 void RemoveFromField( ICard Card ); //場のカードを除去する 6 void AddToHand( ICard Card ); //手札にカードを加える 7 void RemoveFromHand( ICard Card ); //手札からカードを除去する 8 ICard DrawFromDeck(); //デックからカードを1枚引く 9 int Score{ get; set; } //スコア増減用 10} 11//カード効果 12internal interface ICardEffect 13{ 14 string Name{ get; } //効果名称 15 string EffectText{ get; } //効果説明文 16 ICard Owner{ get; } //どのカードの効果か?(※この例では使ってないけど) 17 void Affect( IGameContextForCardEffect IGC ); //効果の実行 18} 19//カード効果を処理するタイミングの種類 20enum EffectTiming 21{ 22 AddedToHand, //カードが手札に追加されたとき 23 AddedToField, //カードが場に出されたとき 24 RemovedFromField, //カードが場から除去されたとき 25 AtTurnEnd_OnField //ターン終了時にカードが場にあるとき 26} 27//カード 28internal interface ICard_Readonly 29{ 30 string Name{ get; } //カード名称 31} 32internal interface ICard : ICard_Readonly 33{ 34 //引数タイミングで処理すべき効果群を返す. 35 //効果は戻り値の並び順で処理される. 36 //処理すべき効果が無い場合には空リストを返す 37 List< ICardEffect > GetActiveEffects( EffectTiming Timing ); 38}
(2)カードの効果とカードを何種類か作ってみる
3種類の効果(ICardEffect
の実装)を用意してみた:
CSharp
1//効果:スコアを増減させる 2internal class CE_ScorePlus : ICardEffect 3{ 4 public int dScore{ get; private set; } 5 public CE_ScorePlus( ICard Owner, int dScore ){ this.Owner=Owner; this.dScore=dScore; } 6 #region ICardEffect 7 public string Name{ get{ return "ScorePlus"; } } 8 public string EffectText{ get{ return "Scoreを " + dScore + " 増やす"; } } 9 public ICard Owner{ get; private set; } 10 public void Affect(IGameContextForCardEffect IGC){ IGC.Score += dScore; } 11 #endregion 12} 13//効果:デックからカードを引いて手札に加える 14internal class CE_DrawCard : ICardEffect 15{ 16 public int nDraw{ get; private set; } 17 public CE_DrawCard( ICard Owner, int nDraw ){ this.Owner=Owner; this.nDraw=nDraw; } 18 #region ICardEffect 19 public string Name{ get{ return "DrawCard"; } } 20 public string EffectText{ get{ return "カードを " + nDraw + " 枚引く"; } } 21 public ICard Owner{ get; private set; } 22 public void Affect(IGameContextForCardEffect IGC) 23 { 24 for( int i=0; i<nDraw; ++i ) 25 { 26 var C = IGC.DrawFromDeck(); 27 Console.WriteLine( C.Name + " をドロー" ); 28 IGC.AddToHand( C ); 29 } 30 } 31 #endregion 32} 33//効果:特定のカードを手札に加える 34internal class CE_GetCard : ICardEffect 35{ 36 public ICard CardToBeAdded{ get; private set; } 37 public CE_GetCard( ICard Owner, ICard CardToBeAdded ){ this.Owner=Owner; this.CardToBeAdded=CardToBeAdded; } 38 #region ICardEffect 39 public string Name{ get{ return "GetCard"; } } 40 public string EffectText{ get{ return "カード " + CardToBeAdded.Name + " を得る"; } } 41 public ICard Owner{ get; private set; } 42 public void Affect(IGameContextForCardEffect IGC){ IGC.AddToHand( CardToBeAdded ); } 43 #endregion 44}
カード(ICard
の実装)も適当に数種類用意します.
とりあえずスコアを増やせるカードを2種類:
CSharp
1//カード[Kitten] 2internal class C_Kitten : ICard 3{ 4 public string Name{ get{ return "Kitten"; } } 5 public List<ICardEffect> GetActiveEffects(EffectTiming Timing) 6 { //ターン終了時にこのカードが場にあるとき,スコア+2. 7 var Ret = new List<ICardEffect>(); 8 if( Timing == EffectTiming.AtTurnEnd_OnField ){ Ret.Add( new CE_ScorePlus( this, 2 ) ); } 9 return Ret; 10 } 11} 12//カード[WildCat] 13internal class C_WildCat : ICard 14{ 15 public string Name{ get{ return "Wild_Cat"; } } 16 public List<ICardEffect> GetActiveEffects(EffectTiming Timing) 17 { //このカードを場に出したとき,カードを1枚ドローする. 18 //このカードが場から取り除かれるとき,スコア+1. 19 var Ret = new List<ICardEffect>(); 20 switch( Timing ) 21 { 22 case EffectTiming.AddedToField: Ret.Add( new CE_DrawCard( this, 1 ) ); break; 23 case EffectTiming.RemovedFromField: Ret.Add( new CE_ScorePlus( this, 1 ) ); break; 24 default: break; 25 } 26 return Ret; 27 } 28}
効果が嬉しくない(スコアが減る,手札圧迫する)カードも作ってみる:
CSharp
1//カード[CatNapDisaster] 2internal class C_CatNapDisaster : ICard 3{ 4 public string Name{ get{ return "CatNapDisaster"; } } 5 public List<ICardEffect> GetActiveEffects(EffectTiming Timing) 6 { //このカードを手札に加えたとき{スコア-3,カード[Meow]が手札に追加される} 7 var Ret = new List<ICardEffect>(); 8 if( Timing == EffectTiming.AddedToHand ) 9 { 10 Ret.Add( new CE_ScorePlus( this, -3 ) ); 11 Ret.Add( new CE_GetCard( this, new C_Meow() ) ); 12 } 13 return Ret; 14 } 15} 16//カード[Meow] 17internal class C_Meow : ICard 18{ //[CatNapDisaster]の効果でのみ生み出される,何の効果も無いカード 19 public string Name{ get{ return "Meow"; } } 20 public List<ICardEffect> GetActiveEffects(EffectTiming Timing){ return new List<ICardEffect>(); } 21}
(3)動かす部分を作ってみる
ゲームのデータと処理を持つクラスを書いて…:
CSharp
1//ゲームデータと処理.IGameContextForCardEffectを実装. 2internal class GameContext : IGameContextForCardEffect 3{ 4 private List<ICard> m_Hand = new List<ICard>(); //手札 5 private List<ICard> m_Field = new List<ICard>(); //場 6 private Random m_Rnd = new Random(); //乱数用 7 public int GoalScore{ get; private set; } //目標スコア 8 public int CurrentTurn{ get; private set; } //現在のターン数 9 public int nHandCard{ get{ return m_Hand.Count; } } //手札の枚数 10 public ICard_Readonly HandCard( int HandCardIndex ){ return m_Hand[HandCardIndex]; } //手札情報参照用 11 //ctor 12 public GameContext( int GoalScore ) //目標スコアを引数で指定 13 { 14 CurrentTurn = 1; 15 Score = 0; 16 this.GoalScore = GoalScore; 17 for( int i=0; i<3; ++i ){ m_Hand.Add( DrawFromDeck() ); } //初期手札3枚 18 } 19 //ゲーム終了判定 20 public bool GoalCheck(){ return (Score>=GoalScore); } 21 //現状の表示 22 public void ShowState() 23 { 24 Console.WriteLine( "Score {0}/{1}", Score, GoalScore ); 25 ShowPacket( "手札", m_Hand ); 26 ShowPacket( "場", m_Field ); 27 } 28 //手札からカード1枚を場に出す 29 public void MoveFromHandToField( int HandCardIndex ) 30 { 31 var Card = m_Hand[HandCardIndex]; 32 RemoveFromHand( Card ); 33 AddToField( Card ); 34 } 35 //ターン終了処理 36 public void ToNextTurn() 37 { 38 while( m_Field.Count > 3 ) //場のカードが3枚以下になるよう,古い側から除去 39 { 40 Console.WriteLine( m_Field[0].Name + " を場から除去する" ); 41 RemoveFromField( m_Field[0] ); 42 } 43 while( m_Hand.Count > 5 ) //手札が5枚以下になるよう,古い側から除去 44 { 45 Console.WriteLine( m_Hand[0].Name + " を手札から除去する" ); 46 RemoveFromHand( m_Hand[0] ); 47 } 48 foreach( var C in m_Field ){ ProcCardEffects( C, EffectTiming.AtTurnEnd_OnField ); } 49 ++CurrentTurn; 50 } 51 //表示用の作業関数 52 private static void ShowPacket( string PacketName, List<ICard> Tgt ) 53 { 54 Console.WriteLine( PacketName ); 55 Console.Write( " { " ); 56 foreach( var C in Tgt ){ Console.Write( C.Name + ", " ); } 57 Console.WriteLine( "}" ); 58 } 59 //カード効果の処理 60 private void ProcCardEffects( ICard Card, EffectTiming Timing ) 61 { 62 var Effects = Card.GetActiveEffects( Timing ); 63 if( Effects.Count == 0 )return; 64 Console.Write( "カード効果:" + Card.Name ); 65 switch( Timing ) 66 { 67 case EffectTiming.AddedToHand: Console.WriteLine( " が手札に加わったとき" ); break; 68 case EffectTiming.AddedToField: Console.WriteLine( " が場に出たとき" ); break; 69 case EffectTiming.RemovedFromField: Console.WriteLine( " が場から除去されたとき" ); break; 70 case EffectTiming.AtTurnEnd_OnField: Console.WriteLine( " が場にある状態でターン終了時" ); break; 71 default: throw new ArgumentException( "BUG" ); 72 } 73 foreach( var E in Effects ) 74 { 75 Console.WriteLine( " -> " + E.Name + " : " + E.EffectText ); 76 E.Affect( this ); 77 } 78 } 79 #region IGameContextForCardEffect 80 public int Score { get; set; } 81 public void AddToField(ICard Card){ m_Field.Add( Card ); ProcCardEffects( Card, EffectTiming.AddedToField ); } 82 public void AddToHand(ICard Card){ m_Hand.Add( Card ); ProcCardEffects( Card, EffectTiming.AddedToHand ); } 83 public void RemoveFromHand(ICard Card){ if( !m_Hand.Remove(Card) ){ throw new InvalidOperationException("BUG"); } } 84 public void RemoveFromField(ICard Card) 85 { 86 if( !m_Field.Remove(Card) ){ throw new InvalidOperationException("BUG"); } 87 ProcCardEffects( Card, EffectTiming.RemovedFromField ); 88 } 89 public ICard DrawFromDeck() 90 { //※デックの実装が面倒なのでてきとーにカードを生成して返している 91 switch( m_Rnd.Next(3) ) 92 { 93 case 0: return new C_WildCat(); 94 case 1: return new C_CatNapDisaster(); 95 default: return new C_Kitten(); 96 } 97 } 98 #endregion 99}
あとは,こいつのメソッドを適当な順序で呼び出す処理をMainに書く:
(Console.ReadLine();
はコンソール出力を目視するための一時停止用)
CSharp
1static void Main(string[] args) 2{ 3 var Game = new GameContext( 7 ); 4 var Rnd = new Random(); //※手札をてきとーに選ぶ用 5 while( true ) 6 { 7 Console.WriteLine( "----------" ); 8 Console.WriteLine( "Turn " + Game.CurrentTurn.ToString() + " 開始時" ); 9 Game.ShowState(); 10 Console.WriteLine(); 11 12 Console.ReadLine(); 13 if( Game.GoalCheck() )break; 14 {//カードを1枚ドロー 15 var C = Game.DrawFromDeck(); 16 Console.WriteLine( "[" + C.Name + " をドロー]" ); 17 Game.AddToHand( C ); 18 } 19 if( Game.nHandCard > 0 ) 20 {//手札から場に1枚出す(てきとーに乱数で選んでる) 21 int index = Rnd.Next( Game.nHandCard ); 22 Console.WriteLine( "[ {0}({1}) を場に出す]", Game.HandCard(index).Name, index ); 23 Game.MoveFromHandToField( index ); 24 } 25 Console.WriteLine( "[ターン終了時処理]" ); 26 Game.ToNextTurn(); 27 } 28 Console.WriteLine( "==終了==" ); 29 Console.ReadLine(); 30}
とりあえず以上で,カードを引いたり場に出したりしたタイミングで効果が発動する形にはなった.
(運が悪いとスコアがどんどんマイナス方向に行って永久に終わらなくなったりするので注意が必要)
(カード効果によってカードをドローしたらそのカードのドロー時効果がそこで発動したりしてわりとカオス)
投稿2021/07/28 07:12
総合スコア11996
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。
2021/07/29 01:14
2021/07/29 08:10
2021/09/08 11:19
0
どのようにスクリプトを書いたらカード効果の実装を実現できるでしょうか。
ちなみにカードの枚数は数百種類あるので、それに適した書き方を教えてほしいです。
まずですね……。カードの枚数が数百種類あれば、
それだけ数百種類の困難(エラー、バグ、不具合など)もあると考えてください。
だって、パラメータがちょっと違うだけじゃないんでしょ? それじゃつまんないし。
たぶん、カード固有の能力が一枚一枚にあるんでしょう?
たとえば、「AというカードはBというカードの効果を1.5倍にする。
しかしCのカードが先に発動していると、その効果は打ち消される」
みたいにきっと、複雑なルールになるのではないか、と予想します。
そういう複雑なルールを、単純なIF文の分岐で実装しようとすると、
ゴチャゴチャになってコードが破綻しやすく、制作も挫折しやすいです。
さらに、カードゲームはたった1枚の強力なカードでバランス崩壊することもあり、
カードの追加や強化/弱体化など、後からバランス調整したくなることと思います。
しかし、ただでさえ仕様が複雑なのに、後から仕様変更が重なるというのは、
プログラミングにおいて、非常に悪条件なのです。
メンテナンスも大変だから、ますます破綻しやすく、挫折しやすい。
じゃあ、どうすればいいか? 私がオススメする手法は、
まずは小さなプロトタイプから始めることです。
商業カードゲームの規模で、そのまま再現するのは大変だから、
たとえば、試作として「トランプ」から始めるとか。
そこで、カードをシャッフルするとか、山札から手札を配るとか、
プレイヤーが交代でカードを場に一枚ずつ捨てていくとか、
基本的な課題を完全にクリアしてから、本番に挑戦しようと。
私の考えでは、「カードゲーム」制作というのは、
高い設計力が要求されるジャンルです。
とくに固有の能力を持つカードが数百枚ともなると、
ゲームのルールが複雑に絡み合って、コードが破綻しがちです。
具体的にはたとえば、ご質問の効果関連では、たとえば「カードの効果発動の優先順位」とか、
「バフの効果持続ターン」とか、「対戦相手のAIによる、カード効果の認識と判断」とか、
いかにも条件分岐が複雑になりそうな部分があるかもしれません。勝手な想定ですが。
素朴なIF文の入れ子とフラグでは、書きたくない所です。
とくに長期間の修正を繰り返していくと、壮絶に汚くなりそう。
では、どうやって設計すればいいのか?
オブジェクト指向の設計技法が有効だと、私は考えています。
そもそもOOPはゲームと相性が良いですが、カードゲームとはさらに良いです。
これはあくまで実装アイディアの一例(別に必ず最善とは限らない)ですが、
たとえばカードの効果をオブジェクトにして、キューに並べる方式にすれば、
いろいろな判定が簡潔になります。どういうことか、見てみましょう。
たとえば、優先順位を並べ直すのは、効果オブジェクトを
詰めている配列なりを整列すればいいだけ。だし、
バフの持続ターンが2以上あれば、1減らしてキューに詰め直すとか。
AIがキューを走査すれば、先回りしてカード効果を認識できる。
(とくにこの「先回り」が、素朴なやり方だと実装が難しいポイント)
ので、(カードの使用と効果の発動タイミングでラグがある場合でも
状況認識できずに、ランダムにカードを選ぶ頭の悪いAIではなく、
発動したカードの効果によって、行動を変える賢いAIにできます。
これをIFの分岐とフラグで実装するのは、可能だけど辛いと思います。
まあもちろん、ゲームデザインやルールにもよるので、あくまで架空の例です。
仕様が不明だから、質問者の方が知りたいことと、きっとズレていることでしょう。
でも、カードゲームの複雑な効果発動を処理する、ひとつの具体例ではあります。
ちなみにこれは、デザインパターンの「コマンドパターン」の応用です。
それを聞いてパッと実装のイメージが思い浮かばないようなら、
やはりまずトランプとか小規模なゲームで試行錯誤してみましょう。
投稿2021/07/27 00:47
総合スコア5592
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。
0
エクセルで管理しているという事はマスターからのデータ取得を想定していると思うのでカラムの値を変えれば同スキルで効果値は別に出来ます。数百種類程度は水増しで作れます
という話で大丈夫なのでしょうか?
正直なところこの回答では駄目だろうし、質問内容も何を聞きたいのか判らないです。
多分マスターデータの話をしても「カラムってなんですか?」と返されるんだろうなと予測しているくらいです。
カード種類は大まかに
ユニット(効果を持つモンスターが大半)、
スペル(基本的に相手ターンにも使えるもの)、
アイテム(基本的に自分ターンに使えるもの)があります。
こういう事を書き出していますがこれの何を聞きたいのでしょうか?
各カードの種類を判別する方法から聞きたいのか、使えるタイミングの判別を聞きたいのか、クラスの作り方から聞きたいのか...
そもそもどこまで出来ていて何を理解出来ているのかすら不明なので返答しようが無いです
質問する場合は数百種類などと誇大な事は書かずにまず最小単位の一種類作ってから聞いて下さい。
それの作りやデータの取得方法を見れば何かしらのアドバイスのしようもあります。
まずは**「自ターンで場にユニットカードを出して敵に1ダメージを与える効果」**の様に極単純なものを作ってそのコードを提示して下さい。
それくらい作れて当たり前だろと思うだろうし私もそう思うのですがそれ位のモノすら作れない人はここの質問者には相当数いるので回答者もそれくらいの温度感で対応してしまいます。
数百種類のスキル作成を質問するのはその温度感を払拭してからが良いかと思います。
※簡単と書きましたが自ターン敵ターンのステート管理にそれに伴う入力制限などキッチリ作ると専門卒上位でも結構な確率で一人では出来ないレベルではあります
投稿2021/07/26 07:03
編集2021/07/26 07:14総合スコア189
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。
2021/07/26 07:38
2021/07/26 18:40
0
カードの効果がどうのいう話が過去にあったのでリンクを示しておく.
参考になるのかどうかは不明ですが.
投稿2021/07/26 02:39
総合スコア11996
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。
あなたの回答
tips
太字
斜体
打ち消し線
見出し
引用テキストの挿入
コードの挿入
リンクの挿入
リストの挿入
番号リストの挿入
表の挿入
水平線の挿入
プレビュー
質問の解決につながる回答をしましょう。 サンプルコードなど、より具体的な説明があると質問者の理解の助けになります。 また、読む側のことを考えた、分かりやすい文章を心がけましょう。