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

相关推荐
Artistation Game1 天前
九、怪物行为逻辑
游戏·unity·游戏引擎
百里香酚兰1 天前
【AI学习笔记】基于Unity+DeepSeek开发的一些BUG记录&解决方案
人工智能·学习·unity·大模型·deepseek
dangoxiba1 天前
[Unity Demo]从零开始制作空洞骑士Hollow Knight第十三集:制作小骑士的接触地刺复活机制以及完善地图的可交互对象
游戏·unity·visualstudio·c#·游戏引擎
先生沉默先2 天前
使用Materialize制作unity的贴图,Materialize的简单教程,Materialize学习日志
学习·unity·贴图
十画_8242 天前
Visual Studio 小技巧记录
unity·visual studio
red_redemption2 天前
cpp,git,unity学习
git·unity·游戏引擎
tealcwu2 天前
【Unity踩坑】Unity更新Google Play结算库
unity·游戏引擎
先生沉默先2 天前
unity 默认渲染管线材质球的材质通道,材质球的材质通道
unity·游戏引擎·材质
白鹭float.2 天前
【Unity AI】基于 WebSocket 和 讯飞星火大模型
人工智能·websocket·unity
一个程序员(●—●)2 天前
Unity各个操作功能+基本游戏物体创建与编辑+Unity场景概念及文件导入导出
unity·游戏引擎