MaxScript学习笔记目录
大家好,我是阿赵
之前写了一些MaxScript的学习笔记,里面实现的功能不算很复杂,所以都是使用了偏向于面向过程的方式去编写的。
我本人其实是比较习惯用面向对象的方式去编写代码。关于面向过程和面向对象之间的优缺点对比,各位如果不是很熟悉的话,有空可以去自行查询了解一下。按我自己的理解简单概括一下:
1、面向过程运行的效率高,但如果代码逻辑复杂的时候,修改和维护的难度会比较大
2、面向对象性能较差,内存也占用得比较多,但它易于管理和维护,扩展性好
在编写MaxScript的时候,可以用Struct结构体来部分实现面向对象,下面来介绍一下。
一、Struct的基础使用
1、例子:
在说理论之前,先看一个简单的例子:
(
local TestPerson
local PrintPerson
struct person (name,age,sex,height = 175)
fn TestPerson =
(
local Tom = person()
Tom.name = "Tom"
Tom.age = 23
Tom.sex = "male"
Tom.height = 180
PrintPerson Tom
local Bill = person name:"Bill" age:19 sex:"male"
PrintPerson Bill
)
fn PrintPerson obj =
(
local content = "";
content +=("name:"+ obj.name+"\n")
content +=("age:"+ (obj.age as string) + "\n")
content += ("sex:"+obj.sex+ "\n")
content += ("height:"+(obj.height as string)+"\n")
print content
return "ok"
)
TestPerson()
)
运行代码,可以看到打印输出:
"name:Tom
age:23
sex:male
height:180
"
"name:Bill
age:19
sex:male
height:175
"
"ok"
2、Struct的创建和属性使用
从上面的例子可以看出,我定义了一个叫做person的结构体。这个person结构体里面有以下几个属性:name、age、sex、height,其中height是有默认值175的
从单词的意思上就可以知道,这是一个人物信息的结构体,它包含了名字、年龄、性别、身高这4个属性。
接下来使用person结构体来创建对象。
从上面的例子里面可以看出,有2种方式可以创建对象
1.创建一个空的person
local Tom = person()
Tom.name = "Tom"
Tom.age = 23
Tom.sex = "male"
Tom.height = 180
用struct名称加括号,可以创建出这个struct的对象。
然后可以在对象后面接”.属性名称”,来给对象身上的属性赋值。
2.创建的时候指定参数
local Bill = person name:"Bill" age:19 sex:"male"
在创建的时候,也可以直接用属性的”名称:值”的方式直接赋值给属性。
3、Struct内部定义方法
除了简单的指定一些变量属性,Struct还可以写函数在里面,然后从外部调用函数。
把上面的例子稍作修改,变成这样:
(
local TestPerson
local PrintPerson
struct person (
name,
age,
sex,
height = 175,
fn GetPersonInfoString =
(
local content = "";
content +=("name:"+ this.name+"\n")
content +=("age:"+ (this.age as string) + "\n")
content += ("sex:"+this.sex+ "\n")
content += ("height:"+(this.height as string)+"\n")
)
)
fn TestPerson =
(
local Tom = person()
Tom.name = "Tom"
Tom.age = 23
Tom.sex = "male"
Tom.height = 180
PrintPerson Tom
local Bill = person name:"Bill" age:19 sex:"male"
PrintPerson Bill
)
fn PrintPerson obj =
(
local content = obj.GetPersonInfoString()
print content
return "ok"
)
TestPerson()
)
运行脚本,会发现结果和之前是一样的。
这里我在person这个结构体里面定义了一个叫做GetPersonInfoString的函数,然后把自身的信息往外返回,在打印信息的时候,只需要向person对象调用GetPersonInfoString函数,就得到了需要打印的结果了。
4、注意事项
1.struct内部的变量和函数的分隔
从上面的例子可以看出,struct的变量和函数,都必须用逗号分割。特别是函数,很容易漏掉。不过在最后一个变量或者函数后,就不能再加逗号了。
2.struct的变量和方法命名
(1)struct内部调用外部变量
来看一段例子:
(
local TestPerson
local PrintPerson
local testVal = 1
struct person (
name,
age,
sex,
height = 175,
fn GetPersonInfoString =
(
testVal = testVal +1
local content = testVal as string
return content
)
)
fn TestPerson =
(
local Tom = person()
Tom.name = "Tom"
Tom.age = 23
Tom.sex = "male"
Tom.height = 180
PrintPerson Tom
local Bill = person name:"Bill" age:19 sex:"male"
PrintPerson Bill
)
fn PrintPerson obj =
(
local content = obj.GetPersonInfoString()
print content
return "ok"
)
TestPerson()
)
这段例子是从上面的例子改的,需要注意的地方是,我在struct外部定义了一个局部变量testVal = 1,然后在struct内部使用这个testVal并对其进行赋值。
运行脚本,会发现打印如下:
"2"
"3"
"ok"
可以看出,在struct内部是可以调用外部的变量的。
(2)struct内部变量和外部变量重名
再对这个例子进行小修改:
(
local TestPerson
local PrintPerson
local height = 1
struct person (
name,
age,
sex,
height = 175,
fn GetPersonInfoString =
(
height = height +1
local content = height as string
return content
)
)
fn TestPerson =
(
local Tom = person()
Tom.name = "Tom"
Tom.age = 23
Tom.sex = "male"
Tom.height = 180
PrintPerson Tom
local Bill = person name:"Bill" age:19 sex:"male"
PrintPerson Bill
)
fn PrintPerson obj =
(
local content = obj.GetPersonInfoString()
print content
return "ok"
)
TestPerson()
print ("height:"+(height as string))
)
这次我把testVal改名了,改成和Struct里面的height变量重名了。
运行脚本,可以看到:
"181"
"176"
"height:1"
从这里我们可以看出来,如果Struct的变量和外部重名了,在使用的时候,会使用内部的变量。
3.内部参数问题
再对例子做小修改:
(
local TestPerson
local PrintPerson
local height = 1
struct person (
name,
age,
sex,
height = 175,
fn GetPersonInfoString =
(
height = height +1
local content = height as string
return content
),
fn SetHeight height =
(
height = height
)
)
fn TestPerson =
(
local Bill = person name:"Bill" age:19 sex:"male"
Bill.SetHeight 200
PrintPerson Bill
)
fn PrintPerson obj =
(
local content = obj.GetPersonInfoString()
print content
return "ok"
)
TestPerson()
print ("height:"+(height as string))
)
这次,我加了一个SetHeight 函数在struct里面,传进去一个height的变量,然后很莫名其妙的height = height,因为height这个名字出现在了3个地方,首先是struct外部的变量,然后是struct本身的变量,最后是函数的传入变量,那么这个height究竟是代表了哪个?
看看打印结果
"176"
"height:1"
可以看出,struct内部的height变量并没有收到height = height的影响,struct外部的height变量也没有受到height = height的影响,这里赋值的只是函数传进去的height参数。
再进行一下修改:
(
local TestPerson
local PrintPerson
local height = 1
struct person (
name,
age,
sex,
height = 175,
fn GetPersonInfoString =
(
height = height +1
local content = height as string
return content
),
fn SetHeight height =
(
this.height = height
)
)
fn TestPerson =
(
local Bill = person name:"Bill" age:19 sex:"male"
Bill.SetHeight 200
PrintPerson Bill
)
fn PrintPerson obj =
(
local content = obj.GetPersonInfoString()
print content
return "ok"
)
TestPerson()
print ("height:"+(height as string))
)
这次把SetHeight 函数的内容改成了
this.height = height
这次的输出结果是:
"201"
"height:1"
可以看出,struct的height被赋值了。
4.公共和私有变量函数
对上面的例子再做修改:
(
local TestPerson
local PrintPerson
struct person (
name,
age,
sex,
height = 175,
fn GetPersonInfoString =
(
return (this.GetWeightStr())
),
fn SetWeight val =
(
this.weight = val
),
private weight = 1,
private fn GetWeightStr =
(
local content = this.weight as string
return content
)
)
fn TestPerson =
(
local Bill = person name:"Bill" age:19 sex:"male"
Bill.SetWeight 100
PrintPerson Bill
)
fn PrintPerson obj =
(
local content = obj.GetPersonInfoString()
print content
return "ok"
)
TestPerson()
)
可以看到在struct里面,多了一个私有的变量weight,还有一个私有的函数GetWeightStr。
这样写是允许的,这个weight变量和GetWeightStr方法,是只有struct内部才能使用,如果在外部调用,就会报错。
还有一点值得注意的是,在struct里面,可以用一个public或者private定义一列连续的变量或者函数
于是我们可以把struct内部的代码改成这样:
struct person (
public
name,
age,
sex,
height = 175,
fn GetPersonInfoString =
(
return (this.GetWeightStr())
),
fn SetWeight val =
(
this.weight = val
),
private
weight = 1,
fn GetWeightStr =
(
local content = this.weight as string
return content
)
)
在public下面的一段变量和函数,都是公共的,在private下面的一段变量和函数,都是私有的。
public和private没有严格的顺序和数量,可以先private后public也行,或者先private再public再private都可以。
5.总结
说了这么多废话,对于本身熟悉其他语言编程的朋友来说,肯定觉得很无聊。其实我是为了照顾本身对编程不是特别熟悉的朋友,上面的实验得出了一个结论,struct里面的变量定义,和一般的脚本语言没区别,作用域是优先当前结构本身,然后才是往上一级。然后如果要指定struct本身,可以使用this来指定。
然后关于public和private,也是和一般脚本区别不大,它可以通过一个public或者private标记一系列连着的变量和函数,
二、关于面向对象的思考
通过上面的例子,我们似乎看到了面向对象的一丝希望,对于复杂的逻辑,我们可以通过Struct结构定义类似于类(Class)的结构,然后把数据都封装在结构体的对象里面,然后定义方法,把处理的逻辑都写在结构体里面。最后,在外部调用时,只需要构建对象,并且传入数据,剩下的逻辑都在对象内部处理。
不过Struct不是Class,它的功能比较有限。比如如果按照严格的概念定义,面向对象应该包含封装、继承、多态。
从上面的例子看,Struct实现封装是没问题的,以为他有public和private的定义。
继承和多态,struct并没有直接现成的方法可以做到。如果非要说能实现继承,也可以通过定义一个方法,在创建子类的时候,把父类传进去,然后复制所有父类变量和方法的定义,最后在子类实现重写,覆盖父类的方法,也能勉强能实现。但我个人感觉,这样子做,变成了是为了实现面向对象而实现,好像有点跑偏了。
从我个人的理解,面向对象的目的是为了让代码变得条例清晰,便于管理。对象内部的问题对象自己解决,外部只负责调用和得到结果。基于这个理念,我觉得不一定非要实现继承和多态,只要在编写代码的时候能比较清晰的划分业务范围,就可以使用面向对象的方式去写maxscript的脚本了。
三、相对完整的应用例子
这里写了一个获取一个biped骨骼所有骨骼的信息的脚本,通过这个脚本,可以看看较为具体的写法。
1、完整代码:
(
--function
local CheckOneObj
local PrintBoneInfo
local OnPickFun
local AddBoneInfoToDict
local GetBoneInfoByName
--var
local TestPickUI
local boneNameList;
local boneDict
struct TransformInfo
(
pos,
rotation,
scale,
fn SetData obj =
(
this.pos = obj.transform.pos
this.rotation = obj.transform.rotation
this.scale = obj.transform.scale
),
fn GetPrintString =
(
local content = ""
content += "pos:"+(this.pos as string)+"\n"
content += "rotation:"+(this.rotation as string+"\n")
content +="scale:"+(this.scale as string+"\n")
return content
)
)
struct BoneInfo
(
public
name,
transform,
children,
parent,
fn SetData obj =
(
this.name = obj.name
this.transform = TransformInfo()
this.transform.SetData obj
this.SetParent obj
this.SetChildren obj
),
fn GetInfoString =
(
local content = this.GetNameString() + this.GetTransformString()+this.GetParentString()+this.GetChildrenString()
return content
),
private
fn SetParent obj =
(
if obj.parent == undefined then
(
this.parent = undefined
)
else
(
this.parent = obj.parent.name
)
),
fn SetChildren obj =
(
local childrenList = obj.children
if childrenList != undefined and childrenList.count >0 then
(
this.children = #()
for i in 1 to childrenList.count do
(
append this.children childrenList[i].name
)
)
else
(
this.children = undefined
)
),
fn GetNameString =
(
local content = "----------\n";
if this.name == undefined then
(
content += "name:null\n"
)
else
(
content += "name:"+this.name+"\n";
)
return content
),
fn GetTransformString =
(
local content = "";
if this.transform != undefined then
(
content = "transform:\n";
content += this.transform.GetPrintString()
)
else
(
content = "transform:null\n"
)
return content;
),
fn GetParentString =
(
local content = "";
if this.parent == undefined then
(
content = "parent:null\n"
)
else
(
content = "parent:"+this.parent+"\n";
)
),
fn GetChildrenString =
(
local content = "";
if this.children == undefined or this.children.count == 0 then
(
content = "children:null\n"
)
else
(
content = "children:"
for i in 1 to this.children.count do
(
content += this.children[i]
if i<this.children.count then
content +=","
)
content +="\n"
)
return content
)
)
fn AddBoneInfoToDict info =
(
if boneNameList == undefined then
boneNameList = #()
if boneDict == undefined then
boneDict = #()
local index = findItem boneNameList info.name
if index <=0 then
(
append boneNameList info.name
append boneDict info
)
else
(
boneDict[index] = info
)
)
fn GetBoneInfoByName val =
(
if boneNameList == undefined or boneNameList.count == 0 then
return undefined
local index = findItem boneNameList val
if index <=0 then
(
return undefined
)
else
(
return boneDict[index]
)
)
fn CheckOneObj obj =
(
local info = BoneInfo()
info.SetData obj
AddBoneInfoToDict info
local childrenList = obj.children
if childrenList != undefined and childrenList.count >0 then
(
for i in 1 to childrenList.count do
(
CheckOneObj childrenList[i];
)
)
)
fn PrintBoneInfo =
(
if boneDict != undefined and boneDict.count >0 then
(
for i in 1 to boneDict.count do
(
print(boneDict[i].GetInfoString())
)
)
)
fn OnPickFun =
(
if $ == undefined then
return 0
boneNameList = #()
boneDict = #()
CheckOneObj $
PrintBoneInfo()
)
rollout TestPickUI "Untitled" width:199 height:177
(
button 'btn1' "pick" pos:[51,48] width:110 height:31 align:#left
on btn1 pressed do
(
OnPickFun()
)
)
createDialog TestPickUI
)
2、执行脚本的结果
运行脚本,会看到只有一个按钮:
创建一个biped骨骼
选择biped的根节点,然后点击pick按钮
会发现输出了很多打印。以横线分割,每一段是一根骨骼的信息。
3、代码说明:
1.struct的说明
这个脚本里面定义了2个结构体,分别是TransformInfo和BoneInfo。其中TransformInfo作为BoneInfo里面的一个变量,记录了骨骼的Transform信息。
BoneInfo除了Transform信息,还有名字、子节点、父节点的信息。
在使用方面,都是直接新建对应的对象,然后用SetData函数把物体对象传进去,然后在对象内部进行的数据分析和记录。
在最后,调用了BoneInfo的GetInfoString函数,获取物体的各种参数。而每一种属性的参数,都是有独立的方法去组建打印的字符串。如果想修改其中一种信息,可以只修改对应的方法。
2.数据存储
脚本里面有写函数,在这个例子里面是没有用到的,我是想顺便展示一下怎样去做这个事情。
fn AddBoneInfoToDict info =
(
if boneNameList == undefined then
boneNameList = #()
if boneDict == undefined then
boneDict = #()
local index = findItem boneNameList info.name
if index <=0 then
(
append boneNameList info.name
append boneDict info
)
else
(
boneDict[index] = info
)
)
fn GetBoneInfoByName val =
(
if boneNameList == undefined or boneNameList.count == 0 then
return undefined
local index = findItem boneNameList val
if index <=0 then
(
return undefined
)
else
(
return boneDict[index]
)
)
本来事情很简单,如果有dictionary或者哈希表这类的数据类型,直接用key和value来存储是很简单的事情。但MaxScript并没有这样的类型,所以我就用其他方法实现了。
这里是建了了2个数组,一个是存储骨骼的名字boneNameList,一个是存储名字对应的对象boneDict,保证两个数组的下标是一样的,然后通过函数AddBoneInfoToDict添加数据,通过函数GetBoneInfoByName来获取数据,获取的时候,先通过名字判断是否在boneNameList数组里存在,如果存在,返回了下标,就通过下标去boneDict数组拿对象。