是真正意义上的AVG完整框架:
我发现这个框架的时候是第二个,第一个框架挺垃圾的,还花了一些时间
https://github.com/Lunatic-Works/Nova/wiki
曾经我师傅说过:
凡是都是2:8原则,当你一生有十个女朋友,那么第三个女朋友是最适合结婚当老婆的
不要怀疑,后面越选,越挑剔,反而越来越差
明显,这是非常科学,非常合理的
同理,我本来计划挑五个框架,所以第2~3个开源项目就是最好的
结果,第二个项目,就让我发现这个框架了
关键字,**AVG,框架,ToLua,**这些就不详细展开说了,直接说功能;
假设你对AVG有兴趣,想做点游戏,又刚好会点编程,然后又在找一些入门资料(这个框架对程序友好),这个框架就挺适合你
Show简单背景
Lua
Show(bg,'')
其中,bg是Lua的名字,也可以在Inspector(节点 Sprites/BackgroundImage , SpriteController.cs)修改对应,总而言之,言而总之,就是Lua那一套,也必须大小写区分
*一般不需要改bg,fg,且作为保留关键字吧
人物加载+动画
创建角色,需要严格执行下面步骤:
- 在Unity Editor的Hierarchy中的
Characters下找到示例角色的game object(如Ergong),把它复制一份 - 将game object的名字改成你的角色的名字(如
Xiaoming),这个名字不会用在脚本中,只是为了在Hierarchy中好看 - 在Inspector中的
GameCharacterControllercomponent中(刚才克隆的GameObject):- 将
luaGlobalName改成你的角色的名字(如xiaoming),脚本中使用的都是这个名字,注意大小写 - 这个名字必须符合Lua变量名的要求,只能包含字母、数字、下划线,并且不能以数字开头,不能包含中文、空格、其他符号等等
- 将
imageFolder和voiceFolder改成对应的文件夹
- 将
- Resources/目录,复制一份,(框架是期待有Body Face Normal等几个贴图的,我暂时用单一贴图测试)
所有这些步骤都在 Main.unity 唯一第一场景中管理
导入
- 把所有部件放到
Assets/StandingsUncropped/<角色名称>/文件夹下 - 可以适当剪裁,框架提供功能
我跳过,只要保证Resources/ 目录下有图片,就不会卡lua 代码(会报错,但是不会卡主流程)

按钮Command--二次开发

Lua
<|
show(hui1999,'normal')
--show(gaotian, 'normal')
add_button()
|>
@<| jump_to 'tut01_2' |>
@<| label 'tut01_2' |>

稍微解释一下;
Assets/Nova/Lua/目录下必有几个.lua脚本,例如alert.lua,框架启动时必然全部加载一遍;所以才能调用 alert() 弹出对话框,所以,我们只要添加一个自定义方法 add_button();必然可以全局调用,测试结果: log 正确;就可以实现各种扩展功能
至于怎么扩展,怎么能在文字中间插入按钮,希望框架能顺带提供API(其实已经是无限扩展,脚手架都已经搭好),C# + ToLua + Unity本身就无敌,即使Unity是闭源,并不开放
配音
??
一些游戏视觉小技巧
<纯口嗨,不详细提供例子和演示了>部分内容就在这个AVG框架内有提供API
- 改颜色Tint
- 基于Tint改变白天黑夜
- 滤镜
- 过场
- 2D~2.5D动画
如何清空原来已完成的剧情历史
???

有几个很坑的 lua 代码的点
假如复制一个对话文件(代码),非常容易犯错"指向错误"
又因为国人很喜欢简写英文+ 喜欢数学
所以经常会:
t000001
t000001_2
i0001_2
就非常容易傻傻分不清楚
t 和 00001根本没有任何含义,写了等于没写

其实复制了两份,aa.lua , bb.lua 原始文件, tut01.lua
所以最红 bb.lua 把 aa 和 tut01 的都覆盖了
对话结束和分支的代码逻辑
结束
Lua
--Script_loader.lua
--- set the current node as an end node
--- should be called at the end of the node
--- a name can be assigned to an end point, which can differ from the node name
--- the name should be unique among all end point names
--- if no name is given, the name of the current node will be used
--- all nodes without child nodes should be marked as end nodes
--- if is_end() is not called under those nodes, they will be marked as end nodes automatically
function is_end()
if not check_eager('is_end') then
return
end
__Nova.scriptLoader:SetCurrentAsEnd()
end
cs
//ScriptLoader.cs
public void SetCurrentAsEnd()
{
CheckCurrentNodeDefaultLocale();
currentNode.type = FlowChartNodeType.End;
flowChartGraph.AddEnd(currentNode);
currentNode = null;
}
//其实就很简单,粗暴,的 Set CurrentNode,和 FlowChart的节点处理
分支
- 设置剧情分支
branch(branches)branches是一个array,每个元素是一个table,它的key有dest, text, mode, conddest是一个字符串,表示目标节点的名称text是一个字符串,表示显示在按钮上的文本mode是一个字符串,可以为'normal'|'jump'|'show'|'enable''normal'表示普通选项,此时cond必须省略'jump'表示如果cond返回true,就会直接跳转到目标节点,否则不显示这个选项,此时text必须省略- 如果省略
cond,则一定会跳转 - 如果有多个选项是
'jump',则会按顺序测试每个cond,按照第一个返回true的来跳转
- 如果省略
'show'表示如果cond返回true,这个选项才会显示'enable'表示如果cond返回true,这个选项才能点击
cond是一个函数,没有参数,返回一个boolean- 如果
cond是字符串,就会把它parse成表达式,然后转换成返回这个表达式的函数 - 这个函数必须没有副作用
- 这个函数必须返回boolean,我们不会进行类型转换,因为Lua会把任何数值和字符串转换为true
- 如果
- 具体的例子可以参考
Assets/Resources/Scenarios/test_branch.txt
分支代码实现-按钮逻辑
Lua
--- add branches to the current node
--- should be called at the end of the node
--- should be called only once for each node, i.e., all branches of the node should be added at once
--- branches should be a list of 'branch'. A 'branch' is a table with the following structure:
--- {
--- dest = 'name of the destination node'
--- text = 'text on the button', should not use if mode is jump
--- image = {'image_name', {x, y, scale}}, should not use if mode is jump
--- mode = 'normal|jump|show|enable', optional, default is normal
--- cond = a function that returns a bool, should not use if mode is show, optional if mode is jump
--- }
--- if cond is a string, it will be converted to a function returning that expression
function branch(branches)
if not check_eager('branch') then
return
end
for i, branch in ipairs(branches) do
local name = tostring(i)
local dest = try_get_local_name(branch.dest)
local image_info = nil
if branch.image then
local image_name, image_coord = unpack(branch.image)
local pos_x, pos_y, scale = unpack(image_coord)
image_info = Nova.ChoiceImageInformation(image_name, pos_x, pos_y, scale)
end
local mode = Nova.BranchMode.Normal
if branch.mode == nil or branch.mode == 'normal' then
-- pass
elseif branch.mode == 'jump' then
mode = Nova.BranchMode.Jump
elseif branch.mode == 'show' then
mode = Nova.BranchMode.Show
elseif branch.mode == 'enable' then
mode = Nova.BranchMode.Enable
else
warn('Unknown branch mode: ' .. dump(branch.mode) .. ', text: ' .. dump(branch.text))
return
end
local cond = branch.cond
if type(cond) == 'string' then
local cond_str = cond
cond = loadstring('return ' .. cond)
if cond == nil then
warn('Failed to parse cond: ' .. cond_str)
end
end
if __Nova.scriptLoader.stateLocale == Nova.I18n.DefaultLocale then
__Nova.scriptLoader:RegisterBranch(name, dest, branch.text, image_info, mode, cond)
else
__Nova.scriptLoader:AddLocalizedBranch(name, dest, branch.text)
end
end
__Nova.scriptLoader:EndRegisterBranch()
end
cs
void RegisterBranches(..._)
{
currentNode.type = FlowChartNodeType.Branching;
lazyBindingLinks.Add(new LazyBindingEntry(currentNode, destination,
new BranchInformation(name, text, imageInfo, mode, condition)));
}
cs
//GameState.cs
private void StepAtEndOfNode(FlowChartNode node)
{
//.....
switch (node.type)
{
case FlowChartNodeType.Normal:
break;
case FlowChartNodeType.Branching:
ExecuteAction(DoBranch(node));
break;
case FlowChartNodeType.End:
break;
default:
throw new ArgumentOutOfRangeException();
}
}
cs
//GameState.cs -> DoBranch(...)
//....
AcquireActionPause();
RaiseChoices(choices);
while (coroutineHelper.fence == null)
{
yield return null;
}
ReleaseActionPause();
var index = (int)coroutineHelper.TakeFence();
SelectBranch(node, choiceNames[index]);
UI的处理和显示Buttons
最终,还是ChoiceController.cs的代码
cs
public void On_RaiseChoices(IReadOnlyList<ChoiceOccursData.Choice> choices)
{
if (choices.Count == 0)
{
throw new ArgumentException("Nova: No active selection.");
}
if (backPanel != null)
{
backPanel.SetActive(true);
}
for (var i = 0; i < choices.Count; i++)
{
var choice = choices[i];
var index = i;
var button = Instantiate(choiceButtonPrefab, transform);
// Prevent showing the button before init
button.gameObject.SetActive(false);
button.Init(choice.texts, choice.imageInfo, imageFolder, () => Select(index),
choice.interactable);
button.gameObject.SetActive(true);
}
buttons = GetComponentsInChildren<Button>();
activeChoiceCount = choices.Count;
}