FF2雪上船Flash小游戏教程(AS2.0 OOP)

2006-11-07 18:32 | Army


这个游戏的前身是在2005年完成的,也是我的Flash游戏处女作。它曾经耗了我至少2天的时间才完成,而如今采用OOP只用了1个多小时。也许这期间我的思维水平和编程经验都有所进步吧,但我更相信那是OOP的带来的好处所致。

我的朋友火光是激发我编写这个小游戏的引路人,当时GBA复刻版的FF1和FF2才发布,我用他mail寄来的进度拼好了新作的地图。随后,我觉得老是做这种体力活(你也许会认为是技术活,但的确大部分时间都是在重复单一的劳动)太乏味了。于是,模仿制作FF里面的小游戏的想法萌发了。经过最后的考虑,我瞄上了我很喜欢的FF2。

这个游戏的玩法是翻转两张卡片,当翻到相同内容的卡片时这两张就永远都是可见状态,否则会继续不可见让你去翻。

阅读这篇教程的人需要一定的Flash水平,绘图方面要求并不高,关键是语言功底。而且在最后,我会提出这个小游戏的3处bug。你可能会迷惑:既然有bug,为何不修正它?我的回答是:我会提出bug,并且同时说出解决它的思想办法。有时候,什么都帮读者做好了未必是件好事。这好比我把这篇教程发布出来,你完全可以不看解说直接复制代码,从而创建出自己的游戏来,但是你未必理解它。让你自己解决bug就是为了加深你的理解,同时锻炼自己的水平。当然,你也可以说是因为我懒…… -_,-

这个游戏的源文件地址:

http://ff9.ffsky.cn/flash_teach\ff2_minigame/Card.as
http://ff9.ffsky.cn/flash_teach\ff2_minigame/exp.fla

首先,来看下MC元件的组成吧,我只用一张图解说:



舞台上放置的6个方形花纹的图案是遮照(名词依次是lct1、lct2、……、lct6),我是直接从游戏里提取的。“天幻网”三个字是需要翻转的“牌”(名字依次是card1、card2、……、card6,相同文字的牌的名字是相邻的),为简便起见,我只写了这三个字,它也容易理解。等你真要写游戏时,可能就是6个、8个甚至更多了……
另一个重要原因就是你能体会到OOP维护起来有多方便:从3个增加到8个,你所要做的仅仅是添加另外的字,然后少许修改一下代码。如果这是嵌套在时间轴里的话……修改起来的确会很恐怖。
库中名字为0的MC就是这个方形,1~3是“天幻网”三个字。而且1~3每个MC都有1个透明度为0的方形作底,这样作仅是为了点击起来方便一些而已(若没有这个方形,除非你能点到字的笔划上,否则是会点空的,用图片代替文字则没这个问题)。

根时间轴的第1帧代码:

stop();
var c1:Card = new Card();
c1.initialize();


1~3每个MC中都有如下代码:

this.onRelease = function(){
  this._alpha = 100;
  _root.c1.checkNum(1);
}


其中第3行checkNum的实参是变化的,名为1的MC是1,2的是2,3的是3。可能你还不明白为何这样做,来看看外部的Card.as文件吧:

[color=red]class Card {
  var locations:Array = new Array(6), cards:Array = new Array(), check_click_num:Array = new Array(0, 0);
  var i:Number = 1, temp:Number, num:Number, finish_num:Number = 0;

  function initialize():Void { //初始化一个长度为6的随机数组,让翻转的牌随机排列并透明度为0
    for(i = 0; i < locations.length; i++) {
      locations[i] = "lct" + (i + 1);
    }
    trace(locations);
    for(i = 1; i <= 6; i++) {
      temp = 6 - i;
      num = Math.floor(Math.random() * temp);
      cards[i-1] = locations[num];
      locations.splice(num, 1);
    }
    trace(cards);
    for(i = 1; i <= 6; i++) {
      eval("card" + i)._alpha = 0;
      eval("card" + i)._x = eval(cards[i-1])._x;
      eval("card" + i)._y = eval(cards[i-1])._y;
    }
    temp = 0;
  }

  function checkNum(clickNum:Number):Void { //检查两次翻转的牌标号是否一致和是否全部翻完了
    check_click_num[temp] = clickNum;
    temp ++;
    trace(clickNum);
    if(temp >= 2) temp = 0;
    if(temp == 0) {
      if(check_click_num[0] == check_click_num[1] != 0) {
        finish_num ++;

        eval("_root.card" + clickNum)._name = "";
        eval("_root.card" + (clickNum + 1))._name = ""; //标号相同,将这两张牌名字修改掉

        if(finish_num >= 3) _root.gotoAndStop(2);
        trace("ok");
        
      }
      else {
        for(i = 1; i <= 6; i++) {
          eval("_root.card" + i)._alpha = 0; //标号不同,则将所有没翻转正确的牌重新设置为不可见
        }
        check_click_num[0] = check_click_num[1] = 0;
      }
      trace("error");
    }
    trace("finish:" + finish_num);
  }
}[/color]

这是一个无构造器(缺省)的Card类,locations是一个长度为6的数组,在initialize()方法中,我实现了将locations[]初始化为值:lct1、lct2、……、lct6,然后利用它生成一个装有这些值的随机数组cards。
让我来解释一下它的算法吧:第1个for循环初始化locations[]的6个值,第2个for循环里是随机挑选一个locations[]的值,并把它塞入cards[]的末尾,然后再将其从locations[]里剔除,这样以后就防止cards[]出现重复的值。splice方法是增加或删除数组的一个元素,两个参数第1个是位置,第2个是数量。从这一点我们可以看出,Flash中的数组是可以基于链表访问并创建的,虽然它像Java一样只是某种意义上的底层实现,并不允许我们自己擅自使用。
第3个for循环将6个天幻网的坐标分配到舞台上的6个方形位置上去。最后temp重置为0,后面还会用到它。

checkNum方法是整个程序的关键,它用以验证两次翻转的牌是否一样。这里我使用一个长度为2的check_click_num数组,用它来装载每次翻转时传递来的参数。

temp每当大于2时自动归0,这样做的道理很明显:翻转1次牌自增1,翻第2次之后归0重翻,无论是否正确,都是要重新验证的。
后面的大串if语句很关键,当temp等于0时,看看传递来的两次clickNum参数是否相同且不为0。如果条件为真的话,说明这两次翻转的牌的内容是相同的,那就可以让计数器finish_num自增1了。当finish_num大于等于3时,也就是说翻转正确了3次,这个游戏就结束了。这就是后面那句if(finish_num >= 3) _root.gotoAndStop(2);的意思。
你可能会对中间两句修改牌的名字的语句发生疑惑,这将在下面解释。

上面说的是传递来的两次lickNum参数相同且不为0的情况,如果不同也就是说两次翻的牌不一样呢?很明显,这就是else里面的内容了:要将所有的牌的透明度设置为0,这样就不可见,从而重新翻转了。可是有一种情况你必须考虑,那就是前面已经翻转过来一些正确的牌,现在没翻对,那么那些正确的牌不也会随之变得不可见吗?这就是刚才所说的为何要修改其名字的原因,改掉之后设置透明度的语句就对它不起作用了。
最后一句是将check_click_num数组重新置0,来判断下一次翻转的情况。

注意程序里我使用了很多trace语句,我是故意把它留下的。trace是打印给你自己看的,并不会被玩游戏的人看到,这样方便自己调试程序。

好了,现在该说出那3个bug的时间了:
1.每轮翻牌时第二张牌都不可见,如果想让其可见一段时间该怎么办?
2.当翻转过来一对牌后,我继续翻它们俩(虽然它们已经被翻过来了,但仍可点击),这样也可以完成游戏,而别的牌根本不用翻。
3.一直点击其中的同一张牌,也能完成游戏。

听起来这些bug很可怕,尤其是最后1个。让我来依次说明它们的解决办法吧:

1.你可以将每张牌的MC设定为10帧,第一帧停止。每轮翻完牌后播放这两张牌,这样就实现了它们“滞留”一会儿的效果。当然,在此之间你不能翻转其它的牌。
2.可以设定一个临时数组,用以存放被翻过来的牌的传递参数。当新翻牌时的传递参数不存在于数组当中时再执行判定。也可以在每张牌的MC里添加一个临时变量,用以判断是否翻对。对了之后就不准再翻了(不能调用对象的checkNum()方法)。
3.这是最不好办的一个,我能想出来的解决办法就是依旧为每张牌的MC添加一个临时变量,翻过一次后不准再翻它,翻过两次后归0重新判断。

这并不是一个完美的思路,或许作为阅读者的你,能提出更好的算法,使它们的代码更简洁、更易读、也更易维护和修改。毕竟,我也只是个引路人——写这篇教程的目的就在于此——就像当初火光激发我的思维一样。