xLua背包实践

主要学习目标

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代码

相关推荐
June bug14 小时前
【领域知识】休闲游戏一次发版全流程:Google Play + Apple App Store
unity
星夜泊客16 小时前
C# 基础:为什么类可以在静态方法中创建自己的实例?
开发语言·经验分享·笔记·unity·c#·游戏引擎
dzj202117 小时前
PointerEnter、PointerExit、PointerDown、PointerUp——鼠标点击物体,则开始旋转,鼠标离开或者松开物体,则停止旋转
unity·pointerdown·pointerup
心前阳光18 小时前
Unity 模拟父子关系
android·unity·游戏引擎
在路上看风景21 小时前
26. Mipmap
unity
咸鱼永不翻身1 天前
Unity视频资源压缩详解
unity·游戏引擎·音视频
在路上看风景1 天前
4.2 OverDraw
unity
在路上看风景1 天前
1.10 CDN缓存
unity
ellis19701 天前
Unity插件SafeArea Helper适配异形屏详解
unity
nnsix1 天前
Unity Physics.Raycast的 QueryTriggerInteraction枚举作用
unity·游戏引擎