Roblox剑九之剑二

Roblox剑九之剑二【御码之剑】



前言

博主于2020年入坑UGC游戏开发,至今已产出多款UGC作品,如异能危机、英雄远征之天劫、王者之剑、百变大富翁……可以说UGC游戏开发是一种创业的新风口。就如当今的低代码一样,易上手,多产出,快成效。


本博文主要带大家攻破LUA这一脚本语言-以Roblox为例。


一、学习路线

Lua学习
Lua基础
Lua的表
Lua应用程序开发
UGC平台与Lua开发的结合
Lua介绍
Lua特性
数据类型
控制语句与函数
Table介绍
Table用法
元表介绍
面向对象
三大特性
客户端脚本
服务器脚本
模块化开发
  • 图片形式

请添加图片描述

二、Lua基础

  目前所知的UGC游戏创作平台【重启世界-Reworld】、【Roblox】均使用Lua这一脚本语言进行UGC游戏的程序编程。博主的第一门面向对象的语言是C#,主要为GIS开发服务。后来遇到UGC游戏开发,学习了辅助语言-Lua,Lua这一脚本语言或许许多人没有听说过,但这一强大的胶水语言确确实实在我们身边,就以游戏为例

  • 完全可以和C/C++、C#、Java完美地进行交互
  • 定义、存储和管理基础游戏数据
  • 创建和维护开发者友好的游戏存储和载入系统
  • 创建功能原型,可以之后用高性能语言移植
  • 进行游戏的热更新操作【即补丁,常用】
    请添加图片描述

1、Lua介绍

Lua是一种轻量小巧脚本语言,它由标准的C语言编写并且是开源的,可以很方便的和其他程序进行集成扩展(C#,Java…),其设计目的是为了嵌入应用程序中,为应用程序提供灵活的扩展和定制功能。

1.1 解释型语言

Lua是脚本语言,脚本语言解释型语言的其中一种。针对编程语言的分类,不同角度有不同类型的分类。其中编译型语言解释型语言主要以程序如何执行而进行的分类。关于两者的具体的区别可以参考戳我
简单来说

  • 解释型语言是解释器边解释边执行程序,生成可执行文件
  • 编译型语言是编译器先编译后执行程序,生成可执行文件,可重复运行可执行文件

2、Lua特性

  • 支持面向过程编程和函数式编程
  • 自动内存管理,只提供一种通用类型的表(table),但可以实现数组哈希表集合对象
  • 语言内置匹配模式。
  • 闭包(closure)。
  • 函数也可以看作一个值。
  • 提供多线程协程,不是操作系统支持的线程)。
  • 通过闭包table可以方便的支持面向对象编辑所需要的一些关键机制,比如数据抽象虚函数继承重载等。

3、Lua的数据类型

3.1 数据类型

Lua中的数据类型主要有八种,分别是nil、boolean、number、string、function(函数式编程)、table、thread、userdata。

数据类型描述
nil这个最简单,只有值nil属于该类,表示一个无效值(在条件表达式中相当于false)。
boolean包含两个值:false和true。
number表示双精度类型的实浮点数
string字符串由一对双引号或单引号来表示
function由 C 或 Lua 编写的函数
userdata表示任意存储在变量中的C数据结构
thread表示执行的独立线路,用于执行协同程序
tableLua 中的表(table)其实是一个"关联数组"(associative arrays),数组索引可以是数字字符串表类型。在 Lua 里,table 的创建是通过"构造表达式"来完成,最简单构造表达式是{},用来创建一个空表。

示例

print(type("Hello world"))      --> string
print(type(10.4*3))             --> number
print(type(print))              --> function
print(type(type))               --> function
print(type(true))               --> boolean
print(type(nil))                --> nil
print(type(type(X)))            --> string

3.2 Lua变量

3.2.1 变量类型

Lua 变量有3种类型

  • 全局变量
  • 局部变量(local)
  • 表中的域
3.2.2 变量约定
  • Lua中的所有变量全部是全局变量,哪怕是在函数体中也是全局变量除非local关键字显式声明为局部变量局部变量的作用域是从声明处到语句块结束。简而言之,如果不是local声明的,都是全局变量。【重点
  • 变量在使用前,必须进行声明
  • 变量的默认值都为nil
  • Lua可以对多个变量同时赋值变量列表值列表需要用逗号隔开
  • Lua语言支持直接赋值不需声明数据类型的操作。

示例

val = 10          --全局变量
local locVal = 10  --局部变量

function value1()
    c = 20
    local d = 10
end

value1()
print(val, locVal, c, d)   --执行函数后 局部变量的值无法打印,全局变量可以

do
    local val = 4          --定义局部变量val
    locVal = 5             --对局部变量重新赋值
    print(val, locVal)     --打印do内的val = 4以及locVal = 5
end

print(val, locVal)         --这里的val是全局变量val,locVal为被重新赋值的局部变量locVal
--多个变量同时赋值
a, b = 10, 2*x  --  a=10; b=2*x 
  • 补充 注释写法
--单行注释
或者
--[[
 多行注释
 --]]

4、控制语句

4.1 分支语句

if(布尔表达式) then
   --[ 布尔表达式为 true 时执行该语句块 --]
else
   --[ 布尔表达式为 false 时执行该语句块 --]
end

4.2 循环语句

循环类型描述
while循环在条件为 true 时,让程序重复地执行某些语句。执行语句前会先检查条件是否为 true。
for循环重复执行指定语句,重复次数可在 for 语句中控制
repeat…until重复执行循环直到 指定的条件时为
4.2.1 while循环
while(condition)do
   <执行体>
end
4.2.2 for循环
1)数值for循环
for var=exp1,exp2,exp3 do  
    <执行体>  
end
  • varexp1变化到exp2,每次变化以exp3为步长递增var,并执行一次”执行体”。
  • exp3是可选的,如果不指定,默认为1
2)泛型for循环

游戏开发中常用
泛型for循环通过一个迭代器函数来遍历所有值,类似java中的foreach语句。

--打印数组a的所有值  
for k,v in ipairs(a) do 
     print(v) 
end

简单而言,k,v为键值对,k是索引值,v是对应索引的元素值

4.2.3 repeat…until 循环

先执行一次,再进行条件判断

repeat
   <执行体>
while(condition)

循环控制语句

  • break   退出当前循环或语句,脚本开始执行紧接着的语句
    lua中没有continue关键词
  • 变相实现continue
 
for i = 1, 100 do
    while true do
        if i % 2 == 1 then break end
 
        -- 这里有一大堆代码
        --
        --
        break
    end
end
 

4.3 函数

optional_全局/局部 function 函数名(argument1, argument2, argument3..., argumentn)
   函数体
 return 函数返回值
end
  • Lua函数可以返回多个结果值,在return后列出要返回的值得列表即可返回多值,如return m, mi
  • Lua函数可以接受可变数目的参数,和C语言类似在函数参数列表中使用三点(…) 表示函数有可变的参数,Lua将函数的参数放在一个叫arg的表中,#arg 表示传入参数个数

二、Lua的表(table)

1、table的介绍

表(Table)是Lua语言中最主要(事实上也是唯一的)和强大的数据结构
使用表,Lua语言可以以一种简单、统一且高效的方式表示数组集合记录其他很多数据结构
Lua语言也使用来表示包( package )其他对象
Lua语言中的表本质上是一种辅助数组( associative array ),这种数组不仅可以使用数值作为索引,也可以使用字符串其他任意类型的值作为索引(nil除外)。
同一个表存储的值可以具有不同的类型索引,并且可以按需求增长以容纳新的元素

示例
当调用函数math.sin时,我们可能认为是“调用了math库中函数sin”;
而对于Lua语言来说,其实际含义是“以字符串"sin"为键检索表math”

2、table的使用

2.1 初始化

1local a = {}
2local a = {["x"] = 12,["mutou"] = 99,[3] = "hello"}
print(a["x"])
3local a = {x=12,mutou=99,[3]="hello"}
print(a["x"])
4local a = {x=12,mutou=99,[3]="hello"}
print(a.x)
注:
1为空表初始化,2为初始化并赋初值的通用操作(使用[]代表索引,使用=将索引和值联系起来)
34为索引是字符串的简洁写法


默认数字索引(列表式构造器)
local a ={[1]=12,[2]=43,[3]=45,[4]=90}
简洁版  local a = {12,43,45,90}
print(a[1])
  • 使用列表式构造器时,默认的索引从1开始而不是0),并且依次类推。这与其它语言不一样。
  • 没有给出索引的,由表自动给出数字索引

2.2 操作

2.2.1 访问元素

重点
使用.这个语法糖后接字符串索引值[索引值]即可访问table的索引对应的元素值
注:table的长度用#表名进行表示

local a = {
{x = 1,y=2},
{x = 3,y = 10}
}
print(a[1].x)
print(#a)

补充   in pairs 和 in ipairs的区别

in pairs方法是比较通用的,不会过分强调table中的key值
in iparis方法则就强调table的key值为顺序排列,当顺序中断的时候,for循环终止
2.2.2 插入

使用table.insert()方法


tab2 = {"a",2,"b",5} --定义一个table

table.insert(tab2,2,"king")  --指定在某一位置插入某值
for i, v in ipairs(tab2) do
   -- print(v)  --输出a king 2 b 5
end

table.insert(tab2,3)  --没有指定位置的话,默认将值插入到末尾位置
for i, v in ipairs(tab2) do
   -- print(v)  --输出a king 2 b 5 3
end

tab3 = {"d",7,"e"}
table.insert(tab2,tab3) -- 将table插入table
for i, v in ipairs(tab2[7]) do
    --print(v)  --输出d 7 e
end

tab2["mm"]="mmm"   --添加一个新的键值对  下面的for迭代器选择pairs才能将新的键值对遍历出来,而非ipairs
for i, v in pairs(tab2) do
    print(i,v)  --输出1 a ; 2 king ; 3 2 ; 4 b ; 5 5 ; 6 3 ; 7 table ;mm mmm
end

2.2.3 删除

使用table.remove()方法


tab4 = {1,4,"tt","jj"}

table.remove(tab4,1) --移除指定位置的table值,若没有指定位置,则默认移除最后一位的元素

for i, v in ipairs(tab4) do
    print(v)  --输出 4 tt jj
end


2.2.4 更新

访问索引对应的元素赋值即可

2.2.5 排序

使用table.sort()方法


language = {"lua","java","c#","c++"}

table.sort(language) --只有table一个参数,使用lua默认的排序方式排序
for i, v in ipairs(language) do
   -- print(v) --输出c# c++ java lua
end

local function my_comp1(element1,element2)  --自定义比较函数 作为table.sort()参数
    return element1<element2
end
table.sort(language,my_comp1)
for i, v in ipairs(language) do
    print(v)   --输出 c# c++ java lua
end


local function my_comp2(element1,element2)  --自定义比较函数 作为table.sort()参数
    return element1>element2
end
table.sort(language,my_comp2)
for i, v in ipairs(language) do
   -- print(v)    --输出lua java c++ c#
end


local function my_comp3(element1,element2)  --自定义比较函数 作为table.sort()参数
    if element1==nil then
        return false
    end
    if element2==nil then
        return true
    end
    return element1>element2
end
language[2]=nil   --table中有nil存在的情况
table.sort(language,my_comp3)
for i, v in ipairs(language) do
   -- print(v)  --输出lua c++ c#
end

2.2.6 移除整个表的内容
  • 可以有多个表名引用于同一个表,当最后一个引用释放时,垃圾收集器会最终删除这个表。
  • 将表设为nil,然后等垃圾收集或强制执行一次垃圾收集(collectgarbage)。
  • 也就是说,在程序开发中,需要将表置空释放表资源
  • 如果不想置空,可以尝试weak table的使用,需要用到元组的概念
local table = {1,1,1,1,1,1,11,1,1}
--1.这个方法比较暴力,直接将table置空
table = {} 或者 table=nil     --释放table的引用
print(#table) 				--- nil
--2.通过遍历循环的办法移除整个table里的元素
for i = #table ,table do 
	table.remove(table,i)
	i = i - 1
end
--注意:在移除table里的元素时,table里的元素会自动补位,所以移除全部从最后一位开始移除

三、元表介绍

1、元表介绍

Lua的本质其实是个类似HashMap的东西,其元素是很多的Key-Value对,如果尝试访问了一个表中并不存在的元素时,就会触发Lua的一套查找机制,也是凭借这个机制来模拟了类似“继承”的行为

元表像是一个“操作指南”,里面包含了一系列操作的解决方案,例如__index方法就是定义了这个索引失败的情况下该怎么办。

设置元表的方法为 setmetatable(son, father) --把son的metatable(元表)设置为father

关键词是setmetatable

1.1 Lua查找表中元素时的规则

  • 在表中查找,如果找到,返回该元素,找不到则继续
  • .判断该表是否有元表,如果没有元表,返回nil,有元表则继续
  • 判断元表有没有__index方法,如果__index方法为nil,则返回nil;如果__index方法是一个,则重复1、2、3;如果__index方法是一个函数,则返回该函数的返回值

2、面向对象

面向对象三大特性

  • 封装
  • 继承
  • 多态

详细介绍可以移步Java剑开天门(二)

Lua是不支持面向对象的,但是可以利用元表实现面向对象

2.1 官方做法

  • 类定义
    • 方法一般用 : 这个语法糖表示
    • self代指自己
    local Class= { value=0 }   --定义属性

    function  Class:new(o)   --定义new方法,模板可以不用修改
         o=o or {};
         setmetatable(o,self);  --将self设置为o的元表
         self.__index=self;     --指向自己
         return o;
    end

    function  Class:showmsg()   --定义方法
        print(self.value);
    end

    local  test = Class:new();   --类的实例化
    test:showmsg();   --方法调用

  • 类继承

    local  A = Class:new();      --类A定义

    function  A:new(o)     -- new方法,模板可不修改
          local  o = o or {};
          setmetatable(o,self);
          self.__index=self;
          return o;
    end
    function  A:showmsg(  )   --普通方法
        print (self.value);   --父类的属性
        print("zhishi 测试啊啊 ")
    end

    local  s = A:new({value=100});   --类A初始化
    s:showmsg();    --方法调用

2.2 优雅方法


class.lua文件
--[[
模拟继承结构的基类
]]
local _class = {}
 
function Class(super)
    local class_type = {}
    class_type.ctor = false            --构造函数
    class_type.super = super           --继承
    class_type.New = function(...)     --实例
        local obj = {}
        do
            local create               --创建闭包函数
            create = function(c, ...)
                if c.super then        --存在父类,递归创建
                    create(c.super, ...)
                end
                if c.ctor then
                    c.ctor(obj, ...)   --构造函数
                end
            end
 
            create(class_type, ...)    --闭包函数入口
        end
 
        setmetatable(obj, {__index = _class[class_type]})    --查找元表
 
        return obj
    end
 
    local vtbl = {}                      --临时表
    _class[class_type] = vtbl            --成员变量赋值
 
    setmetatable(                        --赋值、查找元表(vtbl查找与赋值)
        class_type,
        {
            __newindex = function(t, k, v)
                vtbl[k] = v
            end,
            --For call parent method
            __index = vtbl
        }
    )
 
    if super then                        --继承查找元表(vtbl赋值,返回父类值)
        setmetatable(
            vtbl,
            {
                __index = function(t, k)
                    local ret = _class[super][k]
                    vtbl[k] = ret
                    return ret
                end
            }
        )
    end
 
    return class_type
end

--------使用

--当然要调用class.lua文件
base=Class()       -- 定义一个基类 base_type
function base:ctor(x)  -- 定义 base_type 的构造函数

    print("base ctor")

    self.x=x

end
function base:print_x()    -- 定义一个成员函数 base:print_x

    print(self.x)

end
function base:hello()  -- 定义另一个成员函数 base:hello

    print("hello base")

end



--子类实现
child=Class(base);
function child:ctor()
    print("child ctor");
end

function child:hello()
     print("hello child")
end


--调用
 local a=child.New(21);
 a:print_x();
 a:hello();

两种方式各有优缺点

  • 博主使用的是第二种方式,第二种方式和面向对象编程出入不大。

  • local变量则是创建一个新的变量,遵守子作用域覆盖父作用域的规则。

  • 特别是对于require "modname"中的模块,在编写的时候,不能直接使用全局变量,因为是同一个变量会保存其变量状态影响其他使用。最好的处理方式就是尽可能的依据入参,函数内部定义local 变量等来编写代码。

  • local访问速度更快(原因是local变量是存放在lua的堆栈里面的是array操作,而全局变量是存放在_G中的table中,效率不及堆栈)

  • 封装是可以实现,但不方便嵌入此代码中,所以我们忽略封装深层含义,在Lua中,封装就简单理解为将属性方法封装在一起。

四、UGC平台与Lua开发的结合

1、客户端脚本

客户端程序编写在客户端脚本中,程序功能主要是提交数据,玩家会通过设备进行输入,比如点击按钮、点击鼠标等,客户端程序获取玩家的输入数据并简单处理后提交给服务器
客户端脚本创建的零件只有本地玩家可以看见的。

  • 以Roblox为例,LocalScript 是用于在连接到 Roblox 服务器的客户端上运行 Lua 代码的 Lua 源容器。

2、服务器脚本

服务器代码编写在服务器脚本中,程序功能主要是分析处理这些数据,然后把处理结果返回给客户端进行显示。 
服务器脚本创建的零件是所有玩家可以看见的。

像UGC游戏开发平台中开发的每个游戏,平台都会分配对应的服务器容量

  • 以Roblox为例,Script(脚本)是一种 Lua 代码容器,它的内容可以在服务器上运行。

  • 服务器脚本和客户端脚本可以通过RemoteEvent事件对象等不同平台上的不同通信机制进行两端的通信

3、模块化开发

在大型项目中的开发中,我们采取分治策略,将大项目分成一个个小模块,小模块间留有接口,整合后组成这个大项目。
在UGC平台开发游戏,模块化主要体现在以下两个方面

  • 使用面向对象编程思想进行工程化开发
  • 使用通用模块进行一个个模块的编写,供客户端或服务器脚本调用【关键词是require,通用模块脚本可以被客户端和服务器脚本调用】

以Roblox为例,可以ModuleScript 这一Lua源容器,它只会运行一次,并且必定返回相同的一个值。

然后在 ModuleScript 作为唯一参数的情况下,通过调用 require 返回此值。

以博主看法,模块可以分为客户端和服务器两大模块


客户端模块

  • 常规UI输入【发送请求】
  • 本地场景配置
  • 本地存储
  • 本地逻辑

服务器脚本

  • 常规工具类
  • 服务器存储
  • Controller转发【接收请求】
  • Service业务逻辑
  • Mapper数据持久化

五、结语

////////////////////////////////////////////////////////////////////
//                          _ooOoo_                               //
//                         o8888888o                              //
//                         88" . "88                              //
//                         (| ^_^ |)                              //
//                         O\  =  /O                              //
//                      ____/`---'\____                           //
//                    .'  \\|     |//  `.                         //
//                   /  \\|||  :  |||//  \                        //
//                  /  _||||| -:- |||||-  \                       //
//                  |   | \\\  -  /// |   |                       //
//                  | \_|  ''\---/''  |   |                       //
//                  \  .-\__  `-`  ___/-. /                       //
//                ___`. .'  /--.--\  `. . ___                     //
//              ."" '<  `.___\_<|>_/___.'  >'"".                  //
//            | | :  `- \`.;`\ _ /`;.`/ - ` : | |                 //
//            \  \ `-.   \_ __\ /__ _/   .-` /  /                 //
//      ========`-.____`-.___\_____/___.-`____.-'========         //
//                           `=---='                              //
//      ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^        //
//            佛祖保佑       永不宕机     永无BUG                    //
////////////////////////////////////////////////////////////////////
  • 4
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值