0%

目录

新手引导系统

需求分析

  1. 强制引导。必须要完成的,常见是进入游戏新手副本。

  2. 条件触发引导。由各种条件达成触发,如达到等级,通关副本等。

  3. 中断后再次执行。玩家因异常或主动退出中断引导,根据关键步是否完成情况决定是否再次打开。

  4. 异常处理。当条件不满足时执行具体的处理方法。比如提交物品,不存在对应物品,引导直接结束。

  5. 附加协议。用于某些关键步,确定收发了指定协议然后确定步骤是否完成。比如引导召唤就判断召唤协议是否返回。

  6. 并行。引导动作是否并行执行,比如战斗引导中,英雄挂了,并行执行救援引导。

系统设计架构

新手指引遮罩

挖空矩形遮罩上的目标显示区域实现

  1. 编写继承Graphic的脚本类,绑定在mask view上

  2. 根据指定的节点,计算中心点,宽高等数据,确定挖孔区域

  3. 在OnPopulateMesh回调中重绘顶点数据,绘制四个矩形组成的一个中间空的图形,这个就是所谓的挖孔实现

  4. 脚本类集成IPointerClickHandler接口,在OnPointerClick回调中根据鼠标点位置,判断是否点击在挖孔区域内,接着执行lua点击回调,同时传递点击事件

1
ExecuteEvents.Execute(arrow.gameObject, ed, ExecuteEvents.pointerClickHandler);

行为树

利用行为树控制整个流程,一个引导执行完毕后接着执行另一个引导。

  1. action的执行有行为树FlowSequence类控制,action作为它的children节点,控制其串行执行

  2. 每个节点有状态和结果两个属性,onStart()开始执行,执行完成调用onDone(),通知父节点执行next child的onStart()

目录

海外版本diff工具

一、背景

海外版本是从国内版本基础上做差异化替换而来,代码上采用replaceModule的方式覆盖重写module的方法,资源上优先加载对应语言目录下的替换资源。

这种方案决定了,大多数替换内容(代码、预制物等) 是在源内容的基础上演变而来,依赖源内容。当合并国内版本后,需要检查源内容做了什么哪些修改,代码上有没有逻辑冲突,预制资源方面需不需要更新。

可以总结有两个需求:

  1. 检查replace的module在合并前后代码有哪些更新

  2. 检查出替换的预制在合并前后有哪些是更新了

更新:

追加了其他两个需求:

1.检查多语言的scene目录资源在合并前后的更改和新增情况

2.检查fmod资源的更新情况

二、使用介绍

选项介绍:

  • 语言类型:选择当前语言类型,必填。

  • 合并后版本号:海外dev分支合并国服版本后的commitID,commitID太长不一定需要填全部,git能够识别commit即可,必填。

  • 合并前版本号:海外的dev分支合并国服版本前的commitID,必填。

  • 自定义执行shell文件的exe路径:用于执行diff.sh脚本的shell exe路径。假如你的系统有git-bash.exe,填它的路径也可以。windows上必填,因为windows上默认的终端可能是cmd.exe,它执行shell文件会不成功。mac系统可以不填。

比如台湾版本:

语言类型选:tw

合并前后版本到gitlab pjg-client仓库commit栏目上找commitID。

功能介绍:

  • 导出replace module更改:将replace module代码文件的差异导出到桌面上,文件名replace-module-{commitID0}-{commitID1}.diff。

  • 导出多语言目录zh/ui/views下的预制perfab文件更新情况:将多语言目录下ui的prefab文件对应的zh目录源文件在两个版本间的差异,和zh目录下新增但海外语言目录没有的预制内容导出到桌边,文件名zh-UIPrefab-{commitID0}-{commitID1}.diff。

  • 导出多语言目录zh/scene下所有资源的更新情况:同上,多语言目录的资源对应的zh目录下的资源,对比两个版本前后zh目录的资源更新情况和新增情况,然后由场景决定是否需要更新对应多语言(如tw)目录的资源。执行会导出diff文件到桌面。

  • 导出fmod event的更改情况:检出更改的文件,决定是否更改对应多语言目录下的文件。

三、实现思路

实现原理很简单,就是找出相应的文件名,调用git diff命令输出差异即可。

四、sh脚本语法

工具利用c# API Process类执行sh脚本,diff.sh脚本主要做两件事情,一是解析参数,二是调用git diff命令。

这里简单解析下sh语法,帮助理解代码。

1、获取参数

1
\$@

2、定义数组

1
commitIdArr=()

3、数值自增
1
((commitIdArr\_index++))

4、获取变量值
1
$moduleNameArr_index或者${outputFileName}

5、输出数组内容
1
echo moduleNameArr\[@]

6、数组长度
1
\${#moduleNameArr\[@]}

学习shell脚本语法可以查看这里:shell教程

代码:

LanguageDiffWindow.cs

diff.sh

UIGraphicText表情渲染优化-支持表情来自不同图集

背景

当表情sprite没有被打到一个图集上,UIGraphicText类渲染表情或会错乱。

表情的渲染过程

聊天窗口表情的渲染过程大致如何:

  • ui_emoji.asset包含所有emoji sprite信息,包括name、引用等。
  • UIGraphicText主要是解析输入内容字符串,得出sprite的顶点几何参数等。
  • CanvasRender应是负责渲染的。
  • UIGraphicTextSprites继承自MaskableGraphic类,这个类的作用是管理源图集,截取源图集的某个区域。

获取源图集的代码:

表情的源图集引用被写死,默认第一个表情的源图集。这就是当表情来自其他图集时,显示错误的原因。

优化

目前所有的表情会打到一个图集上,下面两种场景会导致表情打到不同图集:

  1. 表情增加,直到2048*2048大小装不下(图集maxsize为2048是项目组限制,因为在一些低端机上加载不了大于2048的图)
  2. 表情sprite的tag不一致,需要用别的tag划分表情(tag决定sprite所属的图集)

为了满足以上两种场景,需要对这个系统进行优化,支持表情来自不同的图集。

有两个步骤:

  1. 更改mainTexture获取

展开源码

// UIGraphicTextSprites.cs

public class UIGraphicTextSprites : MaskableGraphic

{

private Texture _texSource; // 由外部传入改写mainTexture

public override Texture mainTexture

{

get

{

if (m_spriteAsset == null)

return s_WhiteTexture;

if (m_spriteAsset.listSpriteInfor == null || m_spriteAsset.listSpriteInfor.Count == 0)

{

return s_WhiteTexture;

}

if (_texSource != null)

{

return _texSource;

}

var texSource = m_spriteAsset.listSpriteInfor[0].sprite.texture;

if (texSource == null)

return s_WhiteTexture;

else

return texSource;

}

}

public void SetMainTexture(Texture t)

{

_texSource = t;

}

}

2. 更改对应的源图集

展开源码

// UIGraphicText.cs

void CalcQuadTag(IList\<UIVertex> verts)

{

Texture texSource = listSpriteInfor[0].sprite.texture;

for (int j = 0; j \< listSpriteInfor.Count; j++)

{

//通过标签的名称去索引spriteAsset里所对应的sprite的名称

if (listTagInfor[i].name == listSpriteInfor[j].name) {

spriteRect = listSpriteInfor[j].sprite.textureRect;

texSource = listSpriteInfor[j].sprite.texture;

m_spriteGraphic.SetMainTexture(texSource);

break;

}

}

// Texture texSource = listSpriteInfor[0].sprite.texture;

}

0714更新

发现动态表情在pc上不打图集情况下显示有问题,表情并没有想预期一样动,有的甚至不动。

动态表情的实现原理是按一定的帧率改变sprite的顶点、uv和网格三角形参数,通过CanvasRenderer组件和UIGraphicTextSprites组件(继承MaskableGraphic),截取源图texture(sprite或者图集)得到显示区域。

原因是不打图集时,动态表情的源图获取不正确,截取的源图永远是第一张。

解决方法是在Update()中切换sprite截取参数同时改变UIGraphicTextSprites的mainTexture。

更改commit看这里。

检验

我的unity目标平台是android,先将textureImporter对安卓图片压缩格式更改代码注释掉,否则更改压缩格式会被重置。

更改1601_1,1602_1,1603_1的format为区别于ETC2_RGBA8的其他格式,测试不同压缩格式打包不同图集的场景:

更改1604_1,1605_1的tag,测试不同tag打包到不同图集的场景:

sprite packer上pack图集,检查表情的确打到了三个不同的图集:

C:\\883fd9a545661d5f5811d3edac78f399C:\\c0bf4aa254b650fb676bfa50bfbec15f

pc上测试,正常。

安卓上测试,正常。

游戏项目中一般会用到一些优化手段,大部分优化的是资源的大小对内存的影响,比如项目中特效的加载会拥有两个类:photoFactoryEffect和photoMutilEffect

photoFactory与photoMutil的区别

photoFactory是一个优化类,作用就是减轻通过photoMutil生成的特效克隆go的大小,优化的是内存

photoMutil生成一个挂载特效rt的go,真正管理特效的是photoProducer。

特效加载原理

特效的gameObject通过camaro和rt映射到其他的gameObject上,一个特效只会加载一次,利用rt分发多份。

// PhotoEffectFactory.lua module(“logic.common.ugui.PhotoEffectFactory”,package.seeall) local PhotoEffectFactory = class(“PhotoEffectFactory”) function PhotoEffectFactory:ctor() self._factoryContainer = goutil.create(“PHOTOFACTORYROOT”, false) GameUtils.setPos(self._factoryContainer, -2500, 0, 0) – 所有生效的特效 self._effects={} – 池化 self._pool=ObjectPool.New(20,PhotoEffectFactory._createEffect, PhotoEffectFactory._disposeEffect, PhotoEffectFactory._resetEffect ) end function PhotoEffectFactory._createEffect() local effect = {} local container = goutil.create(“eff”, true) container.layer = Framework.LayerUtil.NameToLayer(SceneLayer.UI) effect.container = container effect.photoEff = Framework.LuaComponentContainer.Add(container, PhotoMultiEffect) effect.photoEff:setEffectLoadedCallback(PhotoEffectFactory._onEffectLoaded, PhotoEffectFactory.instance) –克隆的特效 effect.clones={} – 引用数 effect.count=0 return effect end function PhotoEffectFactory._resetEffect(effect) effect.count=0 effect.clones={} effect.photoEff:clear() end function PhotoEffectFactory._disposeEffect(effect) if not goutil.isNil(effect.container) then goutil.destroy(effect.container) end end – 外部接口:提供256 * 144规格的RT特效克隆体 –uiWidth, uiHeight 需要显示的ui尺寸,请根据具体界面情况传入 function PhotoEffectFactory:getSmallClonePhotoEffect(url, uiWidth, uiHeight) return self:getClonePhotoEffect(url, PhotoUtil.SmallRTWidth, PhotoUtil.SmallRTHeight, uiWidth, uiHeight) end – 外部接口:重新播放RT特效 function PhotoEffectFactory:rePlayPhotoEffect(url) local photoData = url and self._effects[url] if photoData then if not goutil.isNil(photoData.orgGoInst) then goutil.setActive(photoData.orgGoInst, false) goutil.setActive(photoData.orgGoInst, true) end end end – 获取克隆的RT特效预制物 –uiWidth, uiHeight 需要显示的ui尺寸,请根据具体界面情况传入 function PhotoEffectFactory:getClonePhotoEffect(url, rtWidth, rtHeight, uiWidth, uiHeight) if GameUtils.isEmptyString(url) then return end – 如果未生成 local eff=self._effects[url] if eff==nil then eff=self._pool:fetchObject() local container=eff.container rtWidth = rtWidth or PhotoUtil.PartRTWidth rtHeight = rtHeight or PhotoUtil.PartRTHeight goutil.setWidth(container.transform, uiWidth or rtWidth)–ui尺寸默认是rt大小 goutil.setHeight(container.transform, uiHeight or rtHeight) – 放在factory root 下 goutil.addChildToParent(eff.container, self._factoryContainer) – rename eff.container.name=url self._effects[url]=eff –临时处理,后续优化 if rtWidth == PhotoUtil.SmallRTWidth and rtHeight == PhotoUtil.SmallRTHeight then eff.photoEff:showSmallEffect(url) else eff.photoEff:showPartEffect(url) end end eff.count = eff.count + 1 –只是克隆rawImage,transform local objClone = goutil.create(tostring(eff.count),true) objClone.layer = Framework.LayerUtil.NameToLayer(SceneLayer.UI) GameUtils.copyRectTransform(objClone:GetComponent(goutil.Type_RectTransform), eff.container:GetComponent(goutil.Type_RectTransform)) GameUtils.copyRawImage(objClone:AddComponent(typeof(UnityEngine.UI.RawImage)), eff.container:GetComponent(typeof(UnityEngine.UI.RawImage))) – local objClone = goutil.clone(self._urlToPhotoDict[url].container, tostring(self._urlToPhotoDict[url].count)) —————————– – 判断当前特效是否已经加载完成,是则直接显示 if eff.photoEff:checkIsFinishLoadByUrl(url) then local rawImageComp = objClone:GetComponent(typeof(UnityEngine.UI.RawImage)) if rawImageComp then rawImageComp.enabled = true end end table.insert(eff.clones, objClone) return objClone end – 判断创建出来的GameObject的RT与复制品的RT是否指向同一张RT,如果不是,则需要将复制品指向的RT修改过来 function PhotoEffectFactory:_pointToSameRT(url) local photoData = self._effects[url] if photoData and photoData.photoEff then local orgRawImage = photoData.photoEff:getRawImage() if orgRawImage then local goCopy = photoData.clones and photoData.clones[1] if not goutil.isNil(goCopy) then local copyRawImage = goCopy:GetComponent(typeof(UnityEngine.UI.RawImage)) if copyRawImage and orgRawImage.texture ~= copyRawImage.texture then for i = 1, #photoData.clones do goCopy = photoData.clones[i] if not goutil.isNil(goCopy) then copyRawImage = goCopy:GetComponent(typeof(UnityEngine.UI.RawImage)) if copyRawImage then copyRawImage.texture = orgRawImage.texture end end end end end end end end function PhotoEffectFactory:turnOn(url) if self._effects and self._effects[url] then if self._effects[url].photoEff then self._effects[url].photoEff:turnOn() self:_pointToSameRT(url) end end end function PhotoEffectFactory:turnOff(url) if self._effects and self._effects[url] then if self._effects[url].photoEff then self._effects[url].photoEff:turnOff() end end end function PhotoEffectFactory:_onEffectLoaded(goInst, res) if res and res.ResPath then local data = self._effects and self._effects[res.ResPath] if data then data.orgGoInst = goInst if data.clones then local rawImageComp for i = 1, #data.clones do if not goutil.isNil(data.clones[i]) then rawImageComp = data.clones[i]:GetComponent(typeof(UnityEngine.UI.RawImage)) if rawImageComp then rawImageComp.enabled = true end end end end end end end function PhotoEffectFactory:destroyClonePhotoEffect(url,justclear) local photoEffect=self._effects and self._effects[url] if photoEffect then photoEffect.count = photoEffect.count - 1 if photoEffect.count <= 0 then self._pool:returnObject(photoEffect) self._effects[url]=nil end end end – 预留接口:清除所有的PhotoEffect function PhotoEffectFactory:destroy() if self._effects then self._pool:clear() self._effects={} end end PhotoEffectFactory.instance = PhotoEffectFactory.New() return PhotoEffectFactory

// photoMultiEffect.lua module(“logic.common.ugui.PhotoMultiEffect”, package.seeall) local PhotoMultiEffect = class(“PhotoMultiEffect”) function PhotoMultiEffect:ctor(compContainer) self._compContainer = compContainer self._go = self._compContainer.gameObject self._multiLoader = MultiResLoader.New() –资源的引用都在这里引用 self._photo = Framework.PhotoBase.Add(self._go) self._rtWidth = PhotoUtil.PartRTWidth self._rtHeight = PhotoUtil.PartRTHeight self._effectLoadedCallback = nil self._effectLoadedCallbackObj = nil self._effectOnePlayFinishCallback = nil self._effectOnePlayFinishCallbackObj = nil self._bInitUVRect = true self._urls = {} self._goInstList = {} PhotoUtil.initPhotoSetting() self:_initRawImage() end function PhotoMultiEffect:_initRawImage() if not goutil.isNil(self._go) then self._rawImage = self._go:GetComponent(typeof(UnityEngine.UI.RawImage)) if self._rawImage == nil then self._rawImage = self._go:AddComponent(typeof(UnityEngine.UI.RawImage)) end self._rawImage.enabled = false self._rawImage.raycastTarget = false self._rawImage.material = PhotoUtil.getEffMaterial() end end function PhotoMultiEffect:_setRawImageUVRect(rtWidth, rtHeight) if self._bInitUVRect and self._rawImage then self._rtWidth = rtWidth self._rtHeight = rtHeight PhotoUtil.setRTCapacity(rtWidth, rtHeight) –计算RawImage的大小以及偏移 local uvRectW = math.min(1, goutil.getWidth(self._go.transform) / rtWidth) local uvRectH = math.min(1, goutil.getHeight(self._go.transform) / rtHeight) local uvRectX = (1 - uvRectW) * 0.5 local uvRectY = (1 - uvRectH) * 0.5 self._rawImage.uvRect = UnityEngine.Rect.New(uvRectX, uvRectY, uvRectW, uvRectH) self._bInitUVRect = false end end – 该接口只提供给PhotoEffectFactory使用,其他地方不允许调用 function PhotoMultiEffect:getRawImage() return self._rawImage end function PhotoMultiEffect:setEffectLoadedCallback(callback, callbackObj) self._effectLoadedCallback = callback self._effectLoadedCallbackObj = callbackObj end function PhotoMultiEffect:setEffectOnePlayFinishCallback(callback, callbackObj) self._effectOnePlayFinishCallback = callback self._effectOnePlayFinishCallbackObj = callbackObj end function PhotoMultiEffect:setClickEnable(bRaycastTarget) if self._rawImage then self._rawImage.raycastTarget = bRaycastTarget end end function PhotoMultiEffect:setClickCallback(callback, callbackObj) if callback == nil then return end if goutil.isNil(self._go) then return end if not self._btnClick then local compButton = self._go:GetComponent(typeof(UnityEngine.UI.Button)) if compButton == nil then self._go:AddComponent(typeof(UnityEngine.UI.Button)) end self._btnClick = Framework.ButtonAdapter.Get(self._go) self._btnClick:AddClickListener(callback, callbackObj) end end – 小图标规格的RT特效 256 * 144 function PhotoMultiEffect:showSmallEffect(…) local urls = {…} if not urls or #urls == 0 then return end self:loadRes(urls, PhotoUtil.SmallRTWidth, PhotoUtil.SmallRTHeight) end – 中等规格的RT特效 512 * 288 function PhotoMultiEffect:showPartEffect(…) local urls = {…} if not urls or #urls == 0 then return end self:loadRes(urls, PhotoUtil.PartRTWidth, PhotoUtil.PartRTHeight) end – 全屏规格的RT特效 1280 * 720 function PhotoMultiEffect:showFullScreneEffect(…) local urls = {…} if not urls or #urls == 0 then return end self:loadRes(urls, PhotoUtil.RTWidth, PhotoUtil.RTHeight) end – 最大全屏规格的RT特效 1560 * 720,不能再接受更大的尺寸规格 function PhotoMultiEffect:showMaxScreneEffect(…) local urls = {…} if not urls or #urls == 0 then return end self:loadRes(urls, PhotoUtil.MaxRTWidth, PhotoUtil.MaxRTHeight) end function PhotoMultiEffect:loadRes(urls, rtWidth, rtHeight) if self._photo == nil then return end self:_setRawImageUVRect(rtWidth, rtHeight) self._photo:TurnOn(rtWidth, rtHeight) self._isOn = true local renderTexture = self._photo and self._photo.producer and self._photo.producer.renderTexture if renderTexture == nil then printError(“renderTexture is nil, self._go=”, self._go.name) self._rawImage.enabled = false return – 重复 photobase.turnOn里已经做了 – else – self._rawImage.texture = renderTexture end local len = #urls for i = 1, len do if not self._urls then self._urls = {} end table.insert(self._urls, urls[i]) self._multiLoader:addResPath(urls[i]) end PhotoUtil.addUsingRTCount(self, rtWidth, rtHeight) –暂时没有全部加载完毕逻辑回调 self._multiLoader:load(_, self._onSingleResLoaded, self) end function PhotoMultiEffect:_onSingleResLoaded(res) if res.IsSuccess then if goutil.isNil(self._go) or self._photo == nil then self:clear() return end –9/4修改:self._isOn为false表示有两种情况,一种是在特效加载完成之前调用了turnOff接口,一种是调用了clear接口; –按道理调用clear接口并不会回调到这里,所以处理第一种情况就好 if not self._isOn then if self._urls == nil or #self._urls == 0 then self:turnOff() return end end local goInst if self._urls then for i = #self._urls, 1, -1 do if res.ResPath == self._urls[i] then table.remove(self._urls, i) goInst = goutil.clone(res:GetAsset(nil, nil)) goInst.layer = Framework.LayerUtil.NameToLayer(PhotoUtil.LayerName) table.insert(self._goInstList, goInst) break end end end if goInst then if self._photo then –检查当前是turnOff还是turnOn – goutil.setActive(goInst, self._isOn) self._rawImage.enabled = self._isOn if self._isOn then self._photo:ShowTarget(goInst, true) else goutil.addChildToParent(goInst, self._go) end end if self._effectLoadedCallback then if self._effectLoadedCallbackObj then self._effectLoadedCallback(self._effectLoadedCallbackObj, goInst, res) else self._effectLoadedCallback(goInst, res) end end –检查美术是否有挂载EffectPlayer的组件 local _effectCSComp = goInst:GetComponent(typeof(Pjg.EffectPlayer)) if _effectCSComp and not goutil.isNil(_effectCSComp) then –参数默认以组件的,暂不支持外部设置参数 _effectCSComp:AddFinishListener(self._onEffectOnePlayFinish, self) –加载好就立即执行play _effectCSComp:Play() end end end end –暂时没有全部播放完毕逻辑回调 function PhotoMultiEffect:_onEffectOnePlayFinish() if goutil.isNil(self._go) then return end if self._effectOnePlayFinishCallback then if self._effectOnePlayFinishCallbackObj then self._effectOnePlayFinishCallback(self._effectOnePlayFinishCallbackObj) else self._effectOnePlayFinishCallback() end end end function PhotoMultiEffect:getFirstUrl() return self._urls and self._urls[1] end function PhotoMultiEffect:getUrlString() return table.concat(self._urls, “#”) end – 根据传入的路径判断该特效是否已经加载完成 function PhotoMultiEffect:checkIsFinishLoadByUrl(url) local bLoaded = false if self._multiLoader and self._multiLoader:getResource(url) then return true end end – bReplay:是否重播,有些特效需要再次打开的时候调用 – 注意:重播只针对本身是显示状态的特效,如果本身特效由于功能需要自己被隐藏,bReplay传入true也是没法重播 function PhotoMultiEffect:turnOn(bReplay) if not self._isOn then if self._rawImage then self._rawImage.enabled = true end if self._goInstList and self._photo then self._photo:TurnOn(self._rtWidth, self._rtHeight) PhotoUtil.addUsingRTCount(self, self._rtWidth, self._rtHeight) local goInst for i = 1, #self._goInstList do goInst = self._goInstList[i] if not goutil.isNil(goInst) then if bReplay and goInst.activeSelf then goutil.setActive(goInst, false) goutil.setActive(goInst, true) end –检查美术是否有挂载EffectPlayer的组件 local _effectCSComp = goInst:GetComponent(typeof(Pjg.EffectPlayer)) if _effectCSComp and not goutil.isNil(_effectCSComp) then _effectCSComp:Stop() _effectCSComp:RemoveFinishListener() –参数默认以组件的,暂不支持外部设置参数 _effectCSComp:AddFinishListener(self._onEffectOnePlayFinish, self) _effectCSComp:Play() end self._photo:ShowTarget(goInst, true) end end end self._isOn = true end end function PhotoMultiEffect:turnOff() if goutil.isNil(self._go) then return end if self._isOn then if self._rawImage then self._rawImage.enabled = false end if self._goInstList and self._photo then for i = 1, #self._goInstList do if not goutil.isNil(self._goInstList[i]) then –goutil.setActive(self._goInstList[i], false) goutil.addChildToParent(self._goInstList[i], self._go) end end end if self._photo then self._photo:TurnOff() PhotoUtil.reduceUsingRTCount(self, self._rtWidth, self._rtHeight) end self._isOn = false end end function PhotoMultiEffect:clear() self._urls = {} if self._multiLoader then self._multiLoader:clear() end if self._goInstList then for i = 1, #self._goInstList do if not goutil.isNil(self._goInstList[i]) then –检查美术是否有挂载EffectPlayer的组件 local _effectCSComp = self._goInstList[i]:GetComponent(typeof(Pjg.EffectPlayer)) if _effectCSComp and not goutil.isNil(_effectCSComp) then _effectCSComp:Stop() _effectCSComp:RemoveFinishListener() end goutil.destroy(self._goInstList[i]) end end self._goInstList = {} end if self._photo then self._photo:TurnOff() PhotoUtil.reduceUsingRTCount(self, self._rtWidth, self._rtHeight) end self._isOn = false if goutil.isNil(self._go) then return end if self._rawImage then self._rawImage.enabled = false end self._bInitUVRect = true end function PhotoMultiEffect:OnDestroy() self:clear() self._multiLoader=nil self._compContainer = nil self._go = nil self._photo = nil self._rawImage = nil self._effectLoadedCallback = nil self._effectLoadedCallbackObj = nil self._effectOnePlayFinishCallback = nil self._effectOnePlayFinishCallbackObj = nil self._urls = nil self._goInstList = nil if self._btnClick then self._btnClick:RemoveClickListener() self._btnClick = nil end end – 判断是否是空闲,需要加载特效 function PhotoMultiEffect:isFree() return not self._isOn and self._multiLoader.totalCount==0 end return PhotoMultiEffect

// PhotoBase.cs using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.UI; namespace Framework { public class PhotoBase : MonoBehaviour { protected PhotoProducer _producer; protected RawImage _rawImg; public static PhotoBase Add(GameObject go) { return go.AddComponentOnce(); } void Awake() { //2018/8/8修改,防止gameObject或父节点被隐藏,lua已经初始化RawImage后显示gameObject的时候Awake接口被调用 _rawImg = gameObject.GetComponent (); if (_rawImg == null) { _rawImg = gameObject.AddComponentOnce(); _rawImg.raycastTarget = false; _rawImg.texture = null; _rawImg.material = PhotoProducerCache.Instance.GetPhotoMaterial(); } } protected virtual void OnEnable() { if(_producer != null) { _producer.producerContainer.SetActive(true); } } protected virtual void OnDisable() { if(_producer != null && _producer.producerContainer != null) { _producer.producerContainer.SetActive(false); } } public void EnableClick(bool isEnable) { if(isEnable) { _rawImg.raycastTarget = true; } else { _rawImg.raycastTarget = false; } } //在turn on的时候才有值 public PhotoProducer producer { get{ return _producer;} } //在turn on的时候才能调用 public void SetCameraPosition(float x,float y,float z) { GlobalObject.gVec3.x = x; GlobalObject.gVec3.y = y; GlobalObject.gVec3.z = z; _producer.rtCamera.transform.localPosition = GlobalObject.gVec3; } //在turn on的时候才能调用 public void SetCameraRotation(float x,float y,float z) { _producer.rtCamera.transform.localRotation = Quaternion.Euler(x,y,z); } /** * 将一个物体放入拍摄空间内 * */ public void ShowTarget(GameObject go,bool allSameLayer) { if(allSameLayer) go.SetLayerRecursively(PhotoProducerCache.Instance.GetCullingLayer()); else go.layer = PhotoProducerCache.Instance.GetCullingLayer(); go.transform.SetParent(_producer.targetContainer.transform,false); } /** * 把拍摄目标全部移除 * */ public void RemoveAllTargets() { GameObjectUtil.ClearChildren(_producer.targetContainer); } public void TurnOn() { if(_producer == null) { Rect size = (gameObject.transform as RectTransform).rect; _producer = PhotoProducerCache.Instance.Fetch(Mathf.CeilToInt(size.width),Mathf.CeilToInt(size.height)); _rawImg.texture = _producer.renderTexture; } if(this.gameObject.activeInHierarchy) { _producer.producerContainer.SetActive(true); } else { _producer.producerContainer.SetActive(false); } } ///

/// 重载TurnOn接口,支持外部传入需要创建的RT大小 /// public void TurnOn(float width, float height) { if(_producer == null) { if (_rawImg == null) { _rawImg = gameObject.GetComponent (); } _producer = PhotoProducerCache.Instance.Fetch(Mathf.CeilToInt(width),Mathf.CeilToInt(height)); _rawImg.texture = _producer.renderTexture; } if(this.gameObject.activeInHierarchy) { _producer.producerContainer.SetActive(true); } else { _producer.producerContainer.SetActive(false); } } public bool IsOn { get{ return _producer != null;} } public void TurnOff() { if(_producer != null) { PhotoProducerCache.Instance.Return(_producer); _producer = null; } } void OnDestroy() { TurnOff(); } } }

// photoProducer.cs using System.Collections; using System.Collections.Generic; using UnityEngine; namespace Framework { public class PhotoProducer { private GameObject _rtContainer; private GameObject _targetContainer; private Camera _rtCamera; private RenderTexture _rt; public GameObject producerContainer { get { return _rtContainer;} } public GameObject targetContainer { get {return _targetContainer;} } public Camera rtCamera { get {return _rtCamera;} } public RenderTexture renderTexture { get {return _rt;} } public PhotoProducer(int width,int height,int cullingMask,int rtDepth,RenderTextureFormat rtFormat,int antiAliasing) { _rtContainer = new GameObject(“PhotoProducer_“ + PhotoProducerCounter.GetCounter()); _targetContainer = new GameObject(“TargetContainer”); _targetContainer.transform.SetParent(_rtContainer.transform,false); _rtContainer.transform.position = PhotoProducerCounter.GenPosition(); GameObject.DontDestroyOnLoad(_rtContainer); GameObject cameraObj = new GameObject(“Camera”); cameraObj.transform.SetParent(_rtContainer.transform, false); this._rtCamera = cameraObj.AddComponent(); this._rtCamera.useOcclusionCulling = false; _rtCamera.enabled = true; _rtCamera.clearFlags = CameraClearFlags.SolidColor; _rtCamera.backgroundColor = new Color(0, 0, 0, 0); _rtCamera.cullingMask = cullingMask; _rtCamera.targetTexture = _rt; _rtCamera.farClipPlane = 1000; _rt = new RenderTexture(width, height,rtDepth,rtFormat); if(antiAliasing > 0) _rt.antiAliasing = antiAliasing; _rtCamera.targetTexture = _rt; } public void Dispose() { if (_rtCamera != null) { _rtCamera.targetTexture = null; } if(_rt != null) { GameObject.Destroy(_rt); _rt = null; } GameObjectUtil.Destroy(_rtContainer); } } }

// photoProducerCache.cs using System.Collections.Generic; using UnityEngine; namespace Framework { /** * UI特效公用的RenderTexture缓存 以及 用于绘制的Camera和GameObject * RenderTexture的大小 有若干种规格,游戏项目中规划好预定的几种规格规范 * 规格1 1136 640 * 规格2 200*200 * */ public class PhotoProducerCache : Singleton { //一些设置 private int _photoCullingLayer = 0; private int _photoCullingMask = 0; private int _rtDepth = 32; private RenderTextureFormat _rtFormat = RenderTextureFormat.ARGB32; private int _antiAliasing = 2; private Material _photoMat; //每种规格 对应的 缓存RenderTexture列表 private Dictionary<int,List> _cache = new Dictionary<int,List>(); private Dictionary<int,int> _capacityDict = new Dictionary<int, int>(); public void SetAALevel(int aaLevel) { this._antiAliasing = aaLevel; } public void SetCullingLayer(int mask) { _photoCullingLayer = mask; _photoCullingMask = 1<<_photoCullingLayer; } public int GetCullingLayer() { return _photoCullingLayer; } public void SetRenderTextureDepth(int depth) { _rtDepth = depth; } public void SetRenderTextureFormat(RenderTextureFormat format) { _rtFormat = format; } public void SetCapacity(int width,int height,int capacity) { int key = (width << 16) + height; _capacityDict[key] = capacity; } public void SetPhotoMaterial(Material mat) { _photoMat = mat; } public Material GetPhotoMaterial() { return _photoMat; } public PhotoProducer Fetch(int width,int height) { if(width <= 0 || height <= 0) return null; int key = (width << 16) + height; PhotoProducer producer; if(_cache.ContainsKey(key)) { List producers = _cache[key]; if(producers.Count > 0) { producer = producers[producers.Count-1]; producer.targetContainer.SetActive(true); producers.RemoveAt(producers.Count-1); } else { producer = new PhotoProducer(width,height,_photoCullingMask,_rtDepth,_rtFormat,_antiAliasing); } } else { producer = new PhotoProducer(width,height,_photoCullingMask,_rtDepth,_rtFormat,_antiAliasing); } return producer; } public void Return(PhotoProducer producer) { if(producer == null) return; RenderTexture rt = producer.renderTexture; int key = (rt.width << 16) + rt.height; int capacity; _capacityDict.TryGetValue(key,out capacity); if(capacity > 0) { List producers; _cache.TryGetValue(key,out producers); if(producers == null) { producers = new List(); _cache[key] = producers; } if(producers.Count < capacity) { if (producer.producerContainer) { producer.producerContainer.SetActive(false); } producers.Add(producer); } else //剩余容量不够了,销毁掉 { producer.Dispose(); } } else //不需要缓存的 { producer.Dispose(); } } } }

RenderTexture特效系统实现原理

一、RenderTexture阐述

食物语中的显示的特效一般是使用RenderTexture结合摄像机拍摄3D物体实现的,实际上游戏中显示的是一张加载好的RT。

什么是RT?为啥RT可以渲染一张动态的图片?

RT是一种特殊的Texture,可以在运行时实现更新内容。网上有句话可以概括:将一个FBO链接到server-side的texture对象上。通俗一点就是将渲染结果应用到gpu上的texture对象上,而texture对象就是游戏中的一张贴图,渲染结果(FBO的数据)可以动态变化,那么贴图的内容也跟着动态变化。

FBO全称:FrameBufferObject,gpu上的一块buffer区域,用来存储渲染结果。一般有个默认的FBO直接连接显示器窗口区域,其他的FBO存储渲染结果供需要时使用,正是这时候,RT作为一块渲染区域应用这些FBO。

server-side:cpu加载贴图资源到内存,叫做client-side;cpu将资源从内存拷贝到gpu中,叫做发送到server-side。

为什么特效需要用RT应用到项目中,而不直接加载呢?

关键的原因是项目中一般在UI上挂特效,特效资源的渲染层跟UI不属于同一个渲染层级,所以总是在UI资源之前或之后,没办法在两个UI资源之间,让特效显示在最上层的话,那么就会挡住比如断线重连这类层级优先级最高的UI。

所以要利用RT渲染特效,作为一个UI资源加载进来。

二、RT特效系统设计原理

怎样实现RT特效?

很简单就是创建一个RenderTexture对象,赋值给camera的targetTextrue属性,同时赋值给RawImage对象的texture属性。这就相当于camera将渲染结果(特效的表现)写入到了RenderTexture对象,RawImage对象作为一个显示窗口区域,连接到RenderTexture对象存储数据的FBO。

着点于项目上,RT特效系统是如何设计的,这么设计的原因有是什么呢?先看以下系统关系图:

图中清晰的表达了类之间数据流向,关键的就是传递RT和特效实例,各类发挥的作用:

  1. PhotoProducer负责创建RT、摄像机和特效实例的容器TargetContainer,相当于一块特效的渲染场景,而且这个渲染场景跟UI不是同一个渲染层,另外负责把RT传递给PhotoBase的RawImage组件上。
  2. PhotoProducerCache作为对象池,管理PhotoProducer的分配和回收。
  3. PhotoBase就是作为承载RT的容器,显示在UI上。
  4. PhotoMultiEffect负责通过url加载特效资源,并将特效资源实例传递到Pruducer中的TargetContainer容器。

另外这些类通过turnOn和turnOff方法控制特效的显示和隐藏。

  • turnOn方法:将Effect Inst加到TargetContainer节点下,将RT跟Camera和RawImage连接起来。
  • TurnOff方法:将RawImage置为不可用enable为false,将Effect Inst移出TargetContainer,并且将ProducerContainer置为不可见,归还Producer到对象池以便供其他特效使用。

还有一点,PhotoProducerCache的主要优化手段是将RT缓存起来,用RT的规格(宽高)作为索引来复用RT。项目中规定了RT的几种规则:

1
2
3
4
5
6
7
8
9
10
11
12
--最大规格RT尺寸(不能再大了) 1560 * 720
PhotoUtil.MaxRTWidth = 1560
PhotoUtil.MaxRTHeight = 720
--大规格RT尺寸 1280 * 720
PhotoUtil.RTWidth = 1280
PhotoUtil.RTHeight = 720
--中等规格RT尺寸 512 * 288
PhotoUtil.PartRTWidth = 512
PhotoUtil.PartRTHeight = 288
--小规格RT尺寸 256 * 144
PhotoUtil.SmallRTWidth = 256
PhotoUtil.SmallRTHeight = 144

这样做应该是为限制RT规则来方便复用吧,而且面对各种规则的RT特效复用可能会出现特效裁边这种问题。

RT特效系统中还处理了一种情况:显示多个同一类特效。

当然RT肯定是同一张,但是需要创建多个PhotoMultiEffect的实例,进而实例化多个特效资源实例,这样就浪费资源了,创建多个特效实例可以省去,我们需要的是多个RT的承载体,也就是多个挂有RawImage组件的GameObject。

所以就有PhotoEffectFactory类,负责管理同一类特效的多处显示情况。