第二章的问题,在尝试了很多解决办法后,我还是选择了将逻辑层和显示层分开的办法——这很完美的解决了问题,只不过增加了一点工作量。
原来的卡牌UI是cardView,我在这一层上再抽象了一层。这下子,分出了3层,数据层-物理检测层-显示层
________CardUI___________
↓ ↓
raycast cardView
↓
cardData
类似这样,cardView在大部分时间和raycast覆盖范围一样,但是在卡牌悬浮的时候,cardView会放大,让玩家看的更清晰。
这也更方便了缓动动画的实现,不用过度考虑动画进行的时候会不会误触了。
总的来说,我在炉石和杀戮尖塔之间,选择了炉石的办法。
下面谈谈逻辑问题。
依然是教程很少进入的领域,大部分教程都是在教你引擎的基本原理(这不怪教程),教你摆出卡牌的样子,怎么做动画,怪物攻击敌人这些,再加上不少教程都在后期烂尾了,所以很少进入到制作不同效果的卡牌,并让这些效果正常有序执行。
而更多谈到卡牌效果的,更多是一些偏玩家方向的视频——这些视频也不会怎么谈背后的代码原理,我很喜欢的一些主播,例如兽兽,也只会说一些扳机时间,取对象之类的,每个视频只讲一点,看几百期才能有个大概。
例如,施放法术的那一刻,到底是哪一刻?在函数里是哪一行?施法条件的限制用代码来说是if(条件 == true)吗?
⚔️ Forge: The Magic: The Gathering Rules Engine | forge
这个引擎我看了个大概,不太好,很难找到我能用的部分,它的卡牌效果,是个类似于解释性脚本和自然语言之间的形式。
Name:Daemogoth Titan
ManaCost:BG BG BG BG
Types:Creature Demon
PT:11/10
T:Mode$ Attacks | ValidCard$ Card.Self | Execute$ TrigSac | TriggerDescription$ Whenever CARDNAME attacks or blocks, sacrifice a creature.
T:Mode$ Blocks | ValidCard$ Card.Self | Execute$ TrigSac | Secondary$ True | TriggerDescription$ Whenever CARDNAME attacks or blocks, sacrifice a creature.
SVar:TrigSac:DB$ Sacrifice | SacValid$ Creature
DeckHas:Ability$Sacrifice
Oracle:Whenever Daemogoth Titan attacks or blocks, sacrifice a creature.
我很赞叹万智牌爱好者的努力,能够把代码隐藏起来,让一万多张卡牌都正常运行,但是我怎么知道背后是那些代码在处理这些文本?
magefree/mage: XMage - Magic Another Game Engine
public final class AnvilwroughtRaptor extends CardImpl {
public AnvilwroughtRaptor(UUID ownerId, CardSetInfo setInfo) {
super(ownerId,setInfo,new CardType[]{CardType.ARTIFACT,CardType.CREATURE},"{4}");
this.subtype.add(SubType.BIRD);
this.power = new MageInt(2);
this.toughness = new MageInt(1);
// Flying
this.addAbility(FlyingAbility.getInstance());
// First strike
this.addAbility(FirstStrikeAbility.getInstance());
}
这个引擎好了不少,虽然对不写代码的人来说更难了,但是它的卡牌的效果是用java语言写的,我可以通过源码看看万智牌是怎么做到的。
另外,这种代码和杀戮尖塔的代码风格很像,它们之间有没有共同之处呢?
public class Barrage extends AbstractCard {
public static final String ID = "Barrage";
private static final CardStrings cardStrings = CardCrawlGame.languagePack.getCardStrings("Barrage");
public Barrage() {
super("Barrage", cardStrings.NAME, "blue/attack/barrage", 1, cardStrings.DESCRIPTION, AbstractCard.CardType.ATTACK, AbstractCard.CardColor.BLUE, AbstractCard.CardRarity.COMMON, AbstractCard.CardTarget.ENEMY);
this.baseDamage = 4;
}
public void use(AbstractPlayer p, AbstractMonster m) {
addToBot((AbstractGameAction)new BarrageAction((AbstractCreature)m, new DamageInfo((AbstractCreature)p, this.damage, DamageInfo.DamageType.NORMAL)));
}
public void upgrade() {
if (!this.upgraded) {
upgradeName();
upgradeDamage(2);
}
}
public AbstractCard makeCopy() {
return new Barrage();
}
}
Xmage的代码我还没有看完,但是杀戮尖塔的代码写的比较简洁,我看了一些。杀戮尖塔的主要逻辑都在GameActionManager里,它的主要结构就是一个队列,每个卡牌都会AddToBottom一个action,一些action还会衍生新的action(遗物,怪物的技能等),接着以先进先出的方法处理每个action。
好了,一句话的功夫就说完了。
但这并不代表我可以轻松复现!!
杀戮尖塔由于使用的是java框架的原因,所以在显示和数据方面耦合度很高,例子就是:它的action,所有逻辑都写在Update里面,随着时间进行处理逻辑和动画进展,而actionManager在这一个动画结束后,才会开始处理下一个action的逻辑!
whaaaaaaaaat!
而我在炉石和杀戮尖塔之间,还是选择了炉石。或许因为是联网游戏的原因,所以炉石的逻辑不能在本地处理,而玩家的本地处理的,只是每个动画的播放和每一次使用卡牌的选择。这给了炉石一些好处,它可以用很少的运算量处理几十张卡牌之间的交互,而修改动画并不会影响到结算结果。
一个例子就是,炉石里的导演,在出场后,玩家就已经可以继续后面的操作了,而不用等待动画(例如,如果无法操作的话,就是有一个玩家在导演的效果中输掉了游戏)
于是,我便分出了LogicalActionManager和animationActionManager两个类,一个处理游戏的逻辑,一个处理游戏的显示。
然而这导致了更大的灾难。
简单来说,我太笨了,理解不了怎么将卡牌的使用过程转化为action,接着将action用正确的处理顺序放进队列中。
例如,一张卡有使用前的时机(炉石的伊利丹,杀戮尖塔的荆棘),在我打出一张卡之前,伊利丹会召唤一个2/1的随从。
(炉石的卡牌描述模糊了特效触发的时机)
也有使用后的时机:
更绝的是,还有使用过程中的counter,将这个使用过程取消掉。
那么,在队列中,我该怎么正确处理这些效果?(这些效果也应该是action)
用代码来说,伊利丹的效果反而是最简单的,我只要将一个【召唤2/1】的action置于卡牌的【使用】action之前就可以了。但是使用后的效果?我该怎么判断卡牌是否已经使用?毕竟有反制牌的存在。
对于这种多个效果的叠加,我从计算机原理里找到了思路。
Function Call Stack in C - Scaler Topics
函数的执行过程,不是很像卡牌的使用吗?
POWERFUL ACTION-REACTION System in Unity | BEST Card Game ARCHITECTURE!
这也是个不错的教程。
最后——经过了一个周的时间——在不断的试错中,我放弃了使用单个队列来处理所有卡牌的执行效果,我知道这是可以做到的,就像杀戮尖塔一样,但是这对我的大脑太复杂了。
依然是卡牌会产生action,action被送入manager中等待执行。但是我为action给了更多数据结构,分为preAction,BeginAction,postAction三个队列,用来储存使用前/使用后/使用中三种效果,正好对应上图三种分类。而preAction中的action,依然有它自己的action,这样无限递归,直到所有action处理完毕且没有新的action。
希望这样不会产生更大的麻烦,希望如此……
暂无关于此日志的评论。