主要学习目标
1.巩固学习的AB包+Lua语法+xLua解决方案的3部分知识
2.学会Unity+Lua+VSCode环境调试
3.学会Unity结合xLua进行游戏功能开发
4.学会制作Lua文件迁徙小工具
准备工作
1.导入xlua
将xlua文件夹下的Assets中的Plugins和XLua文件夹导入Unity
2.ab包导入
导入asset Bundle Browser,新版本中已经下架,可以在官方手册中通过git下载。
设置如图
3.导入ProjectBase
4.导入相关lua文件
5.C#Main以及Lua 的Main
cs
void Start()
{
LuaMgr.GetInstance().Init();
LuaMgr.GetInstance().DoLuaFile("Main");
}
Lua
print("准备就绪")
vscode环境搭建
1.下载扩展
(///快捷注释)
(debugger for Unity)
2.改变Unity启动的编辑器
3.验证C#环境搭建成功
跳出transfrom说明成功
选择Attach to Unity即可开始调试
4.lua环境搭建
下载Emmylua扩展
添加新的调试配置
需要jdk1.8以上并设置环境变量
设置好后就可以正常调试了。
主面板拼凑
Canvas设置
背包面板拼凑
加上toggleGroup组件
记得给每个tog的group指定
左对齐
格子面板拼凑
效果
预设体图样
常用类别名准备
新建InitClass.lua
Lua
--常用别名都在这里定位
--准备我们之前导入的脚本
--面向对象
require("Object")
--字符串拆分
require("SplitTools")
--Json解析
Json = require("JsonUtility")
--Unity相关的
GameObject = CS.UnityEngine.GameObject
Resources = CS.UnityEngine.Resources
Transform = CS.UnityEngine.Transform
RectTransform = CS.UnityEngine.RectTransform
--图集对象类
SpriteAtlas = CS.UnityEngine.U2D.SpriteAtlas
Vector3 = CS.UnityEngine.Vector3
Vector2 = CS.UnityEngine.Vector2
--UI相关的
UI = CS.UnityEngine.UI
Image = UI.Image
Button = UI.Button
Text = UI.Text
Toggle = UI.Toggle
ScrollRect = UI.ScrollRect
--自己写的C#脚本相关
--直接得到AB包资源管理器的单例对象
ABMgr = CS.ABMgr.GetInstance()
Main.lua
Lua
print("准备就绪")
--初始化所有准备好的类别名
require("InitClass")
数据准备
道具表配置
生成json表,打包进AB包
先编辑excel表
icon命名参考下面图集,加Icon是为了区分其它图集,表明是Icon图集中的资源
图集设置
使用转json工具将excel表转为json(推荐bejson网站)
注意有的转json网站会将数字用字符串表示,同时最后一行可能多了逗号或者多空一行。
将json文件打成json ab包中,之前的预设体和icon图集打到ui包中
之后就build。
Lua读取json表及准备玩家数据
Main.lua修改
Lua
print("准备就绪")
--初始化所有准备好的类别名
require("InitClass")
--初始化道具表信息
require("ItemData")
--玩家信息
--1.从本地读取 本地存储 有PlayerPrefs和json或者二进制
--2.网络游戏 从服务器读取
require("PlayerData")
PlayerData:Init()
新建ItemData.lua
Lua
--将json数据读取道lua中的表中进行存储
--首先应该先把Json表 从AB包中加载出来
--TextAsset 是InitClass中定义的TextAsset = CS.UnityEngine.TextAsset
local txt = ABMgr:LoadRes("json","ItemData",typeof(TextAsset))
--获取它的文本信息 进行json解析
local itemList = Json.decode(txt.text)
print(itemList[1]) --打印出一个table
print(itemList[1].id .. itemList[1].name)
--加载出来是一个像数组结构的数据
--不方便我们通过 id 来获取里面的内容 所以 我们用一张新表 转存一次
--而且这张表 在任何地方 都能被使用
-- 一张用来存储道具信息的表
-- 键值对形式 键是道具ID 值是道具表一行信息
ItemData = {}
for _, value in pairs(itemList) do
ItemData[value.id] = value
end
for key,value in pairs(ItemData) do
print(key,value.tips)
end
PalyerData.lua
Lua
PlayerData = {}
--目前只做背包功能 所以只需要它们的道具信息
PlayerData.equips = {}
PlayerData.items = {}
PlayerData.gems = {}
--为玩家数据写一个 初始化方法后 以后直接改这里的数据来源即可
function PlayerData:Init()
--道具信息 不管存本地 还是服务器 都不会把道具的所有信息存起来
--道具ID和数量
--目前因为没有服务器 为了测试 就写死道具数据作为玩家数据
table.insert(self.equips,{id = 1, num = 1})
table.insert(self.equips,{id = 2, num = 1})
table.insert(self.items,{id = 3, num = 50})
table.insert(self.items,{id = 4, num = 30})
table.insert(self.gems,{id = 5, num = 99})
table.insert(self.gems,{id = 6, num = 88})
end
主面板逻辑
编写MainPanel.lua
Lua
--只要是一个新的对象(面板) 我们就新建一张表
MainPanel = {}
--不是必须写 因为lua的特性 不存在声明变量的概念
--这样写的目的 是当别人看到这个lua代码时 知道这个表(对象)有什么变量很重要
--关联的面板对象
MainPanel.panelObj = nil
--对应的面板控件
MainPanel.btnRole = nil
MainPanel.btnSkill = nil
--需要做 实例化面板对象
--为这个面板 处理对应的逻辑 比如按钮点击等等
--初始化该面板 实例化对象 控件事件监听
function MainPanel:Init()
--面板对象没有实例化过 才去实例化
if self.panelObj == nil then
--1.实例化面板对象 ABMgr + 设置父对象
self.panelObj = ABMgr:LoadRes("ui","MainPanel",typeof(GameObject))
self.panelObj.transform:SetParent(Canvas,false)
--2.找到对应控件
--找到子对象 再找到身上挂载的 想要的脚本
self.btnRole = self.panelObj.transform:Find("btnRole"):GetComponent(typeof(Button))
print(self.btnRole)
--3.为控件加上监听事件 进行点击等等逻辑处理
--以下方法,如果直接传入自己的函数 那么在函数内部 没办法用self获取内容
--self.btnRole.onClick:AddListener(self.BtnRoleClick)
self.btnRole.onClick:AddListener(function ()
self:BtnRoleClick()
end)
end
end
function MainPanel:ShowMe()
self:Init()
self.panelObj:SetActive(true)
end
function MainPanel:HideMe()
self.panelObj:SetActive(false)
end
function MainPanel:BtnRoleClick()
--print(123123)
--print(self.panelObj)
--等写了背包面板
--在这写显示背包
end
更新Main.lua
Lua
print("准备就绪")
--初始化所有准备好的类别名
require("InitClass")
--初始化道具表信息
require("ItemData")
--玩家信息
--1.从本地读取 本地存储 有PlayerPrefs和json或者二进制
--2.网络游戏 从服务器读取
require("PlayerData")
PlayerData:Init()
--之后的逻辑
require("MainPanel")
MainPanel:ShowMe()
背包面板逻辑
新建BagPanel.lua
Lua
-- 一个面板 对应一个表
BagPanel = {}
--"成员变量"
--面向对象
BagPanel.panelObj = nil
--各个控件
BagPanel.btnClose = nil
BagPanel.togEquip = nil
BagPanel.togItem = nil
BagPanel.togGem = nil
BagPanel.svBag = nil
BagPanel.Content = nil
--"成员方法"
--初始化方法
function BagPanel:Init()
if self.panelObj == nil then
--1.实例化面板对象 ABMgr + 设置父对象
self.panelObj = ABMgr:LoadRes("ui","BagPanel",typeof(GameObject))
self.panelObj.transform:SetParent(Canvas,false)
--2.找到对应控件
--找到子对象 再找到身上挂载的 想要的脚本
--关闭按钮
self.btnClose = self.panelObj.transform:Find("btnClose"):GetComponent(typeof(Button))
--3个toggle
local group = self.panelObj.transform:Find("Group")
self.togEquip = group:Find("togEquip"):GetComponent(typeof(Toggle))
self.togItem = group:Find("togItem"):GetComponent(typeof(Toggle))
self.togGem = group:Find("togGem"):GetComponent(typeof(Toggle))
--sv相关svBag
self.svBag = self.panelObj.transform:Find("svBag"):GetComponent(typeof(ScrollRect))
self.Content = self.svBag.transform:Find("Viewport"):Find("Content")
--3.为控件加上监听事件 进行点击等等逻辑处理
--以下方法,如果直接传入自己的函数 那么在函数内部 没办法用self获取内容
--self.btnRole.onClick:AddListener(self.BtnRoleClick)
--关闭按钮
self.btnClose.onClick:AddListener(function ()
self:HideMe()
end)
--单选框事件
--切页签
--toggle 对应委托 是UnityAction<bool>
self.togEquip.onValueChanged:AddListener(function (value)
if value == true then
self:ChangeType(1)
end
end)
self.togItem.onValueChanged:AddListener(function (value)
if value == true then
self:ChangeType(2)
end
end)
self.togGem.onValueChanged:AddListener(function (value)
if value == true then
self:ChangeType(3)
end
end)
end
end
--显示隐藏
function BagPanel:ShowMe()
self:Init()
self.panelObj:SetActive(true)
end
function BagPanel:HideMe()
self.panelObj:SetActive(false)
end
--逻辑处理函数 用来切页签
--type 1装备 2道具 3宝石
function BagPanel:ChangeType(type)
print("当前类型为".. type)
--切页 根据玩家信息 来进行格子创建
end
新增内容MainPanel
Lua
function MainPanel:BtnRoleClick()
BagPanel:ShowMe()
--print(123123)
--print(self.panelObj)
--等写了背包面板
--在这写显示背包
end
新建CSharpCallLua.cs
为了能xlua调用UnityAction<bool>
cs
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using XLua;
public class CSharpCallLua
{
[CSharpCallLua]
public static List<Type> cSharpCallLuaList = new List<Type>();
//记得代码生成
//目的是为了生成xlua代码 ???
}
可以参考https://blog.csdn.net/woodengm/article/details/112614506
格子逻辑
之前的MainPanel和BagPanel每次初始化都是一个表,只能表示一个对象
而格子会有很多个则没法用这种做法做。
粗暴的方法(后面有面向对象的方法)
BagPanel更新
Lua
...
--用来存储当前显示的格子
BagPanel.items = {}
BagPanel.nowType = -1
....
--显示隐藏
function BagPanel:ShowMe()
self:Init()
self.panelObj:SetActive(true)
if self.nowType == -1 then
self:ChangeType(1)
end
end
...
--逻辑处理函数 用来切页签
--type 1装备 2道具 3宝石
function BagPanel:ChangeType(type)
--如果已经是该页,就不更新
if self.nowType == type then
return
end
--切页 根据玩家信息 来进行格子创建
--更新之前 把老的格子删掉 BagPanel.items
for i = 1, #self.items do
--销毁格子对象
GameObject.Destroy(self.items[i].obj)
end
self.items = {}
--再根据当前选择的类型 来创建新的格子 BagPanel.items
--要根据传入的type 来选择显示的数据
local nowItems = nil
if type == 1 then
nowItems = PlayerData.equips
elseif type == 2 then
nowItems = PlayerData.items
else
nowItems = PlayerData.gems
end
--创建格子
for i = 1, #nowItems do
--有格子资源 在这 加载格子资源 实例化 改变图片 和文本 以及位置
local grid = {}
--用一张新表 代表 各自对象 里面的属性 存储对应想要的信息
grid.obj = ABMgr:LoadRes("ui","ItemGrid");
--设置父对象
grid.obj.transform:SetParent(self.Content,false)
--继续设置它的位置
grid.obj.transform.localPosition = Vector3((i-1)%4 * 175, math.floor((i-1)/4) * 175,0)
grid.imgIcon = grid.obj.transform:Find("imgIcon"):GetComponent(typeof(Image))
grid.Text = grid.obj.transform:Find("num"):GetComponent(typeof(Text))
--设置它的图标
--通过 道具id 去读取 道具配置表 得到图标信息
local data = ItemData[nowItems[i].id]
--想要的是data中的图标信息
--根据名字 先加载图集 再加载图集中的 图标信息
local strs = string.split(data.icon, "_")
--加载图集
local spritAtlas = ABMgr:LoadRes("ui",strs[1],typeof(SpriteAtlas))
--加载图标
grid.imgIcon.sprite = spritAtlas:GetSprite(strs[2])
--设置它的数量
grid.Text.text = nowItems[i].num
--把他存起来
table.insert(self.items,grid)
--这里实现了显示逻辑,但每次切换type要记得把老格子删除,逻辑在上面
end
end
优化格子对象
前面虽然实现了格子的基本逻辑,但因为不是面向对象的实现方式,不能实现格子的各种功能,如果需要与格子进行交互,就只能再bagPanel里实现前面的方法就不可行。
创建ItemGrid.lua
Lua
--用到之前讲过的 GameObject
--生成一个table 继承Object 主要目的是要它里面实现的 继承方法 subClass 和 new
Object:subClass("ItemGrid")
--"成员变量"
ItemGrid.obj = nil
ItemGrid.imgIcon = nil
ItemGrid.Text = nil
--成员函数
--实例化格子对象
function ItemGrid:Init(father,posX,posY)
self.obj = ABMgr:LoadRes("ui","ItemGrid");
--设置父对象
self.obj.transform:SetParent(father,false)
--继续设置它的位置
self.obj.transform.localPosition = Vector3(posX,posY,0)
--找控件
self.imgIcon = self.obj.transform:Find("imgIcon"):GetComponent(typeof(Image))
self.Text = self.obj.transform:Find("num"):GetComponent(typeof(Text))
end
--实例化格子对象
--data 是外面传入的 道具信息 里面包含了 id 和 num
function ItemGrid:InitData(data)
--通过 道具id 去读取 道具配置表 得到图标信息
local itemData = ItemData[data.id]
--想要的是data中的图标信息
--根据名字 先加载图集 再加载图集中的 图标信息
local strs = string.split(itemData.icon, "_")
--加载图集
local spritAtlas = ABMgr:LoadRes("ui",strs[1],typeof(SpriteAtlas))
--加载图标
self.imgIcon.sprite = spritAtlas:GetSprite(strs[2])
--设置它的数量
self.Text.text = data.num
end
--初始化格子信息
--加自己的逻辑
function ItemGrid:Destroy()
GameObject.Destroy(self.obj)
self.obj = nil
end
修改BagPanel
Lua
--更新之前 把老的格子删掉 BagPanel.items
for i = 1, #self.items do
--销毁格子对象
self.items[i]:Destroy()
end
...
--创建格子
for i = 1, #nowItems do
--根据数据 创建一个格子对象
local grid = ItemGrid:new()
--要实例化对象 设置位置
grid:Init(self.Content,(i-1)%4*175,math.floor((i-1)/4)*175)
--初始化它的信息 数量 和 图标
grid:InitData(nowItems[i])
--把他存起来
table.insert(self.items,grid)
--这里实现了显示逻辑,但每次切换type要记得把老格子删除,逻辑在上面
end
end
修改Main.lua
Lua
...
--之后的逻辑
require("MainPanel")
MainPanel:ShowMe()
require("BagPanel")
require("ItemGrid")
面板面向对象
面板里有Init()等相同的函数或变量,可以创建面板基类
新建BasePanel.lua
Lua
--利用面向对象
Object:subClass("BasePanel")
BasePanel.panelObj = nil
--相当于模拟一个字典 键为 控件名 值为控件本身
BasePanel.controls = {}
--用来判断是否已经初始化过了,因为父类的方法,子类不能用self.panelObj == nil 来判断
--所以用这个变量来判断
--作为事件监听标识
BasePanel.isInitEvent = false
function BasePanel:Init(name)
if self.panelObj == nil then
--公共的实例化对象的方法
self.panelObj = ABMgr:LoadRes("ui",name,typeof(GameObject))
self.panelObj.transform:SetParent(Canvas,false)
--GetComponentsInChildren() 得到所有挂载的
--找所有UI控件 存起来
--所有UI控件都继承UIBehaviour
local allControls = self.panelObj:GetComponentsInChildren(typeof(UIBehaviour))
--如果存入没用的UI控件怎么办
--为了避免找 各种无用控件 我们定一个规则 拼面板时 控件名按一定规则来
--Button btn名字
--Toggle tog名字
--Image img名字
--ScrollRect sv名字
for i = 0, allControls.Length - 1 do
local controlName = allControls[i].name
--对应c#中的数组,从0开始
if string.find(controlName,"btn") ~= nil or
string.find(controlName,"tog") or
string.find(controlName,"img") or
string.find(controlName,"sv") or
string.find(controlName,"txt") then
--为了让我们在得的时候 能够确定控件类型 我们需要存储类型
--利用反射 Type 得到 控件的类名
local typeName = allControls[i]:GetType().Name
--一个对象可能有多个ui组件 如同时又img txt,因此用表来存组件,名字为键
--最终存储形式
--{btnRole = {Image = 控件, Button = 控件 } ,
-- toggle = {Toggle = 控件 } }
if self.controls[allControls[i].name] ~= nil then
--table.insert(self.controls[allControls[i].name],allControls[i])
self.controls[controlName][typeName] = allControls[i]
else
--self.controls[allControls[i].name] = {allControls[i]}
--这仍有点问题,我们该怎么区分表里的btn和img这些类别呢,因此要用到上面存储的类型
--以下是正确的
self.controls[controlName] = {[typeName] = allControls[i]}
end
end
end
end
end
--得到控件 根据 控件依附对象的名字 和 控件的类型字符串名字 Button Image Toggle
function BasePanel:GetControl(name,typeName)
if self.controls[name] ~= nil then
local sameNameControls = self.controls[name]
if sameNameControls[typeName] ~= nil then
return sameNameControls[typeName]
end
end
return nil
end
function BasePanel:ShowMe(name)
self:Init(name)
self.panelObj:SetActive(true)
end
function BasePanel:HideMe()
self.panelObj:SetActive(false)
end
修改MainPanel.lua和BagPanel.lua
Lua
BasePanel:subClass("MainPanel")
function MainPanel:Init(name)
--使用父类的方法,但用 . 而不是 :
--要传入自己
--里面已经有判空
self.base.Init(self,name)
--为了只添加一次事件监听
if self.isInitEvent == false then
btnRole = self:GetControl("btnRole","Button")
btnRole.onClick:AddListener(function ()
self:BtnRoleClick()
end)
self.isInitEvent = true
end
end
function MainPanel:BtnRoleClick()
BagPanel:ShowMe("BagPanel")
end
Lua
BasePanel:subClass("BagPanel")
BagPanel.Content = nil
--用来存储当前显示的格子
BagPanel.items = {}
BagPanel.nowType = -1
--"成员方法"
--初始化方法
function BagPanel:Init(name)
self.base.Init(self,name)
--2.找到对应控件
--找到子对象 再找到身上挂载的 想要的脚本
--关闭按钮
if self.isInitEvent == false then
--找到没有挂载UI控件的对象还是需要手动去找
self.Content = self:GetControl("svBag","ScrollRect").transform:Find("Viewport"):Find("Content")
local group = self.panelObj.transform:Find("Group")
self:GetControl("btnClose","Button").onClick:AddListener(function ()
self:HideMe()
end)
--单选框事件
--切页签
--toggle 对应委托 是UnityAction<bool>
local group = self.panelObj.transform:Find("Group")
self:GetControl("togEquip","Toggle").onValueChanged:AddListener(function (value)
if value == true then
self:ChangeType(1)
end
end)
self:GetControl("togItem","Toggle").onValueChanged:AddListener(function (value)
if value == true then
self:ChangeType(2)
end
end)
self:GetControl("togGem","Toggle").onValueChanged:AddListener(function (value)
if value == true then
self:ChangeType(3)
end
end)
self.isInitEvent = true
end
end
--显示隐藏
function BagPanel:ShowMe(name)
self.base.ShowMe(self,name)
if self.nowType == -1 then
self:ChangeType(1)
end
end
--逻辑处理函数 用来切页签
--type 1装备 2道具 3宝石
function BagPanel:ChangeType(type)
--如果已经是该页,就不更新
if self.nowType == type then
return
end
--切页 根据玩家信息 来进行格子创建
--更新之前 把老的格子删掉 BagPanel.items
for i = 1, #self.items do
--销毁格子对象
self.items[i]:Destroy()
end
self.items = {}
--再根据当前选择的类型 来创建新的格子 BagPanel.items
--要根据传入的type 来选择显示的数据
local nowItems = nil
if type == 1 then
nowItems = PlayerData.equips
elseif type == 2 then
nowItems = PlayerData.items
else
nowItems = PlayerData.gems
end
--创建格子
for i = 1, #nowItems do
--根据数据 创建一个格子对象
local grid = ItemGrid:new()
--要实例化对象 设置位置
grid:Init(self.Content,(i-1)%4*175,math.floor((i-1)/4)*175)
--初始化它的信息 数量 和 图标
grid:InitData(nowItems[i])
--把他存起来
table.insert(self.items,grid)
--这里实现了显示逻辑,但每次切换type要记得把老格子删除,逻辑在上面
end
end
lua文件迁移小工具
LuaCopyEditor.cs
cs
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.IO.Enumeration;
using Unity.VisualScripting;
using UnityEditor;
using UnityEngine;
public class LuaCopyEditor : Editor
{
[MenuItem("XLua/自动生成txt后缀的lua")]
public static void CopyLuaToTxt(){
//找到所有lua文件
string path = Application.dataPath + "/Lua/";
if(!Directory.Exists(path))
return;
//得到每一个lua文件的路径 才能迁移拷贝
//得到.lua文件路径
string[] strs = Directory.GetFiles(path,"*.lua");
//把lua文件拷贝到新的文件夹中
//首先 定一个新路径
string newPath = Application.dataPath + "/LuaTxt/";
//为了避免一些被删除的lua文件 不再使用 我们应该先清空目标路径
//判断新路径文件夹是否存在
if(!Directory.Exists(newPath))
Directory.CreateDirectory(newPath);
else{
//得到该路径中 所有后缀.txt的文件 把他们全部删除
string[] oldFileStrs = Directory.GetFiles(newPath,"*.txt");
for(int i = 0; i < oldFileStrs.Length; i++){
File.Delete(oldFileStrs[i]);
}
}
List<string> newFileNames = new List<string>();
string fileName;
for(int i = 0; i < strs.Length; i++){
//得到新的文件路径 用于拷贝
fileName = newPath + strs[i].Substring(strs[i].LastIndexOf("/") + 1) + ".txt";
newFileNames.Add(fileName);
File.Copy(strs[i],fileName);
}
AssetDatabase.Refresh();
//刷新过后再来改指定AB包 如果不刷新 第一次改变 会没用
for(int i = 0; i < newFileNames.Count; i++){
//Unity API
//该API传入的路径 必须是 相对Assets文件夹的 Assets/.../...
AssetImporter importer = AssetImporter.GetAtPath( newFileNames[i].Substring(newFileNames[i].IndexOf("Assets")));
if(importer != null)
importer.assetBundleName = "lua";
}
}
}
LuaMgr中可以注释掉
cs
//luaEnv.AddLoader(MyCustomLoader);
只使用
cs
luaEnv.AddLoader(MyCustomLoaderFormAB);
即从AB包中加载
注意事项
每次ab包打包时,要将xlua代码清楚后打包,不然会报错
记得打包完后重新生成xlua代码