|
本翻译教程只在闪客帝国、经典论坛发表,如需转载,请与译者联系
这次给大家带来MOOCK在世界性FLASH大会:FF2K1大会上的演说,要说到ActionScript的权威,当然要数MOOCK,他写的ASDG(一本书)是可以和FLASH自带的AS字典相提并论的宝贝。虽然他是这样一个高高手,但是他的这个演讲却是非常的浅显,如果你某处读起来觉得难,那也一定是chocobo翻译得不好。(有些地方实在是太浅,我受不了就改了 ) 这篇文章预算的演讲时间很长,所以读起来也会觉得较长,你可以分开来读。 还有,本文是关于FLASH5的AS的,毕竟FLASH4的AS已经淘汰。
第一章:由O开始
为了示范,MOOCK将会做一个多选题问答的动画来做为例子。 这个例子以4种不同版本的方法来实现。 对于用户来说,4种版本使用起来将没有区别,只是对于我们开发者来说,研究的就是如何改进这些FLASH代码的结构与组织。
改进的重点在于:
*更便易的维护 *更便易的扩展 *更快捷的建设
要牢记的是,学习编程是一个过程(process)而不是一个事件(event)。 如果你第一次学习到的时候不能照着完成也不必担心。 接下来,MOOCK说到为了照顾没有多少编程经验的菜鸟,会由最最最简单的开始。
关于计算机语言: 编程语言是用来发信息给电脑,从电脑接受信息的 编程语言也有词汇表和文法,如人类的语言类似 通过编程语言我们可以告诉电脑去做什么,也可以从他那获得信息
关于语法和逻辑
学习编程的思想比学习一种语言的语法重要 假设FLASH是懂我们语言的单词的,我们对FLASH说:“Flash, 让一个球在屏幕里面弹来弹去吧” FLASH会什么都做不了,FLASH要我们用他的世界懂的东西,例如:movie clips, buttons, frames,来描述它要做的事。 那我们再说一次:“Flash, 让一个名字叫ball_one的movie clip在屏幕里面弹来弹去吧” 我们已经用MC这个FLASH懂的东西来描述了,但FLASH还是什么都不做,因为它需要更多的信息: *这个球有多大 *它放在哪里? *一开始它首先往哪个方向运动? *它初始的速度是多少 *它在屏幕的那一个范围之内弹来弹去呢? *弹来弹去要持续多久呢?
看来我们需要有逻辑结构地告诉FLASH它应该怎么去做:
1、一个球指的是一个叫ball_one的圆形MC,直径50像素 2、还有一个方形MC叫square,边长300像素 3、将ball_one放在square上某处 4、以一个随机的角度,75像素每秒的速度开始移动ball_one 5、如果ball_one接触到square的某边,就弹回 6、一直不停地运动下去,知道我们让它停 如果FLASH真的懂我们语言的单词,他应该知道如何开始做了
总结一下关键点:
无论是使用什么语言,编程思想的艺术都在于用如何将逻辑步骤列出 在你开始将要做的事用计算机语言描述之前,用我们人类的语言描述一次会很有帮助 即使你将他们翻译成任何的计算机语言,他们的逻辑步骤应该是一样的 电脑不懂用假设完成去你的想法,他们没有思考能力(chocobo:就是你要把要求完全无遗漏地写出来让它运行) 第二章:基础
首先了解如何在FLASH输入程序
在FLASH菜单Window里面Actions可打开ACTION面板,ACTION面板分右面的脚本窗,和左面的工具窗。 脚本窗就是放脚本的地方,而工具窗用于快速地输入各种ACTION,运算符,函数,属性,对象。 MOOCK建议大家使用专家模式,在FLASH菜单的Edit的Preferences的Actions panel里面选EXPert Mode,以后每次打开ACTION面板都会自动设置为专家模式(专家模式可直接输入代码,初学者学下去就会知道,很多代码无法在普通模式里输入)
AS的一些概念
所有代码都需存于某个地方,可以是frame(帧), button(按钮), or movie clip(电影夹子)。 只要你选择了某按钮或MC,你之后输入的代码就存放在它的上面了,注意此时ACTION面板上方提示为Object Actions。同理你也可以将代码存放于帧,不过此时提示为Frame Actions。 当你在专家模式的时候仍无法输入代码,你首先要检查你是否选择了frame, button, 或MC来存放代码。
然后我们象学所有语言一样吧,来个HELLO WORLD吧 在ACTION面板输入 var message = "HELLO WORLD"; trace (message); 然后我们按CTRL和ENTER键,看到结果了吗?
以下两章比较基础。MOOCK是骗演讲费吗?
第三章:解读
第一行代码:var message = "HELLO WORLD"; 告诉FLASH记住一个名字叫message的容器(通常在计算机里称为变量(variable),在FLASH里面,变量可以装文字,和可以装数字)里面装了这样的数据(datum),内容为:"HELLO WORLD" “=”等号是一个常用的运算符号(operators),在FLASH里面它不是代表相等,而是代表赋值 var 命令的含义是宣布给整个电影,此变量的内容是什么。
第二行代码:trace (message); 就是要FLASH显示message里面的内容,为什么FLASH能输出内容呢,因为当你按CTRL+ENTER又或者在网上打开这个电影的时候,你输入的每一段Action Scrpit代码,都会通过FLASH的解释器(interpreter)来解读然后执行 如果解释器能解释你的代码,他会运行它,并返回你要返回的结果,如果解释器看不懂你的代码,他也会返回错误代码——告诉你错误的原因
通常,我们发给解释器的命令不只命令这么简单,还包括命令的内容例如trace (message); 这个trace输出命令的内容就是message,计算机里就称为参数(argument或parameter),如果一个命令支持多个参数,参数之间一般用“,”来分割
第四章:还有一些概念
表达式(expression): (2 + 3) * (4 / 2.5) - 1可称为一个表达式,"a"+"b"也是表达式,需要解释器运算才得到结果的值,一个相反的概念,不需要运算的直接引用的,称为literal 条件(conditionals):(look at a series of options and make a decision about what to do based on the circumstances) 不翻译了,大概意思就是美女穿着新衣上街,会先看看外面会否下雨,这就叫条件判断啦 。 if(天气=="下雨"){trace("还是带把雨伞吧")}
循环(loop): 如果要输出5个数字,你是写5遍trace命令吗?更多的就不行了吧 你可以用while和for命令来完成重复的动作
事件(events): 放在frame里面的代码只要电影播放到就会执行,放在MC、button的代码则只在解释器发现预先设置好的事件被触动的时候执行。最典型的就是一下鼠标点击按钮造成press时间啦。
函数(functions): 将一组程序打包成一句命令来调用他,其实FLASH里面的很多命令就是函数:trace、play、gotoAndStop等等都是。 第五章 开始第一个版本的选择题的制作
多选题共有两道
·磁盘阵列(Disk Array)原理
如图,大家应该养成一个好习惯,用一个独立的层来放置代码,并把该层放到最上面会更便于修改。第二层则独立放置Label。不要觉得麻烦,只要你想到世界上大部分好的闪客都是如此做的,你就不会嫌麻烦了。以下的层放的是选择题的内容,如上图
开始创建题目
在question 1层的第一帧,写上题目"When were movie clips introdUCed into Flash?" (什么时候FLASH开始引入电影夹子的概念?) 再写上三个选项:Version 1, Version 2, Version 3 跟着我们再做一个用来选择的方框按钮,从Library里面拖出这样的三个按钮,分别放到各个选项的前面。 第二题的创建我们用第一题来做个模版,选择question 1层的第一帧,选择菜单上的Edit>>Copy Frames 再选择question 2的第十帧,Edit>>Paste Frames 将第一题复制过来了 当然问题要改成"When was mp3 audio support added to Flash?" (什么时候FLASH开始支持MP3格式导入?),答案也要改成Version 3, Version 4, Version 5
数据初试化
虽然是个很简单的FLASH,但是象其他复杂的FLASH一样,先告诉FLASH要用到的数据,例如题目的答案等,这是个好习惯,越复杂的FLASH,受惠越多 正常来说定义数据应该LOADING结束之后的。 为了把题目定在第一题,ACTION还需要一句stop(); 选择第一帧,在ACTION面板里面输入代码 // init main timeline variables var q1answer; // user 第六章:再来补充一点AS知识
数据分类是很有必要的,象8005154556这样的一个数字是没有多大意义的,但是如果将他归类到电话号码:800-515-4556,那就有意义了。(这是WHO的电话啊?是不是MOOCK的?呵呵)
AS里面数据的类型相对其他语言已经算少的,有: * 字符串String 由一系列的characters组成,可以包含字母,数字和符号,一般用双引号""扩住(记住不要写了全角的“”) * 数字Number * 布尔值Boolean 用于条件判断的时候,只有两个值true和false * Null and Undefined 这也是数据的类型之一,Null代表变量还没有内容,Undefined是连变量都未定义 * 数组Array 用来按顺序地存放一组数据 * MovieClip 这也是数据的一种吗?你就当它是FLASH特有的一种数据吧,装的就是一个个MC INSTANCE(解释一下吧,MC从library拖到场景中就是一个独立的instance,一个MC可以创立多个instance),还有MC里面的其他数据 * Object 可以是FLASH已经内部定义的,或者是用户写程序时自定义的一类数据
再分类一下 number, string, boolean, undefined, and null,这些属于简单的数据类型,特征是只有一个值。array, object, movieclip. 就可以包含不止一个值
chocobo:其实array也应该算是object,上面这些概念的东西总是有些沉闷,没关系,留下印象,以后用到了,自然会回过来真正了解一遍的
第七章:可以重复的函数(function)
不是用几个例子来示范吗?怎么还不讲例子啊?是的,下一个例子要用到函数,所以再补充点函数知识。(上当了吧?chocobo也上当了,开始翻译的时候没想到这篇东西这么长的,这才讲完第一个例子呢 55~)
一个函数是下面这个样子的: function funcName () { statements }
在ACTION面板里面,function这个单词是变色的,FLASH认识这个单词,它代表后面的就是描述函数的内容。funcName是函数的名字,当你以后要用这函数的时候,就要敲这个名字了(是的,建函数就是为了以后反复调用它) ()小括号里面是参数,什么是参数一会再讲 {}大括号里面的就是函数所要完成功能的一句句代码。
当我们建立函数之后,函数并不运行,只在你用函数的名字调用它出来,才正式开始运行。例如我们有这样一个函数: function sayHi () { trace("Hi!"); } 当我们在FLASH某帧需要sayHi的时候,输入AS: sayHi(); 是不是和最常见的stop();还有play();一样啊?因为它们都是函数嘛。
sayHi函数真是弱智,来个有意义的函数吧。先在场景里放好一个名字叫ball的instance(千万记得要给instance输入名字,否则函数运行没结果别找我) 在第一帧输入这样一个函数: function moveBall () { ball._x += 10; ball._y += 10; } 怕有人不懂,解释一下,_x代表ball的横坐标,ball._x +=10 的意思是ball._x = ball._x + 10 ,这样省略的写法还有 -= *= /= 等等。
再做一个按钮,按钮的AS: on (release) { moveBall(); } 好的,运行,从AS你可以看到,每点一下按钮,执行一次函数,而函数让小球下斜下移动。(FLASH的坐标轴,原点在左上角)
为什么要建立函数呢,就是想更有效率,现在有这个函数已经不用每移动一下写一次AS了,但还是不够,我们需要更有扩展性(flexibility)的函数。 这个函数只能移动叫ball的MC,只能向固定的方向移动固定的距离,我们需要可以移动任何MC,向任何方向移动任何距离的函数,这样可以省却很多输入AS的工夫(这就叫一劳永逸,呵呵)
我们的新函数有三个地方是每次调用都不一样的 1、移动的那个MC的名字 2、水平移动的距离 3、垂直移动的距离(呵呵,用极坐标,也可以把2、3说成移动的距离,和移动的角度,不过大家都不习惯极坐标)
为了告诉函数这些变化的地方,我们需要参数(parameters),参数在定义函数的时候就要输入,我们的函数改写好了: function moveClip (theClip, xDist, yDist) { theClip._x += xDist; theClip._y += yDist; } 当我们要实现原来函数功能的时候,现在调用新函数就变成 moveClip (ball, 10, 10);
定义函数的时候function moveClip (theClip, xDist, yDist) { 这里的theClip等参数(parameters)只是定义,本质上说是不存在的,因为没有内容嘛 当我们用moveClip (ball, 10, 10);调用的时候,ball就输入到theClip中去了,这里的ball称为arguments(偶读得书少,不会翻译) arguments可以是一个变量,也可以是一个表达式(例如"a"+"b",先得出结果再传输给函数)只要用逗号隔开各个参数就行
函数如何结束呢
正常来说,函数运行完 {}里所有语句结束,我们也可以用一句AS:return; 让它中途结束,例如: function say(msg) { return; trace(msg); } 这个函数被调用的时候将什么都不做就结束
return还有更重要的用途: function sqr(x) { // Squares a number 平方 return x * x; } a=sqr(2); //a将会被赋予2的平方 4 return更重要的用途就是返回数据
在AS里面,有个内建的函数Math.sqrt(就是说你敲入的时候会变色),其功能和我们刚才做的sqr函数是一样的,现在你该知道内建函数也没什么神秘的,我们一样可以做出相同功能的来。 第八章:第二个版本选择题的制作
读到这你发现什么,我是发现了,MOOCK不是在教AS,他简直是在上编程课。
在第一个版本的制作里你发现了什么缺点?是的,输入了很多次AS,很麻烦。 我们要做的是用函数来集中我们的代码,只有集中了才好修改,越复杂的程序越是如此(想象一下在很多个MC之间查代码,真是头痛,当然是集中一块好) 这个多选题,我们就归结成两个函数吧answer和gradeUser
代码(可以直接看源程序,在上面地址那个ZIP里面的quiz-version2.fla): 大部分的代码都被集中到第一帧了,千万不要被一大堆代码吓着了,代码这么长,只是为了让阅读者看得更清楚而已。(其实越短的代码才越可怕呢,呵呵) // Stop the movie at the first question stop ();
// Initialize main timeline variables 定义变量 var displayTotal; // Textfield for displaying user's score var numQuestions = 2; // Number of quiz questions var q1answer; // User's answer for question1 var q2answer; // User's answer for question2 var totalCorrect = 0; // Number of questions answered correctly 以上和例一一样 var correctAnswer1 = 3; // The correct choice for question 1 第一题的正确答案 var correctAnswer2 = 2; // The correct choice for question 2 第二题的正确答案
// Function to register user's answers 这个函数的功能是提交答题者的答案 function answer (choice) { answer.currentAnswer++; //现在answer.currentAnswer是1,正在提交的是第一题,下一次answer.currentAnswer就变成2,代表提交的是第二题 set ("q" + answer.currentAnswer + "answer", choice); // 不复杂不复杂,"q" + answer.currentAnswer + "answer"第一题的时候就是q1answer,第二题是q2answer,把参数choice传过来的值放到两个变量里面而已 if (answer.currentAnswer == numQuestions) { // 判断是不是两题都答完了,是就转到问题结束帧 gotoAndStop ("quizEnd"); } else { gotoAndStop ("q" + (answer.currentAnswer + 1)); } }
// Function to tally user's score 这个函数是改题的 function gradeUser() { // Count how many questions user answered correctly 将两个答案和正确答案比较,对就totalCorrect加一 //此处用了一个for循环,大家如有疑问的,可以查AS字典,在帝国就有中文版 for (i = 1; i <= numQuestions; i++) { // 下面用的eval有必要说一下,它的作用是将字符串和变量组成一个新的变量名,是个很方便的功能 if (eval("q" + i + "answer") == eval("correctAnswer" + i)) { totalCorrect++; } }
// Show user's score in an on-screen text field 将答案显示出来,与第一个例子同 displayTotal = totalCorrect; }
好了,第一帧的函数写好了,之后每个答案的选择按钮就简单了 例如第一题的选项一,就写: on (release) { answer(1); } 第二题的写法同上(如果你的选择题有很多道,做法都是一样的,只要复制第一题,然后把题目改了就行) 最后在quizEnd帧里面调用改题的函数gradeUser();
分析第二个例子是代码,你会发现比第一个例子精简了很多。
而集中在同一帧的代码,将: * 更容易修改 * 更容易升级 * 更少的出错机会 * 更容易查错 * 更精简(更少的字节数)
第九章:数组(arrays)
在下一个新版本的多选题里,我们将使用什么AS的特性,来让它更好呢?那就是数组。数组就是一系列的数据(MOOCK又开始上课了,chocobo的英文和计算机都不算好,为免误人子弟,概念性的东西尽量精简)
例如这样两个变量储存的数据: fruit1 = "oranges"; fruit2 = "apples"; 它们是互相独立的,使用起来很不方便,我们需要的是数组,以下是数组的定义方法,用“[]”框住,用“,”分隔开每个元素: fruitList = ["oranges", "apples"];
现在两个数据是放到同一个数组里面了,我们开始详细解说数组。 数组里面每一个数据称为元素(element)。而每一个元素都有个独立数字代表所处的位置,数字叫索引(index),注意! 第一个数据的索引是0,第二个才是1。 要按索引来提出数据,我们要用一个运算符[], 例如使用fruitList第一个元素赋值给a: a=fruitList[0]; 又例如将a的值赋给fruitList第一个元素: fruitList[0]=a;
当然[]里面也可以放表达式、变量: var index = 3; // Set numApples to 2 var a = fruitList[index]; 下面是个使用表达式的例子: // Create a myFrames array. Note the legal formatting. 建立一个记录LABEL的数组 var myFrames = ["storyEnding1", "storyEnding2", "storyEnding3", "storyEnding4"];
// Set randomFrame to a randomly picked element of myFrames // by calculating a random number between 0 and 3 // 随机从数组中提取一个LABEL var randomFrame = myFrames[Math.floor(Math.random() * 4)];
// Now go to the random frame // 然后跳到该LABEL播放 gotoAndStop(randomFrame);
而数组包含数据的个数称为长度(length),例如fruitList.length 就等于2
对数组最常用的处理就是从数组中选出有用的数据了,来看一个运用循环的例子: // Create an array 建立数组,里面放了一些歌的类型 var soundtracks = ["electronic", "hip hop", "pop", "alternative", "classical"];
// Check each element to see if it contains "hip hop" // 一个循环,检查每一个元素是否等于"hip hop"这个类型 // 另外,请留意此处MOOCK对FOR的写法,J=0之前有一个VAR,这好象可有可无,其实是一个好习惯! for (var j = 0; j < soundtracks.length; j++) { trace("now examining element: " + j); if (soundtracks[j] == "hip hop") { trace("the location of 'hip hop' is index: " + j); break; // 跳出循环,找到了就不用再找了 } }
关于数组的方法(method)
方法就是从属于某一对象(object)的函数,通常都是对该对象进行处理的函数。 好象太抽象了?我们还没讲到什么是对象,其实数组是对象的一种,我们就暂且将数组的方法理解为一个专门处理数组内数据的结构和内容的工具吧。 例如一个叫push()的方法就是一个工具,用于为数组添加一个元素,并且加在该数组的最后。使用起来并不复杂,看例子就知: // Create an array with 2 elements var menuItems = ["home", "quit"];
// Add an element 加一个元素 // menuItems becomes ["home", "quit", "products"] // 现在数组的结构变成["home", "quit", "products"] menuItems.push("products");
// Add two more elements 这次是加两个 // menuItems becomes ["home", "quit", "products", "services", "contact"] menuItems.push("services", "contact");
跟push()相反从最后弹出一个元素的方法是pop() 而跟push()类似,但是是将一个元素加到数组的开头的方法是unshift(),与之相反的是shift()。
方法sort和reverse,用于重新排列数组的元素 方法splice 用于从数组中间删除某元素 方法slice和concat 可以在某些数组的基础上生成另一个新的数组 方法toString和join 可以将整个数组变成单一个字符串
以上方法都可以从AS字典里面查到
第十章:第三个版本的选择题
首先,此版本沿用了上一版本的函数answer和gradeUser 在这一版本中,用户的答案与正确答案将使用数组来存放
看看我们的新代码: stop(); // *** Init main timeline variables var displayTotal; // Text field for displaying user's final score var numQuestions = 2; // Number of questions in the quiz var totalCorrect = 0; // Number of correct answers // 上一版本中,用户答案使用了两个变量来存放,但是试想如果是10题、100题呢?使用数组将更容易管理,也更容易处理 var userAnswers = new Array(); // Array containing user's guesses 这是定义数组的语句,但是还未输入数据 var correctAnswers = [3, 2]; // Array containing each correct answer 这一句既定义数组,同时输入数据,因为正确答案是已知的
// *** Function to register the user's answers function answer (choice) { // Tack the user's answer onto our array 将数据PUSH进数组,因为是顺序答题,所以用方法PUSH userAnswers.push(choice); // Do a little navigation, baby // 如果答案数超过题目总数,自然就跳到quizEnd帧了 // 注意在本例中,已经不用上例的answer.currentAnswer而是使用userAnswers.length来控制问题是否结束 // 我们甚至可以用correctAnswers.length来代替numQuestions,记录正确答案数组的长度,不就是题目总数吗? if (userAnswers.length == numQuestions) { gotoAndStop ("quizEnd"); } else { gotoAndStop ("q"+ (userAnswers.length + 1)); } }
// *** Function to tally the user's score function gradeUser() { // Count how many questions were answered correctly. // 开始改题,这里就不用再用上个版本的eval啦,那个东东实在是难懂兼难用,这个版本相对就很清晰明快 for (var j = 0; j < userAnswers.length; j++) { if (userAnswers[j] == correctAnswers[j]) { totalCorrect++; } } // Show the user's score in a dynamic text field displayTotal = totalCorrect; }
电影的其他部分不用改动(这就是使用FUNTION的好处啦,升级多快~)
OK,进入下一章之前想想目前版本的弱点 * 题目,每次修改题目都要进入FLASH的场景修改,麻烦 * 按钮,每题就要做三个按钮
这都是麻烦的地方,我们要更精益求精地修改,让我们的多选题,轻易地从两题变成10题、100题!接下来我们要做的是: * 进一步改进我们存放数据的结构 * 让我们可以动态地生成每一道题目,只需输入数据,而不需要在FLASH里面操作就可以自动生成
所以——我们需要面向对象编程!(object oriented programming)
chocobo:嘻嘻,众菜鸟是不是都倒了,AS基础教程竟然开始讲OOP了,呵呵,没关系啊,上面的教程一直都这么浅,以后也深不了 第十一章:一点面向对象编程知识
At is heart, OOP simply means that you treat portions of your program as self-contained, but interrelating modules called objects. 这是什么呀?我不翻译了,概念的我们就先不懂吧,对象主要构成包括属性(properties)和方法(methods)。一个对象通常都以现实世界里的某个东东做蓝本。 例如我们可以定义一个对象叫球,那么这个球将会有这样的属性:半径、X坐标、Y坐标、颜色。同时,也会用属于球自己的方法例如:移动球、计算球的面积
当然,我们还可以定义一些相对抽象的对象,例如我们要做的多选题
所有的对象都属于某类(class),类的意思其实是用于创建对象的模版
一个实例(instance)就是某一个类的特定某一个case(好复杂,概念性的东西我翻不过来啦,反正实例就类创建出来的某一个对象)
还是再举例吧 * 例如我们有个一个叫Chair的类 * 这个类定义了一个东西需要它有四条腿,一个坐垫 * 然后我们就可以用Chair这个类来定义我们的不同对象(可理解为椅子的款式),每个对象就有它特有的高、宽、材料、重量、颜色,正是这些属性使每个对象互相区别。
所有的椅子互相区别,有自己的属性,但是他们又有同样的结构:四条腿、一个坐垫
OK,那AS里面的类和对象呢? 是的,AS里我们可以自己创建对象,也有可以使用的内建对象 内建的类(你可以用它来创建对象)包括:Array, Boolean, Color, Date, MovieClip, Number, Object, Sound, String, XML, XMLSocket 内建的对象(已经可以直接使用的对象)包括:Arguments, Key, Math, Mouse, Selection 内建的类、对象当然都是在FLASH里面有自己功能的东西。而正是这些功能非常常用,FLASH才内建了这些类和对象,例如: Mouse.hide(); // Hide the mouse pointer 将鼠标隐藏,你经常用吧?现在才知道其实是内建的Mouse对象的一个hide()方法吧?
在学习如何创建自己的类和对象之前,先来了解一下内建类和对象是怎样工作的吧
与数组的结构类似,对象是容器们(containers)的容器(container) 一个对象,用各个独立的属性来存放数据,只不过数组区分每个容器是用数字,而对象则是用属性名,要调用一个数组里面某个数据,我们需要它是索引值,而要调用对象的属性,则要知道属性名 看看以下这个例子,这是个叫BALL的对象:
点击查看大图
BALL对象有两个属性:radius 和 color 而两个属性分别赋值为:50 和 0xFF0000 (这是AS里面表达16进制的方法)
概念清楚了,说一AS里面使用对象要注意的地方
首先,对象的属性很灵活,它储存的数据可以是strings(字符串), numbers(数字), booleans(布尔值), null(空), undefined(未定义), functions(函数), arrays(数组), movie clips(电影夹子), or even other objects(甚至是其他的对象,包括自定义的).
调用属性,可用点语法:objectName.propertyName 例如我们赋值给BALL里面的属性radius: ball.radius = 100;
我们也可以用符号[]来访问属性,但是[]里面的属性名需用双引号扩住,同上例: ball["radius"] = 100; (所以说数组也是对象一种,访问方法也一样,并没有搞特殊化) 与点语法比较,[]更灵活,可以动态地改变属性的名字,里面可以是变量或表达式,例如: var propName = "radius"; ball[propName] = 100; 这点是点语法无法办到的
纯粹的OOP中,我们几乎不会直接访问对象的属性 我们一般都使用方法(methods) ,来改变属性 例如改变mySound这个声音对象的音量属性 AS的用法应该是mySound.getVolume(100); 而不是mySound.volume=100; 但是为了简单,多选题这个例子里不一定遵循这个原则,事实上AS里面很多对象都不可以办到这点,所以有人说AS不是面向对象语言。
关于对象的方法
方法method其实是一些附属于对象的函数,其作用主要是访问对象中的数据,或完成某种功能。所以调用方法和调用普通函数类似:objectName.methodName() 只是前面加上了对象的名字。 例如上面的BALL对象,我们需要它的面积,使用 getArea 这个方法: ball.getArea();
预习一下,在下一个例子中,虽然MOOCK不打算建立自己的方法,但是将使用内建于MovieClip对象的两个方法: *MovieClip.attachMovie() 此方法是将Library(CTRL+L按出来的那个)里面的symbol复制一个instance到场景中 (chocobo:给不懂概念的人紧急补习,放在库(Library)里面的叫符号(symbol),拉到场景中就叫实例(instance),一个符号可创建多个实例,符号改变则实例随之改变) 与之类似的是MovieClip.duplicateMovieClip(),但是该方法需要事先在场景里已经有一个实例
*MovieClip.removeMovieClip() 此方法与上面相反,是删除场景里面的instance的
大家看到了,在下一例中,我们将使用MovieClip这个内建对象,因为它就可以满足我们的需要,不必新类型的对象了 我们先来熟悉一下MC这种对象
它有很多属性_height(高度)、 _alpha (透明度),MC的属性大家可以在AS字典里面查到,在AS面板里这写属性也是变色显示的。同时MC的方法,例如 MC.play(); 等也可以查到。当我们在MC里面的时间线使用这些属性、方法的时候,可以省略不写前面部分,直接写play();
MC可以互相嵌套,MC当中又有MC,就出现类似: mc1.mc2.play(); 的情况 为了调用包含自己的上一级MC的方法、属性,AS里面用_parent,例如: 在mc1包含mc2 ,在mc2中想使mc1播放:_parent.play(); 还有另一个保留字:_root指代该电影的主时间线 例如想在某个mc里面让整个电影播放:_root.play(); 想在某个MC中让另一mc1播放:_root.mc1.play();
还有,MC中定义的变量,将作为MC的一个属性可被访问 在mc1里面写了一句 var a=1; 我们将可使用 mc1.var来调用它
关于类(class)
这个概念大家还是不大懂吧?类就是定义一个对象将拥有什么的方法跟属性的东东,类似MC里instance与symbol的关系,instance的结构就是由symbol决定的,每个instance又可以不一样 AS里面没有专门定义类的例如class这样的关键字,我们使用函数来定义类,这种函数称构造函数constructor function,函数的作用就是产生我们定义好的类的实例(就是对象) 举例最实在: // make a Ball constructor 最简单的构造函数 function Ball () { // do nothing 里面是空的 } 现在我们可以定义新类型Ball的对象了 myBall = new Ball(); 语法就是前面是新对象实例名,等号后是new加构造函数名 myBall就拥有了Ball定义的一切结构了(虽然Ball里面是空的,嘻嘻)
不过不是所有对象创建都用new,例如mc创建就用的是attachMovie()或duplicateMovieClip()
好了,我们的构造函数总不能空的,如何定义类,让新对象有自己的属性呢? 使用this这个关键字,看例子,新的构造函数: function Ball () { this.radius = 10; this.color = 0xFF0000; this.xPosition = 35; this.yPosition = -4; } 以后我们定义出来的新对象,都拥有半径,颜色、XY坐标属性啦 慢着,怎么每个新对象都一模一样啊? 再改,用函数的参数来定义动态的属性: // Make the Ball constructor accept // property values as arguments. function Ball (radius, color, xPosition, yPosition) { this.radius = radius; this.color = color; this.xPosition = xPosition; this.yPosition = yPosition; } 我们可以定义不同属性的对象了 myBall = new Ball(6, 0x00FF00, 145, 200);
本教程关于还有创建方法以及如何在类之间继承方法和属性没讲, 无论如何,OOP都不可能在这么短时间之内说清楚, 但这不妨碍我们做下一个例子了。 第十二章:第四个版本的选择题
第三个版本的时候我们已经设想好,新版本中题目将是动态生成的,不用我们在FLASH的场景里面一题一题输入了,我们要做的只是输入题目和题目答案的数据就够了。
很明显,每一条题目都将是一个对象(不然我们学这么多对象的知识干嘛?),而这些所有的题目,会用一个数组来存放
再重提一下,可配合源程序学习 http://www.moock.org/webdesign/lectures/ff2001sfWorkshop/moockQuizzes.zip
好,开始设计题目的模版
模版就是一个MC,包含两个TEXT FIELD,里面不用填东西,分别起变量名为:(FOR小鸟:TEXT FIELD就是按工具条里T按钮拉出来的文本框,同时还要在文本面板(ctrl+t)里将其改为Dynamic Text,变量名则在面板的Variable处改) * qNum (以后将显示题目的编号) * qText (以后将显示题目的正文) 我们还要在库里面做标识,点一库面板(ctrl+l)右上的Options>> Linkage ,选第二个Expert this symbol,identifier填上questionTemplate,至此,题目模版完成
再制作选项的模版
选项模版应包括一个选择用的按钮 还有该选项的内容,一个起名为answerText的TEXT FIELD 在本例的后面,将为每一个动态生成的选项一个唯一的名字,譬如: "answer0", "answer1",..."answern". 答题者所选定的答案将由这个名字来决定,调用一个MC的名字,用的是_name这个属性 所以答题的按钮上面的AS为: on (release) { // Trim the prefix "answer" off this clip's name // 下面使用了String.slice()方法,例如_name为answer0,它将被处理成0,slice的具体语法请查阅AS字典 // 按钮提交什么由该MC的名字决定的,我作个标记 @@ ,记得一会看回来 choice = _name.slice(6, _name.length); // 与前面的例子一样,最后将答案提交给answer函数处理,不过现在我们是在某一MC里面用外面主时间线的函数了,所以得加上_root _root.answer(choice); } 最后,Options>> Linkage,标识名:answerTemplate,制作模版的工作就完成了
下面将是放在第一帧的程序主体,可要打起精神来了:
// Stop the movie stop(); // Init main timeline variables var displayTotal; // Text field for user's final score var totalCorrect = 0; // Number of questions answered correctly
// Array containing the user's guesses 记录作答答案的数组 var userAnswers = new Array();
// Number of the question the user is on 记录正在作答中题目的编号 // 要注意的是,它是由0开始的,第一题的编号是0,因为我们要用到数组,数组的第一个编号是0,所以这里我们也用0 var currentQuestion = 0;
// The Question constructor // 以下是新类型对象question的构造函数,包含三个属性:正确答案,题目正文,各个选项 function Question (correctAnswer, questionText, answers) { this.correctAnswer = correctAnswer; this.questionText = questionText; this.answers = answers; }
// Import the source file containing our array of question objects // 咦?应该是输入各条题目的数据先啊,放哪去了?因为嘛,数据输入是个与编程无关的过程,为了让代码更优雅,这些繁琐的东西扔别地方去了,AS太长,会使查阅相当麻烦,分开存放也是好习惯! // #include是引用外部AS命令,可以将AS分开储存于各个后缀名为AS的文件中,输入题目的代码就是放到了questionsArray.as中(记得和FLA放在同一目录下喔) #include "questionsArray.as"
//// 我改变了一下教程的结构,把questionsArray.as的内容也插入进来了,因为跳过这段的话,看起来会有疑问 //// 以下内容系存放questionsArray.as中的
// 输入数据其实是建立对象 // MOOCK用一个数组还存放这些对象,这样对象才更易于管理 // 不要被括号给弄昏了,输入对象参数的中间还有中括号,是因为输入题目的参数“各个选项”是一个数组 // 因为是存放于数组中,每个对象之间记得应有逗号分隔 // Remember to place a comma after each object // in the array except the last
questionsArray = [new Question (2, "Which version of Flash first introduced movie clips?", ["version 1", "version 2", "version 3", "version 4", "version 5", "version 6"]),
new Question (2, "When was ActionScript formally declared a scripting language?", ["version 3", "version 4", "version 5"]),
new Question (1, "Are regular expressions supported by Flash 5 ActionScript?", ["yes", "no"]),
new Question (0, "Which sound format offers the best compression?", ["mp3","aiff", "wav"]),
new Question (1, "True or False: The post-increment operator (++) returns the value of its operand + 1.", ["true", "false"]),
new Question (3, "Actionscript is based on...", ["Java", "javascript", "C++", "ECMA-262", "Perl"])];
//// 离开questionsArray.as部分,我们继续
// Begin the quiz 出题目!调用makeQuestion函数来完成,我们只需要给这个函数一个参数:题目的编号,函数就会按编号提取数据,结合我们刚才做的模版生成题目。 makeQuestion(currentQuestion);
// Function to render each question to the screen // 下面就是makeQuestion函数 function makeQuestion (currentQuestion) {
// Clear the Stage of the last question //这句是清理上一题生成的MC,这句从第二题开始生效,questionClip就是题目的MC名,MC从哪来的?看下面就知道了 questionClip.removeMovieClip();
// Create and place the main question clip // 利用模版questionTemplate生成一个叫questionClip的MC,这个MC就是我们的问题 attachMovie("questionTemplate", "questionClip", 0);
// 设定MC的位置 questionClip._x = 277; questionClip._y = 205;
// 把题目编号输入MC的qNum文本框中 questionClip.qNum = currentQuestion + 1;
// questionsArray[currentQuestion]就是数组questionsArray里的第currentQuestion个对象,例如currentQuestion是0,那么就是我们的第一条题目 // questionsArray[0].questionText就是第一条题目对象的问题属性 // 然后问题输入MC的qText文本框中 questionClip.qText = questionsArray[currentQuestion].questionText;
// Create the individual answer clips in the question clip // 以下循环将结合选项模版生成这一条题目的各个选项的MC
// questionsArray[currentQuestion].answers记得吗?选项这个属性可是个数组,所以我们把它的长度作为循环的次数,这个数组有多大,我们就生成多少个选项 for (var j = 0; j < questionsArray[currentQuestion].answers.length; j++) {
// Attach our linked answerTemplate clip from the Library. // It contains a generalized button and a text field for the question. // 用answerTemplate做模版生成MC,MC名为"answer" + j ,即第一个选项MC名为answer0,第二个为answer1,选项的名字可是关系到按钮选什么的,如果你忘了,看看上面有 @@ 标记的地方 // 同时它们的深度为j,每次不同
// 但和上面不同的是,我们要把选项MC生成到题目MC里,这样我们清除题目MC的同时也清除了选项 // 所以写成questionClip.attachMovie questionClip.attachMovie("answerTemplate", "answer" + j, j);
// Place this answer clip in line below the question. // 设定MC的位置,第一个高度为70,之后顺序加15 questionClip["answer" + j]._y += 70 + (j * 15); questionClip["answer" + j]._x -= 100;
// Set the text field in the answer clip to the appropriate // element of this question's answer array. // 下面语句的:questionClip["answer" + j]可不是指数组,j=0的时候,它代表questionClip.answer0,具体解释可见上一章。 // 这句语句的作用是把questionsArray[currentQuestion]这个对象数组里面的answers[j]数组,输入到对应的选项MC的answerText文本框中,就是该选项的内容 questionClip["answer" + j].answerText = questionsArray[currentQuestion].answers[j]; } //生成选项的循环结束
}
// Function to register the user's answers // 以下是记录答题者答案的函数,记录在数组userAnswers中,和上一例同 // 每一个选项如果被选都会调用此函数,并用参数choice传来作答的答案 function answer (choice) { userAnswers.push(choice);
// 判断是否题目全部完成,是的话清楚掉题目MC,并跳转到quizEnd帧 if (currentQuestion + 1 == questionsArray.length) { questionClip.removeMovieClip(); gotoAndStop ("quizEnd"); } else { // 在这里改变题目的编号,然后调用makeQuestion函数再次生成新的题目 currentQuestion++; makeQuestion(currentQuestion); } }
// Function to tally the user's score // 改题的函数 function gradeUser() { // Count how many questions the user answered correctly for (var j = 0; j < questionsArray.length; j++) { // 将答题数组userAnswers[j]与问题数组questionsArray[j]的属性correctAnswer逐个比较 if (userAnswers[j] == questionsArray[j].correctAnswer) { totalCorrect++; } } // Show the user's score in an onscreen text field // 显示答对与题目数比 displayTotal = totalCorrect + "/" + questionsArray.length; }
好了,我们来总结一下这个例子吧
我们已经完成了第三个版本结束时候定下来目标,更快捷的建设,更便易的扩展 我们的题目跟选项都是动态生成的,也就是说生成10、100题或更多题目也只需要修改questionsArray.as这个文件就可以了 如果说生成两题这个例子用时要长于上面的例子,那么生成100题,你会发现用这个例子将是最快的。还有,在100题里面修改其中一题,也是最快的。
看完了例子,希望大家不是只明白了代码的含义,最重要是理解运用对象来编程的方法。同样的例子,其实还可以用其他的对象结构来完成的,更有效地组织数据也是编程艺术的一部分。
为了更进一步改进我们的例子,最后一个,就是第五个版本,将用XML代替那个存放对象的数组(也就是在questionsArray.as里那个),它实在太难懂了,是不是啊?呵呵 XML,名字是不是很COOL啊,其实,FLASH里面用XML不难(其他地方的应用就……所以千万别说XML不难,是FLASH里面用不难),呵呵,好吧,开始吧。
十三章:XML
XML是一种标记语言,通常用于储存,组织和传输数据
XML文档主要由一系列的元素(elements)和属性(attributes)组成,看下面一个XML的例子:
<BOOK> <TITLE>ActionScript: The Definitive Guide</TITLE> <AUTHOR SALUTATION="Mr.">Colin Moock</AUTHOR> <PUBLISHER>O'Reilly</PUBLISHER> </BOOK>
这个例子就是由元素 BOOK, TITLE, AUTHOR, PUBLISHER 组成的
在元素<AUTHOR SALUTATION="Mr.">里就包含了一个属性:SALUTATION
这些元素怎么让浏览器解释是什么意思呢?它需要DTD,一套决定这些标记的意义的规则。(例如我们常听说的WML、SVG,它们都是XML,但对应不同的DTD)
XML与Html想比要求格式更严格,格式要求: * tags 一定要嵌套 (就是说有<book>就一定要有</book>) * 一定要有一个根元素 (例如例子中的 BOOK) * 开始部分要用XML声明标记 :<?xml version="1.0"?>
但是AS里面的XML不需要DTD(这就是我说FLASH里面用XML不难的原因,哈哈)
从我们面向对象的角度来看,我们XML的内容可以当做为对象,下图就是我们建立XML对象的层次结构
点击查看大图
FLASH已经内建有XML类让我们可以定义自己的XML对象,同时XML对象还有很多方法。
我们还是更进一步用例子分析吧,如果我们建立了如上图的XML对象,那么FLASH首先会自动建立一个元素DOCUMENT,下面才是我们自己的元素。
本来是第一个元素的BOOK成为了DOCUMENT的第一个节点(node),不过我们把它继续当我们XML数据的根也无妨
当一个节点包含于另一个节点时,这个节点称为另一节点的子节点(child),反之另一节点称为其的父节点(parent)
例子中BOOK就是DOCUMENT的child,DOCUMENT就是BOOK的parent
再看图,BOOK有7个子节点,是不是和你想象不同?多了四个节点#text,因为FLASH把标记之间的空格和回车也读成一个节点了。
几个子节点的关系成为兄弟(siblings),如果要找AUTHOR的下一个兄弟(next sibling),FLASH就会给你找来#text
这可不是我们想要的,解决的方法 * 直接在XML里面把空格回车都删除掉,就是说一个TAG紧挨着一个 * 用AS把无用的子节点删除 * 在FLASH读入XML源数据之前,将该XML对象的一个属性ignoreWhite设置为true,但是该属性只在R41版本的PLAYER生效(注:网上可以更新的版本为R41,但是随FLASH附带的FLASHPLAYER的版本是R30)
再回到我们的例图,三个子节点下面还有子节点,最尾的节点也可以叫叶节点。
但是图里面还有个东西我们没找到,就是AUTHOR的属性SALUTATION,属性不是该节点的子节点,要访问我们属性,要用XML.attributes
概念先说这么多,现在看看我们怎么把XML源程序输入进FLASH
首先定义一个新的XML对象了: myDocument = new XML(); 这个对象是空的,我们通过appendChild, parseXML, 和 load 三种方法来输入数据 当然我们也可以在定义的时候就输入数据: myDocument = new XML('<P>hello world!</P>'); 这时候我们的myDocument就有了一个叫P的子节点,P的叶节点是hello world! 之后我们就可以访问这个XML对象了,firstChild这个XML属性指向第一个子节点,childNodes是XML对象的子对象,指向所有的子节点 myDocument.firstChild // Accesses P myDocument.childNodes[0] // Also accesses P 两个AS语句指向的都是节点P
要访问叶节点的内容需要属性nodeValue 我们要显示P节点的子节点的内容,就要写成: trace(myDocument.firstChild.firstChild.nodeValue); 要给它赋值: myDocument.firstChild.firstChild.nodeValue = "goodbye cruel world"; 要删除P节点,用方法removeNode: myDocument.firstChild.removeNode(); 新建一个节点叫P,用方法createElement创建元素: newElement = myDocument.createElement("P"); 再将该元素加进去作为一个节点,用方法appendChild: myDocument.appendChild(newElement);
做一个叶节点方法类似: newText = myDocument.createTextNode("XML is fun"); myDocument.firstChild.appendChild(newText);
更详尽的方法还是查阅AS字典吧 第十四章:最后一个版本选择题
上一个版本面向对象的代码对我们这个基于XML的版本很有帮助,上一个版本我们是用对象的数组来存我们的数据,这个版本里面,我们使用外部的XML文件 下面先看看XML文件的结构: <QUIZ>
<QUESTION TEXT="at which version of flash were movie clips introduced?" ANSWER="2"> <CHOICE>version 1</CHOICE> <CHOICE>version 2</CHOICE> <CHOICE>version 3</CHOICE> <CHOICE>version 4</CHOICE> <CHOICE>version 5</CHOICE> <CHOICE>version 6</CHOICE> </QUESTION>
<QUESTION TEXT="when was actionscript formally declared a scripting language?" ANSWER="2"> <CHOICE>version 3</CHOICE> <CHOICE>version 4</CHOICE> <CHOICE>version 5</CHOICE> </QUESTION>
<QUESTION TEXT="are regular expressions supported by flash 5 actionscript?" ANSWER="1"> <CHOICE>yes</CHOICE> <CHOICE>no</CHOICE> </QUESTION>
<QUESTION TEXT="which sound format offers the best compression?" ANSWER="0"> <CHOICE>mp3</CHOICE> <CHOICE>aiff</CHOICE> <CHOICE>wav</CHOICE> </QUESTION>
<QUESTION TEXT="true or false: the post-increment operator (++) returns the value of its operand + 1." ANSWER="1"> <CHOICE>true</CHOICE> <CHOICE>false</CHOICE> </QUESTION>
<QUESTION TEXT="actionscript is based on..." ANSWER="3"> <CHOICE>java</CHOICE> <CHOICE>javascript</CHOICE> <CHOICE>c++</CHOICE> <CHOICE>ecma-262</CHOICE> <CHOICE>perl</CHOICE> </QUESTION> </QUIZ>
这个XML里面,QUIZ是我们的根元素。每一题都放在QUESTION元素内,题目正文为其属性TEXT,正确答案为其属性ANSWER(ANSWER=1代表选第二个答案) 每一题的选项则是QUESTION的子节点CHOICE。其实根本就不用解释,大家直接看都能看懂。
使用了外部XML之后,我们升级题目只需改动XML文件即可,而上一个版本,修改外部AS文件之后还是需要EXPORT一次。
这个版本里面,将保留上个版本大部分的代码,除了输入题目数据的部分,将用XML来代替。 以下代码写到questionsArray.as中覆盖其原来内容: //首先仍然定义一个数组来存放数据 var questionsArray = new Array(); //然后我们定义一个XML对象来存放XML数据 var quizDoc = new XML(); //之后是建立将XML解释为我们存放题目的对象格式的函数buildQuestionsArray(),同时将它连接到新建的XML对象的onLoad函数,让XML下载完成之后执行这个函数 quizDoc.onLoad = buildQuestionsArray //然后是执行下载XML的AS quizDoc.load("quiz.xml");
最后我们详细解说一下解释XML的函数 // *** builds an array of question objects based on the dom tree in quizDoc function buildQuestionsArray () { // first, strip unwanted whitespace nodes from the tree. // 除去无用的节点,上一章已经有介绍无用节点是如何出现的 stripWhitespaceDoublePass(quizDoc);
// now assign a convenient reference to the root QUIZ node // XML文件的根节点QUIZ节点就是 quizDoc.childNodes[1],这里将其指名为quizNode,以便运用 var quizNode = quizDoc.childNodes[1]; // for each question node that is a child of the QUIZ node... // 下面的循环将逐个提取QUIZ节点的子节点,即每条题目 for(var k = 0; k < quizNode.childNodes.length; k++) { // make an array of the text nodes from each CHOICE node // 为每条题目建立一个选项数组 var choicesArray = new Array(); // 下面的循环则是将题目的子节点,即各选项的nodeValue输入到choicesArray数组中 for(var j = 0; j < quizNode.childNodes[k].childNodes.length; j++) { choicesArray[j] = quizNode.childNodes[k].childNodes[j].firstChild.nodeValue; }
// construct a question object for each QUESTION node, // and store it in questionsArray // 用题目正文、选项数组、正确答案(正确答案目前还是字符串,所以用Number函数将之转为数字)作为参数,建立Question对象(定义Question对象的代码已经在上个例子中解释了) // 将新建的Question对象作为questionsArray数组的一个元素 questionsArray[k] = new Question ( Number(quizNode.childNodes[k].attributes.answer), quizNode.childNodes[k].attributes.text, choicesArray); }
// done loading and processing the quiz questions loadMsg = "";
// begin the quiz // 调用函数makeQuestion,之后的进度就同上一个例子了 makeQuestion(currentQuestion); }
// *** Strips whitespace nodes from an XML document // *** by passing twice through each level in the tree // 下面函数用于除去无用的空白节点,参数是需要处理的XML的根元素(我们已经将其指名为XMLnode了) function stripWhitespaceDoublePass(XMLnode) { // Loop through all the children of XMLnode // 循环依次将根元素的子元素提取出来 for (var k = 0; k < XMLnode.childNodes.length; k++) { // If the current node is a text node... // 如果该节点是一个文本节点,就开始以下检查 ... if(XMLnode.childNodes[k].nodeType == 3) {
// ...check for any useful characters in the node. var j = 0; var emptyNode = true; for(j = 0;j < XMLnode.childNodes[k].nodeValue.length; j++) { // A useful character is anything over 32 (space, tab, // new line, etc are all below). // 因为空格、TAB或换行等空白无意义字符的ASCII码都小于32,检查若大于32,即为有数据的节点,同时用break;跳出检查的循环 if(XMLnode.childNodes[k].nodeValue.charCodeAt(j) > 32) { emptyNode = false; break; } }
// If no useful charaters were found, delete the node. // 若该节点没有数据,就是解释时的错误,将其删除 if(emptyNode) { XMLnode.childNodes[k].removeNode(); } } }
// Now that all the whitespace nodes have been removed from XMLnode, // call stripWhitespaceDoublePass on its remaining children. // 但是还没完,我们只检查了所有子节点,而没有检查子节点的子节点,所以以下的循环将所有子节点也送到本函数再继续检查 // 这种函数自己调用自己的方法,称为递归,它将一直检查子节点的子节点的子节点的……一直到该节点没有子节点为止 for(var k = 0;
k < XMLnode.childNodes.length; k++) { stripWhitespaceDoublePass(XMLnode.childNodes[k]); } }
现在,我们可以把SWF和XML组成的题目交给一个不会编FLASH的老师了,他只要就会用记事本修改XML文件就行了
全教程完
(出处:网侠)
|