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、执行脚本的结果
运行脚本,会看到只有一个按钮:
阿赵的MaxScript学习笔记分享十四《Struct结构体的使用和面向对象的思考》-LMLPHP

创建一个biped骨骼
阿赵的MaxScript学习笔记分享十四《Struct结构体的使用和面向对象的思考》-LMLPHP

选择biped的根节点,然后点击pick按钮
阿赵的MaxScript学习笔记分享十四《Struct结构体的使用和面向对象的思考》-LMLPHP

会发现输出了很多打印。以横线分割,每一段是一根骨骼的信息。

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数组拿对象。

04-21 08:20