阿赵的MaxScript学习笔记分享十四《Struct结构体的使用和面向对象的思考》
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 = 23Tom.sex = "male"Tom.height = 180 PrintPerson Tomlocal 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 contentreturn "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 = 23Tom.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 = 23Tom.sex = "male"Tom.height = 180 PrintPerson Tomlocal Bill = person name:"Bill" age:19 sex:"male"PrintPerson Bill
)fn PrintPerson obj =
(local content = obj.GetPersonInfoString()print contentreturn "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 +1local content = testVal as stringreturn content)
)fn TestPerson =
( local Tom = person()Tom.name = "Tom"Tom.age = 23Tom.sex = "male"Tom.height = 180 PrintPerson Tomlocal Bill = person name:"Bill" age:19 sex:"male"PrintPerson Bill
)fn PrintPerson obj =
(local content = obj.GetPersonInfoString()print contentreturn "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 +1local content = height as stringreturn content)
)fn TestPerson =
( local Tom = person()Tom.name = "Tom"Tom.age = 23Tom.sex = "male"Tom.height = 180 PrintPerson Tomlocal Bill = person name:"Bill" age:19 sex:"male"PrintPerson Bill
)fn PrintPerson obj =
(local content = obj.GetPersonInfoString()print contentreturn "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 +1local content = height as stringreturn content), fn SetHeight height = (height = height)
)fn TestPerson =
( local Bill = person name:"Bill" age:19 sex:"male"Bill.SetHeight 200PrintPerson Bill
)fn PrintPerson obj =
(local content = obj.GetPersonInfoString()print contentreturn "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 +1local content = height as stringreturn content), fn SetHeight height = (this.height = height)
)fn TestPerson =
( local Bill = person name:"Bill" age:19 sex:"male"Bill.SetHeight 200PrintPerson Bill
)fn PrintPerson obj =
(local content = obj.GetPersonInfoString()print contentreturn "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 stringreturn content)
)fn TestPerson =
( local Bill = person name:"Bill" age:19 sex:"male"Bill.SetWeight 100PrintPerson Bill)fn PrintPerson obj =
(local content = obj.GetPersonInfoString()print contentreturn "ok"
)TestPerson()
)
可以看到在struct里面,多了一个私有的变量weight,还有一个私有的函数GetWeightStr。
这样写是允许的,这个weight变量和GetWeightStr方法,是只有struct内部才能使用,如果在外部调用,就会报错。
还有一点值得注意的是,在struct里面,可以用一个public或者private定义一列连续的变量或者函数
于是我们可以把struct内部的代码改成这样:
struct person (publicname,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 stringreturn 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、完整代码:
(--functionlocal CheckOneObjlocal PrintBoneInfolocal OnPickFunlocal AddBoneInfoToDictlocal GetBoneInfoByName--varlocal TestPickUIlocal boneNameList;local boneDictstruct TransformInfo(pos,rotation,scale,fn SetData obj =(this.pos = obj.transform.posthis.rotation = obj.transform.rotationthis.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(publicname,transform,children,parent,fn SetData obj = (this.name = obj.namethis.transform = TransformInfo()this.transform.SetData objthis.SetParent objthis.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.childrenif 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 thencontent +=",")content +="\\n")return content))fn AddBoneInfoToDict info = (if boneNameList == undefined thenboneNameList = #()if boneDict == undefined thenboneDict = #()local index = findItem boneNameList info.nameif index <=0 then(append boneNameList info.nameappend boneDict info)else(boneDict[index] = info))fn GetBoneInfoByName val = (if boneNameList == undefined or boneNameList.count == 0 then return undefinedlocal index = findItem boneNameList valif index <=0 then(return undefined)else(return boneDict[index]))fn CheckOneObj obj = (local info = BoneInfo()info.SetData objAddBoneInfoToDict infolocal childrenList = obj.childrenif 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 thenreturn 0boneNameList = #()boneDict = #()CheckOneObj $PrintBoneInfo())rollout TestPickUI "Untitled" width:199 height:177(button 'btn1' "pick" pos:[51,48] width:110 height:31 align:#lefton 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 thenboneNameList = #()if boneDict == undefined thenboneDict = #()local index = findItem boneNameList info.nameif index <=0 then(append boneNameList info.nameappend boneDict info)else(boneDict[index] = info))fn GetBoneInfoByName val = (if boneNameList == undefined or boneNameList.count == 0 then return undefinedlocal index = findItem boneNameList valif index <=0 then(return undefined)else(return boneDict[index]))
本来事情很简单,如果有dictionary或者哈希表这类的数据类型,直接用key和value来存储是很简单的事情。但MaxScript并没有这样的类型,所以我就用其他方法实现了。
这里是建了了2个数组,一个是存储骨骼的名字boneNameList,一个是存储名字对应的对象boneDict,保证两个数组的下标是一样的,然后通过函数AddBoneInfoToDict添加数据,通过函数GetBoneInfoByName来获取数据,获取的时候,先通过名字判断是否在boneNameList数组里存在,如果存在,返回了下标,就通过下标去boneDict数组拿对象。