Mapz's Blog

可以递归的函数指针

作为 GAS 系统中的核心元素之一 GameplayAbility 代表一个可大可小的 “能力”

这个能力能干啥,基本全靠你自己定义的边界,大到一个复杂的技能,小到一个动作,都可以作为一个“能力”

可以说 GA 的目的是基于一系列的行为控制来生成 GameplayEffect 改变 GameplayAttribue, 创建 GameplayCue 来处理显示和声音效果

这次我们基于 UE 版本 4.26 来简析一下 UGameplayAbility 类

基本信息

类型继承 UObject,并实现 IGameplayTaskOwnerInterface 接口

实现 IGameplayTaskOwnerInterface 的目的是为了可以在其中执行异步的 UGameplayTask

而 GA 使用的 GameplayTasksComponent 是 GA 的当前释放者的 AbilitySystemComponent (UAbilitySystemComponent 也是继承自 UGameplayTasksComponent 的)

重要的函数

如源码中的注释所写的,GA 里面有几个重要的函数

CanActivateAbility

函数定义

1
virtual bool CanActivateAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayTagContainer* SourceTags = nullptr, const FGameplayTagContainer* TargetTags = nullptr, OUT FGameplayTagContainer* OptionalRelevantTags = nullptr) const;

函数功能是检查是否可以激活此 Ability

GAS 中在 UAbilitySystemComponent::TryActivateAbility 中调用

  • Handle 参数:AbilitySpcHandle,这个代表一个 Ability 的实例和数据载体的 Handle
  • ActorInfo 参数:当前技能的释放者信息,类型 FGameplayAbilityActorInfo 包括技能所有者,技能 Avatar Actor (代表物理上释放技能的 Actor,可以没有)和 PC ,骨架 Mesh ,是否本地 Actor 等信息
  • SourceTags:表示释放者拥有的 Tags
  • TargetTags:表示目标拥有的 Tags
  • OptionalRelevantTags:用来存放一些额外的信息,本体里面用来放技能释放失败的原因 Tags

函数默认流程

函数是 virtual 的,你可以自己重写

默认流程如下

  1. 判断是否符合执行策略(只能在服务器执行,只能在客户端发起之类的)
  2. 判断 ActorInfo 中是否含有 AbilitySystemComponent ,没有则不可以释放
  3. 判断用户是否禁止释放技能(AbilitySystemComponent.UserAbilityActivationInhibited,例如菜单状态下不能释放之类的)
  4. 判断技能冷却是否完成

    冷却是由 TSubclassOf CooldownGameplayEffectClass 来控制的,在技能成功被 commit 的时候,会触发生成这个 GE,判断是否冷却了就是判断是否有这个 GE

  5. 判断技能消费是否够

    同样的消费是由 TSubclassOf CostGameplayEffectClass 来控制的,这个 GE 是懒加载的,这里会调用 AbilitySystemComponent->CanApplyAttributeModifiers 来检查是否有资源释放技能

  6. 检查当前 Tags 是否满足释放条件,包括需要的 Tag,Block 的 Tag 等
  7. 获得技能实例的技能信息(FGameplayAbilitySpec)
  8. 如果绑定了输入,检查是否为不允许输入的技能(AbilitySystemComponent->IsAbilityInputBlocked)
  9. 检查蓝图覆盖的 K2_CanActivateAbility 是否可以释放

CallActivateAbility

函数定义

1
void CallActivateAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo, FOnGameplayAbilityEnded::FDelegate* OnGameplayAbilityEndedDelegate = nullptr, const FGameplayEventData* TriggerEventData = nullptr);

函数用来激活 Ability

参数除了上面的以外还有

  • ActivationInfo:FGameplayAbilityActivationInfo 类型,存储一些技能的激活信息,包括 PK 和 ActivationMode (激活的模式, Authority 或者 Client 等)
  • OnGameplayAbilityEndedDelegate: Ability Ended 回调
  • TriggerEventData:FGameplayEventData 类型,包括技能发起者,目标,目标数据,增幅,Tag容器等,还可以传入两个 Custom 的 UObject 用来传递数据

函数主要流程

  1. 执行 PreActivate(Virtual 可覆盖)
    a. 刷新对齐 ServerMovement
    b. 如果是实例化的 Activity,设置 bIsActive 为激活状态,设置为 bIsBlockingOtherAbilities 为 True
    c. 设置 Activity 的当前 ActorInfo 和 ActivationInfo
    d. 加入需要激活的 GameplayTags
    e. 加入 AbilityEnded 回调
    f. 发送 AbilityActivatedCallbacks 事件
    g. 更新 Ability 的 Blocking 和 Cancel Tags
  2. 执行 ActivateAbility(Virtual 可覆盖)
    a. 如果有蓝图实现,则使用蓝图实现 K2_ActivateAbility 激活任务
    b. 如果有蓝图的 TriggerEvent 实现,则执行 K2_ActivateAbilityFromEvent(*TriggerEventData) 激活任务,灭有 TriggerData 则 End Ability
    c. 如果没有蓝图实现,则直接 Commit Ability

CommitAbility

函数定义

1
virtual bool CommitAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo, OUT FGameplayTagContainer* OptionalRelevantTags = nullptr);

函数是确认 Ability

默认流程

  1. 调用 CommitCheck 检查是否可以 Commit (和 CanActivateAbility 差不多,也可以做额外的检查)
  2. 设置冷却,执行消耗资源 (ApplyCooldown 和 ApplyCost)
  3. 执行蓝图 K2_CommitExecute
  4. 发送 AbilityCommittedCallbacks 广播

CancelAbility

函数定义

1
virtual void CancelAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo, bool bReplicateCancelAbility);

函数用来取消 Ability

默认流程

  1. 检查 Ability 是否可以被 Cancel (如果是非实例化的,则可以被 Cancel,如果是实例化的,检查是否可以被 Cancel 的设置)
  2. 检查当前的 Ability 是否正在 ApplyGameplayEffectSpecToTarget (ScopeLockCount),如果正在被操作,加入 WaitingToExecute 中,并会在执行完成后,执行本次 Cancel 操作
  3. 如果需要,执行 ReplicateEndOrCancelAbility 执行客户端或者服务器的 CancelAbility 或者 EndAbility 的 RPC
  4. 广播 OnGameplayAbilityCancelled 事件
  5. 执行 EndAbility

EndAbility

函数定义

1
virtual void EndAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo, bool bReplicateEndAbility, bool bWasCancelled);

函数用来结束 Ability

默认流程

  1. 检查 Ability 是否可以被 End
    a. 不是 Active 的实例化 Acitivity 不可以被 End
    b. 检查 Handle 的 ActivitySpec 是否 Active
  2. 查当前的 Ability 是否正在 ApplyGameplayEffectSpecToTarget (ScopeLockCount),如果正在被操作,加入 WaitingToExecute 中,并会在执行完成后,执行本次 EndAbility 操作
  3. 执行蓝图 K2_OnEndAbility
  4. 清除这个 Ability 在 World 中的所有 timer 和 latent actions
  5. 广播 OnGameplayAbilityEnded 事件,并移除回调
  6. 广播 OnGameplayAbilityEndedWithData 事件,并移除回调
  7. 对于实例化 Ability 设置 bIsActive 为非激活
  8. 结束所有挂在 Ability 上的 GameplayTask,并 Reset 列表来移除内存占用
  9. 如果需要执行 ReplicateEndOrCancelAbility
  10. 移除 ActivationTags
  11. 移除技能添加的 GameplayCue
  12. 移除 AbilityComponent 上的 Blocking 和 Cancel Tags
  13. 移除 PK 缓存
  14. 移除 AbilityComponent 上的 AnimatingAbility
  15. 播出 AbilityComponent 上的 AbilityEndedCallbacks , OnAbilityEnded 事件
  16. 需要同步的 Ability ,移除 Spec 的 ReplicatedInstance(服务器)
  17. 不需要同步的 Ability ,移除 Spec 的 NonReplicatedInstances
  18. 服务器上如果没有活动的 Spec 则,且设置为在 Activation 后就从 Component 中移除 Ability 则 ClearAbility,否则 MarkAbilitySpecDirty

这里的 ClearAbility,和 GiveAbility 对应,是从 AbilityComponent 中移除这个 Ability 的 Handle 而 MarkAbilitySpecDirty 是如字面意思表示 AbilitySpec 已经被修改过了

配置项

  • ReplicationPolicy:同步策略
  • InstancingPolicy:实例化策略,可以设置为非实例化,每个 Actor 实例化,或者每次执行实例化,非实例化的 Ability 只是用其 CDO 来做逻辑
  • bServerRespectsRemoteAbilityCancellation : 是否可以从客户端来 Cancel 服务器上的 Ability
  • bRetriggerInstancedAbility:是否在激活一个实例化的 Ability 的时候, 先 End 再重新激活
  • NetExecutionPolicy:网络同步策略,包括本地预测,服务器 only 等
  • NetSecurityPolicy:网络安全政策,设置 Ability 的执行权限
  • CostGameplayEffectClass:消费的 GE
  • AbilityTriggers:可以激活 Ability 的 Tags
  • CooldownGameplayEffectClass:冷却的 GE
  • CancelAbilitiesWithTag:执行的时候,会 Cancel 含有这些 Tag 的其他 Ability
  • BlockAbilitiesWithTag:执行的时候,会 Block 含有这些 Tag 的其他 Ability
  • ActivationOwnedTags:激活的时候附加到 Owner 的 Tags
  • ActivationRequiredTags:激活的时候需要的 Tags
  • ActivationBlockedTags:激活的时候,如果有这些 Tags 会阻止激活
  • SourceRequiredTags:Source Actor 有这些 Tags 的时候才能激活
  • SourceBlockedTags:Source Actor 有这些 Tags 的时候不能激活
  • TargetRequiredTags:Target Actor 有这些 Tags 的时候才能激活
  • TargetBlockedTags:Target Actor 有这些 Tags 的时候不能激活
  • bReplicateInputDirectly: 是否直接把输入事件传到服务器
  • AbilityTags:拥有的 Tags,会同其他的 Ability 的 CancelAbilitiesWithTag 和 CancelAbilitiesWithTag 协同作用

小结

  1. 非实例化的 Ability ,总是可以被 Cancel,也总是会 Block 其他的 Ability,而不需要设置
  2. 对于每个实例会不同的 Ability,需要设置为实例化,否则不要设置为实例化,以节省资源,Ability 所需的实例数据事实上存在 AbilitySpec 中

电脑上 C 盘空间太小,只有 100 G
其他盘的 SSD 也不够大,虚拟内存都快放不下了
打开 WinDirStat 一看 UE4 DDC 占了几十个 G 了
这就准备移动一下

  1. 打开 UE4安装目录\Engine\Config\BaseEngine.ini
  2. 找到 InstalledDerivedDataBackendGraph 下的
1
Local=(Type=FileSystem, ReadOnly=false, Clean=false, Flush=false, PurgeTransient=true, DeleteUnused=true, UnusedFileAge=34, FoldersToClean=-1, Path="%ENGINEVERSIONAGNOSTICUSERDIR%DerivedDataCache")

修改 Path 到自己的目录即可

如果没有成功,可以修改环境变量

UE-LocalDataCachePath

值修改为自己的目录即可

FPredictionKey 的基本信息

FPredictionKey 是 UE4 在 Gameplay Ability System 插件中提供的一个结构体

编写目的是为了在 GAS 系统中实现客户端预测从而不用等待服务器返回提前就提前做一些操作

达到提高游戏流畅性的目的

服务器返回通过或者拒绝后,客户端可以相应的 Confirm 或者在 Reject 后回滚此次操作所做的修改

除了在 GAS 本身中

你也可以在其他需要客户端预测的地方使用

例如动画的预测

但是需要依赖 GAS 中的 UAbilitySystemComponent

  • 一个 Predication Key 可以看做是一个行为在服务器和客户端的唯一标识
  • Predication Key 的 ID 是全局唯一的,参考:
1
2
3
4
5
6
7
8
9
10
11
void FPredictionKey::GenerateNewPredictionKey()
{
// GKey 作为静态局部变量,其值为全局唯一,每次都 ++
static KeyType GKey = 1;
Current = GKey++;
if (GKey < 0)
{
GKey = 1;
}
bIsStale = false;
}
  • 在服务器上创建的 PK 则只作为一个行为的标识,不作为客户端预测用

  • PK 可以通过 void FPredictionKey::GenerateDependentPredictionKey() 创建依赖上一个 PK 的 PK ,但是不能在服务器执行,因为没有意义(无需预测就无需确认和回滚)

  • PK 可以通过 bIsStale 设置为是否可以在一次确认或拒绝后继续使用

  • PK 可以通过 NewRejectedDelegate 和 NewCaughtUpDelegate 来创建确认或者拒绝的回调,即是服务器同意或者拒绝客户端预测的时候所调用的回调

  • PK 的网络序列化是自定义的

    • 第一位是 PK 是否在当前 Connection 可用,在序列化的时候设置,如果是服务器创建的,则都可用,否则只有 PK 的连接信息和当前连接一致的时候可用
      1
      ValidKeyForConnection = (PredictiveConnection == nullptr || (Map == PredictiveConnection) || bIsServerInitiated) && (Current > 0);
    • 第二位是是否有依赖的 PK (仅当当前 Connection 可用的时候可用)
    • 第三位是是否为服务器创建
    • 如果是可用连接,则写入当前的 PK ID
    • 如果有依赖的 PK 则写入依赖 PK ID
    • 如果是服务器创建的 PK 则设置当前 PK 的 Connection 为当前 Connection

从 Activate Ability 来看 PredicationKey 的使用

我们从 UAbilitySystemCompoment::TryActivateAbility 看起,这里仅讨论一种情况,就是客户端发起激活,服务器检查并确认的方式,略过其他方式的流程

  1. 先判断技能是否可以被释放(是否已经被 Give To Actor)
  2. 判断技能释放的 Actor 是否合法
  3. 模拟端不可以释放技能,return
  4. 检查技能是否是客户端远程释放的,并检查是否可以远程释放,如果可以释放则 创建新的 PK 并调用 CallServerTryActivateAbility 准备执行服务器 RPC ServerTryActivateAbility
  5. CallServerTryActivateAbility 中检查是否存在合批的 RPC 数据,如果合批的 RPC 数据未开始,则更新 PK 并开始,否则直接调用 服务器 RPC ServerTryActivateAbility ,并传入创建的 PK
  6. Server 调用 InternalServerTryActivateAbility
    a. Activate 失败的时候,调用客户端 RPC ,通知此 PK 调用失败,客户端调用此 PK 的拒绝委托,从而达到回滚等目的,并且执行 EndAbility
    b. Activate 成功的时候,调用客户端 RPC ClientActivateAbilitySucceed 通知客户端激活 Ability 成功
    c. 客户端和服务器在成功的时候,都会调用 CallActivateAbility 来启动 Activate Ability

这个里面,一个 PredicationKey 通过 RPC 在客户端和服务器之间传递并储存,并且通过 RPC 通知执行成功或者失败,在成功和失败的时候,执行通过 PK 对应起来的成功和失败的委托

Activate Ability 并没有直接用到回滚相关的内容,因为这里没有在客户端 Try Activate Ability 后做其他操作,而是等待服务器返回 Confirm 或者 Reject

我们在释放技能后,也可以会做过一些附加操作,这些操作可以在 Reject 委托中绑定自己的回滚操作

写其他一些功能的时候,也可以直接在客户端发送 RPC 后,直接开始处理自己的操作,等待服务器 Reject 后,用挂在 Reject 的委托上,用于回滚客户端操作,例如停止播放动画

有时候我们需要在 UE4 关闭游戏的时候,处理一些事情,那么可以重写 UGameInstance::ShutDown()

但是有时候在游戏关闭的时候,这个函数并未调用,于是我有必要探究一番他的调用链

这里仅讨论 Engine 而不讨论 EditorEngine

调用栈

  • UGameInstance::ShutDown()
  • UGameEngine::PreExit()
  • FEngineLoop::Exit()
  • 调用 EngineExit 或者平台调用退出
  • EngineExit 被放在了 GuardedMain 函数中定义的一个结构体中
1
2
3
4
5
6
7
8
   // make sure GEngineLoop::Exit() is always called.
struct EngineLoopCleanupGuard
{
~EngineLoopCleanupGuard()
{
EngineExit();
}
} CleanupGuard;

也就是说游戏 Main 函数结束执行的时候,一定会执行 ShutDown

那么为什么我测试的时候日志中没有出现 ShutDown 相关内容呢

检查发现我们调用关闭游戏的时候用的是

1
FPlatformMisc::RequestExit(true);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
void FWindowsPlatformMisc::RequestExit( bool Force )
{
UE_LOG(LogWindows, Log, TEXT("FPlatformMisc::RequestExit(%i)"), Force );

RequestEngineExit(TEXT("Win RequestExit"));
FCoreDelegates::ApplicationWillTerminateDelegate.Broadcast();

if( Force )
{
// Force immediate exit. In case of an error set the exit code to 3.
// Dangerous because config code isn't flushed, global destructors aren't called, etc.
// Suppress abort message and MS reports.
//_set_abort_behavior( 0, _WRITE_ABORT_MSG | _CALL_REPORTFAULT );
//abort();

// Make sure the log is flushed.
if( GLog )
{
// This may be called from other thread, so set this thread as the master.
GLog->SetCurrentThreadAsMasterThread();
GLog->TearDown();
}

TerminateProcess(GetCurrentProcess(), GIsCriticalError ? 3 : 0);
}
else
{
// Tell the platform specific code we want to exit cleanly from the main loop.
PostQuitMessage( 0 );
}
}

可见传入 true 为参数的时候,直接把进程杀了,导致未执行 UGameInstance::ShutDown()

前言

现在开始正式制作我们的FC重制游戏 SuperSimon

创建环境

  1. Unity 2018.3
  2. 使用 Package Manager 下载 2D Pixel Perfect 插件(需要设置 Package Manager 的高级设置,打开显示预览包
  3. 因为本次使用新的输入包,使用 Package Manager 下载 Input System
  4. 使用 这里 的文章中的方式,从恶魔城3的SpriteSheet中创建好字体

场景中的内容

  1. 摄像机设置正交,背景色 solid color,大小128
  2. 摄像机GO上添加一个 Pixel Perfect Camera,设置pixel perUnit 1,分辨率256 x 240,下面钩子全打上
  3. 创建UICanvas,用摄像机 Render,Pixel perfect 勾上,Canvas Scaler 分辨率设置好,模式选择随屏幕大小缩放。 pixel per unit 1。Match mode expand。

做像素游戏的时候,你通常会自定义字体,或者绘制像素字体。

字体画好,或者像我们一样直接从 FC 游戏里面抠出图集来之后,怎么样来做成BitmapFont呢?

网上的内容通常教你怎么从安装好的ttf来导出,现在我们手上只有 PNG 怎么办呢?

用到的工具

  • BMFont
  • Unity

假设

  1. 我们的字符图集中文字大小都是 8 X 8,并且无间隔(也可以是其他大小,总之Unity可以自动按格子大小来切就行)

步骤

  1. 我们从FC Dump 下来的字体图集,一般都是8x8的字符集,为了能够使用BMFont来导出Bitmap Font,我们需要先把这个图集打成单个的字符。
  2. 打开 Unity,导入字符集图片,导入设置为 压缩->无,并把可读写打开,Sprite类型为多个
  3. 打开 Sprite Editor ,切片方式 By Cell Size 大小 8 x 8,切好图集
  4. 使用如下脚本导出为单个的图片
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
using UnityEditor;
using UnityEngine;
static class BitmapFontTool {
[MenuItem ("BitmapFontTool/导出切好的Sprite")]
static void SaveSprite () {
//每一张贴图类型Advanced下 Read/Write Enabled打上勾才能进行文件读取
//导入类型不能是压缩过的
string bitMapfontPath = @"Assets/BitmapFontTool/";
foreach (Object obj in Selection.objects) {

string selectionPath = AssetDatabase.GetAssetPath (obj);
string selectionExt = System.IO.Path.GetExtension (selectionPath);
string fileName = System.IO.Path.GetFileNameWithoutExtension (selectionPath);
string loadPath = selectionPath.Substring (0, selectionPath.Length - selectionExt.Length);
if (selectionExt.Length == 0) continue;

//加载此文件下的所有资源
Object[] spriteList = AssetDatabase.LoadAllAssetsAtPath (selectionPath);

if (spriteList.Length > 0) {
//创建导出文件夹
string outPath = bitMapfontPath + "/SpriteExportOutput/" + fileName;
System.IO.Directory.CreateDirectory (outPath);

foreach (var sprite in spriteList) {
try {
var sprite_ = (Sprite) sprite;
Texture2D tex = new Texture2D ((int) sprite_.rect.width, (int) sprite_.rect.height, sprite_.texture.format, false);
tex.SetPixels (sprite_.texture.GetPixels ((int) sprite_.rect.xMin, (int) sprite_.rect.yMin, (int) sprite_.rect.width, (int) sprite_.rect.height));
tex.Apply ();
//写出成png文件
System.IO.File.WriteAllBytes (outPath + "/" + sprite_.name + ".png", tex.EncodeToPNG ());
Debug.Log ("SaveSprite to" + outPath);
} catch (System.Exception e) {
Debug.Log (e);
}

}
Debug.Log ("保存图片完毕!" + outPath);
}
}
// }
}
}
  1. 网上搜索 BMFont 教程,按教程使用导出的图片,创建 fnt 字体
  2. 把输出的文件导入 Unity ,下载 BitmapFontImporter 插件
  3. Assets -> Bitmap Font -> build 创建 Unity 自定义字体
  4. 使用字体

缺点

  1. BMFont 导入大量图片文件的时候非常的难用,不适合批量使用
  2. 既然已经有图集了,何不在图集上自己创建fnt文件呢?

思考

在Unity下,自己写个工具来制作fnt也并不难
挖个坑留着以后来尝试

之前做了 FC 荒野大镖客的 retro demo

总结了一些问题,现在准备先放在一边,不想改了

根据总结的这些问题,开个新坑吧

准备弄成一个 FC 大杂烩游戏

内容

恶魔城西蒙的大冒险

素材为各个 FC 游戏内容

横版 Platform
根据可能 也会变成 横版清关

分辨率

还原 FC 的 256 X 240 分辨率

预计用到的内容和改进

  1. 之前的 Demo 没有经过设计,想到哪写到哪,太潦草,所以这次先设计好再出发
  2. 这次使用 CSV 的方式来配置单位属性,并初始化
  3. Pixel Perfect 摄像机
  4. 先弄成单机打包模式,后期再弄成可热更新模式
  5. 先期接入 Lua
  6. 使用 Unity 的 Preview 新输入方式
  7. AI 使用 行为树
  8. 使用一些 Shader 来制作效果
  9. 生怪器的进一步优化
  10. 做一个从 PNG 导出 Bitmap Font 的工具

接着上次我们继续来玩 Tiny Mode

创建一个新项目

菜单 -> tiny -> file -> new project

创建新的项目 TinyNew

我们可以看到,在 Asset 目录下创建了一个新的名为 TinyNew 的目录

下面有一个 TinyNew.utproject 文件

这个文件就是 tiny mode 的项目文件了

和 Unity 不同,tiny mode 不存在 Scene

双击项目文件,就可以加载项目了

项目的 Inspector 如图

可以选择 Build 类型,需要加载的模块

还有显示设置以及物理设置

从可加载的模块看我们现在基本也可以知道 Tiny Mode 现有的功能有哪些

物理选项只有一个重力,可以看出相比 Unity 本体非常的精简

我们要做的是平面上球触边弹的效果,所以重力设置为 0


开始创建主实体

以前我们的各种玩意,都是放在 GameObject 上的,在 ECS 中,GameObject 没有了,用来放内容的变成了 Entity,所以还是老一套,需要一个 Entity 来放置摄像机什么的

创建一个新的 Entity 现阶段只能从 Tiny 菜单 Create EntityGroup 或者层级图中,点击 Create EntityGroup

我们在 Entities 目录下新建一个 MainGroup,点击后

在 EntityGroup 的 Inspector 中可以看到两个按钮

  • Load: 加载这个组的一个 Entity 实例到层级图中,方便操作
  • Set as StartUp: 设置为初始 Group,游戏启动时,会自动加载,不能被 Unload

我们把 MainGroup 设置为启动加载

在 MainGroup 右边的小菜单里面选择 Camera 新增一个 Camera

再 Create Empty,新建一个空组件,命名为 Spawner,用来产生球球

再 Create Empty,新建一个空组件,命名为 Border,作为边界

导入一个矩形 Sprite, 这里导入 Sprite 的方式和 Unity 本体是一样的,就不多说明了

在 Border 下面 Create 2D Object -> Sprite

做4个边

给4个边加上 RigidBody2D 和 BoxCollider2D 来做物理效果

刚体和碰撞检测的选项都比较简单

说一下刚体的几个 BodyType

  • Static:静态,不做物理模拟,质量无限,不会同其他 static 和 kinematic 做碰撞检测
  • Kinematic:运动学,质量无限,运动靠 velocity,质量无限,所以不受力的作用,不会同其他 static 和 kinematic 做碰撞检测,移动靠设置他的 Position 或 velocity
  • Dynamic:动态,全物理模拟,和其他三种类型的刚体做碰撞检测,一般靠给其施加一个力来运动,当然也可以直接设置 velocity 和 Position 但是意义不大
  • BulletDynamic:子弹动态,和动态差不多,但是使用的连续碰撞检测,更精确,也更消耗资源,好处是不会因为速度过快而穿过其他刚体

所以我们的边界选择什么很明显了-> Static

其他几个参数:

  • freezeRotation:如果 true 则不能旋转
  • friction:摩擦力,0 代表无摩擦,1 代表大摩擦,1 以上代表比 1 大的摩擦(废话)
  • restitution:弹性系数,0 代表完全吸收,1 代表完全反弹
  • density:密度,决定刚体的 质量 = 密度 * 体积

这里我们选择全反弹 restitution 设置为 1

自定义我们的产生器 spawner

我们自定义一个组件来产生小球

在 菜单 -> Tiny -> Component

新建一个 Spawner 组件

点击组件,在 inspector 中可以对其字段来做修改

如果 hide_flags 设置为 hide in inspectors 则在层级图中即使添加到实体上了也不会显示出来

add new field 可以添加任意可玩的数值和属性类型

点击属性名称可以修改之,下面可以设置默认值

最后设置如下

我们在延时 1.5s 后每 1.5s 创建一个 spawnedGroup 的实体 最多 maxCount 个

类型右边的 3 个图标分别是

  • 是否数组类型
  • 编辑器是否可见:比如我们的 curCount 这个字段编辑器不可见,因为是不用编辑也不能编辑
  • 子菜单 字面上的意思,Document 选项应该和自动生成文档的内容有关

生成器组件有了,我们继续来搞生成系统

菜单,创建 TypeScript System (后面省略哪里的菜单了哦)

1
2
3
4
5
6
7
8
9
10
11
12

namespace game {

/** New System */
export class NewSystem extends ut.ComponentSystem {

OnUpdate():void {

}
}
}

我们可以看到生成了一个继承 ut.ComponentSystem 的类,重写一个 OnUpdate 函数

是不是和 Unity ECS 的方式一毛一样

ut 是 unity tiny 的简称

文件名修改为 SpawnSystem

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

namespace game {

export class SpawnSystem extends ut.ComponentSystem {

OnUpdate(): void {
this.world.forEach([game.Spawner], (spawner) => {
if (spawner.isPaused)
return;

let time = spawner.time;
let delay = spawner.delay;

time -= ut.Time.deltaTime();

if (time <= 0) {
time += delay;
console.log("spawn");
if (spawner.curCount < spawner.maxCount) {
ut.EntityGroup.instantiate(this.world, spawner.spawnedGroup);
spawner.curCount++
}

}

spawner.time = time;
});
}
}
}

代码如上,循环 World 中的每一个 Spawner 来操作

至此,生成器组件完成,我们在 MainGroup 的 Spawn 元素的 insepector 中点击 Add Tiny Component 增加一个 Spawner 组件

于是就可以在编辑器中设置这个自定组件的值了

spawnedGroup 设置为 game.BallGroup

maxCount 设置为 10


创建球球实体

创建新的实体组 名为 BallGroup

点击 Load 按钮

放到层级图中

加入一个 2d sprite UnitBall 如图

新建一个新的空组件 BallFlag 用来代表实体是球球

再增加 CircleCollider2D RigidBody2D 组件到球球

调整好圆形检测的半径

做好之后 Unload BallGroup,后面我们都通过 Spawner 来生成球球

球球的行为

我们需要一个脚本来控制球球生成的地方,并且给一个力让他跑起来

菜单 创建 TypeScript Behavior

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
namespace game {

export class NewBehaviourFilter extends ut.EntityFilter {
node: ut.Core2D.TransformNode;
position?: ut.Core2D.TransformLocalPosition;
rotation?: ut.Core2D.TransformLocalRotation;
scale?: ut.Core2D.TransformLocalScale;
}

export class NewBehaviour extends ut.ComponentBehaviour {

data: NewBehaviourFilter;

// ComponentBehaviour lifecycle events
// uncomment any method you need

// this method is called for each entity matching the NewBehaviourFilter signature, once when enabled
//OnEntityEnable():void { }

// this method is called for each entity matching the NewBehaviourFilter signature, every frame it's enabled
//OnEntityUpdate():void { }

// this method is called for each entity matching the NewBehaviourFilter signature, once when disabled
//OnEntityDisable():void { }

}
}

NewBehaviourFilter 相当于是 Unity ECS 中的 GetEntityGroup 中的 struct

将会获得有这个过滤器下字段的所有 Entity

所以我们需要给 UnitBall 增加一个 空的 BallFlag 来确认他是球球

  • OnEntityEnable. Called only once, the first frame this entity is matched by this behaviour. 获得此Entity的第一帧执行
  • OnEntityUpdate. Called every frame on matching entities.
  • OnEntityDisable. Called only once, the first frame this entity is marked as disabled by this behaviour. 设置为 disable 的第一帧执行

球球行为代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
namespace game {
export class BallBehaviorFilter extends ut.EntityFilter {

position: ut.Core2D.TransformLocalPosition;
entity: ut.Entity;
ball: game.BallFlag;

}

export class BallBehavior extends ut.ComponentBehaviour {

data: BallBehaviorFilter;

OnEntityEnable(): void {
console.log("ball Enabled");

// 随机球球出现的位置
let randomX = getRandom(-100, 100)
let randomY = getRandom(-100, 100)
this.data.position.position = new Vector3(randomX, randomY, 0);

// 随机给球球一个力,动起来
// 我们可以看到,这里用了 this.world.addComponentData 给球球添加了一个加力组件,这是一个特殊的组件
// 这里的 AddImpulse2D 组件会在这一帧之后自动消失掉
let impulse = new ut.Physics2D.AddImpulse2D;
let newImpulse = new Vector2(getRandom(-1, 1), getRandom(-1, 1)).normalize();
impulse.impulse = new Vector2(newImpulse.x * 100, newImpulse.y * 100);
this.world.addComponentData(this.data.entity, impulse);

console.log("ball created:" + this.data.position.position);

}


OnEntityUpdate(): void {

}
}

function getRandom(min, max) {
return Math.random() * (max - min + 1) + min;
}
}


运行

到此我们的工作告一段落,unity 执行后

会在我们的默认浏览器打开一个窗口

在浏览器中使用开发者模式可以看到打印

也可以扫描二维码在移动设备上查看效果

运行效果如图

Project Tiny 是什么?

2018年12月,Unity在2018.3beta版本上开放了一个新包的预览下载,叫做ProjectTiny

并发布了一个基于此来制作的网页游戏

原文Blog传送门

Tiny Arms Revenge 游戏体验地址(需科学上网)

Project Tiny 的目的,是为更多即开即玩的游戏和互动广告提供支持

现在可以制作2D类型的Web游戏

体验游戏大小仅仅只有 1.8 MB,启动只需下载 969 KB


Project Tiny 现在是什么情况,以后会怎么样?

现在正在预览版本,只支持2D游戏,只支持TS语言,将来会支持C#,并移除TS支持,并且不断完善工具链和特性,并支持3D和AR体验


Project Tiny 的特色和亮点?

  • 纯的ECS模式,或许其编辑器可以看做未来Unity的ECS正式版编辑器的一个预览
  • ECS模式带来的效率提高
  • 快速制作游戏和互动广告

我的感觉

Project Tiny 顾名思义就是一个精干版本的 Unity 啦,用来制作 web 内容,并且其 ECS 模式很可能就是以后的 Unity ECS 模式的预览,学习 Project Tiny 对于我们进一步熟悉 Unity ECS 和提前熟悉新的编辑器都有好处


如何开始制作我们的第一个项目?

  1. 下载 Unity 2018.3 Beta
  2. 打开 Windows -> package manager
  3. 点击 Advanced,勾选 show preview packages
  4. 下载 Tiny Mode 包
  5. 菜单新增项目 Tiny,表示一切 OK
  6. Tiny -> File -> NewProject 新建项目

例子项目在哪里?

菜单 Tiny -> Import Samples 会加载例子到项目中