本博文目的在于帮助对Killing Floor 1&2 插件制作感兴趣的玩家/程序员快速的了解插件的运作机制和编写方法。
这里只介绍一代版本,二代版本的编写只会比一代更简单,原理相通。博主虚幻零基础,琢磨插件用了小半个月,踩了不少坑(特别是换了好几个编辑器),目前已经提交了两个插件,特此来这里记录和分享一下,顺便试试博客的Markdown~~。
只是入坑!因为博主也刚入坑不久
学习插件编写的好处:
1.认识更多的人
2.装饰你的Steam个人资料
3.对虚幻引擎(旧版本)有一个初步的认识
4.从根本上了解和改变你手里的游戏
5.在玩家面前装逼
正式开始
一.准备工作:
1.一个好用的代码编辑器
一个好用的编辑器非常重要!习惯了C#+VS的我根本无法忍受没有代码补全、Goto、查找引用、语法检查的编辑器。在最初的时候我尝试了很多款,Sublime,NotePad++,nFringe,UnrealScriptIDE等等,最后选择了VSCode。下载完了记得安装一个UnrealScript语法拓展。虽然它对UT2004版本的UnrealScript支持的还不是很好,但是部分功能还是可以用的。
2.能够适应UnrealScript脚本语法
UnrealScript这个语言基本上已经死了,最新版本的UE4更希望开发者使用C++,而且UT2004的UnrealScript所提供的库也很少。但是语言只是工具,暂时学习一下也不会很困难,多看别人的源码就好了。
3.良好的英语基础 or 活用翻译软件
国内的文档很少!如果要查一些教程或者介绍,你需要去T社官网的插件讨论区,Legacy Unreal Wiki,T社官方教程区和UT2004UDN文档翻找,或者看开发者在源码中留下的文字,甚至去讨论区或者在steam向国外的插件大佬讨教。抵触英文的话会让你的学习之路变得非常痛苦。
4.基本了解面向对象编程
这个没啥好解释的,游戏就是用OOP写的,不懂肯定很难受。要是不懂也没事,下功夫多看看,大家都会,没什么难的。
二.创建你的第一个插件:
下载Killing Floor SDK 位置:Steam -> 库 -> 工具
T社官方插件最简教程
在killing floor目录下创建目录和文件ExampleMutator/Classes/ExampleMutator.uc
如果你按照官方的文档来做的话,得到的代码应该是这样
class ExampleMutator extends Mutator;
function PostBeginPlay()
{
SetTimer(1, true);
}
function Timer()
{
local KFHumanPawn Player;
foreach DynamicActors(class 'KFHumanPawn', Player)
{
if (Player.Health + 2 <= Player.HealthMax) Player.Health += 2;
else Player.Health = Player.HealthMax;
}
}
defaultproperties
{
GroupName="KFExampleMutator"
FriendlyName="Example Mutator"
Description="Mutator description here"
}
1.最开始我们可以看到,这个插件继承了Mutator
父类。
2.PostBeginPlay()
方法,类似于Unity里的Start()
方法,意思是在你的mut对象实例化之后执行,是继承的Actor
的方法,在该方法内调用了SetTimer()
方法,含义是创建一个计时器,调用Timer()
方法,传入的参数具体含义去查询Legacy UnrealWiki。其实就是运行间隔和是否循环,如果要取消计时器就SetTimer(0,false)
。
类似的还有PreBeginPlay()
,这些都是方便Actor
子类初始化一些内部数据用的。毕竟工厂模式不可能把构造函数留给你写,都是Spawn()
封装的。
3.注意力回到Timer()
,能看到里面定义了KFHumanPawn
的对象,然后用DynamicActors()
迭代器查询所有KFHumanPawn
子类的对象,返回值给Player
,并修改其Health
属性。
4.下面的defaultproperties
是存储插件的一些需要持久化的数据用的,官方标注的这三个缺一不可,否则无法在KF1插件面板上显示。如果你定义了config var bool bDisplayMessage
之类的东西,最好也在最下面里写上一个默认值.
好,这么官方给的最简单的一个插件就分析完了,但是还远远不够,我们需要让他变成自己想要的样子。比如你想再添加护甲,但是你不知道该修改什么变量,或者代码写在什么地方,那么下一步,就是编写插件的过程中最重要的部分:阅读源码。
三.阅读SDK源码
以前写Unity代码的时候我最喜欢干的事情就是分析代码的框架,理清继承关系和消息机制处理流程。但是因为VSCode要在UnrealScript环境下查找变量方法的所有引用是很困难的事情,因此理清这些事情会变得比较麻烦(特别是零虚幻基础,像博主一样)。那我们就要多翻源码了,这是没办法的事情。
什么是KFHumanPawn?什么是Mutator?要搞清楚这些问题,最简单的办法就是从SDK里找到定义他们的代码,以及他们的父类。
KFHumanPawn -> KFPawn -> xPawn -> UnrealPawn -> Pawn -> Actor -> object
Mutator -> Info -> Actor -> object
可以看出Mutator和KFHumanPawn都是Actor的子类,这也是虚幻程序员的共识。
那他们自己身上的属性方法和父类的属性方法将会是插件修改的突破口。我们看下Mutator都有哪些方法可以重写——T社官方Mutator类介绍。
function PostBeginPlay(){}
function Tick(float delta){}
function PostBeginPlay(){}
function Timer(){}
function ModifyPlayer(Pawn Other){}
function Mutate(string MutateString, PlayerController Sender){}
function bool CheckReplacement(Actor Other, out byte bSuperRelevant){}
static function FillPlayInfo(PlayInfo PlayInfo){}
static function string GetDescriptionText(string SettingName){}
function ModifyLogin(out string Portal, out string Options){}
function NotifyLogout(Controller Exiting){}
......
我翻译一下官方教程里提到的部分
- PostBeginPlay
标准actor方法,在mutator实例化之后调用。可以用来初始化一些变量,创建必要的actor和设置一些初始计时器等。不过这个时候不能确保已经有玩家存在,所以可能调用不到player。
function PostBeginPlay()
{
// Handle initialization here
}
- Tick
又一个标准actor方法,会在游戏循环的每一次调用。比如游戏60fps时就是一秒调用60次,经常被其他actor们用来执行每帧的操作,更新变量什么的。参数delta
表示两帧之间的间隔时间(类似unity中的Time.deltaTime)。
function Tick(float delta)
{
// Handle updating of mutator logic here
}
- Timer
又又一个重要的actor方法,这个方法会被代码定期调用,也可以不断地定期调用。
function PostBeginPlay()
{
SetTimer(0.5, false); // Call timer in half a second, do not repeat (=false)
}
function Timer()
{
// Handle timer-related stuff here
}
- ModifyPlayer
当玩家进入游戏时调用,这个方法允许你在玩家实例化之前修改他的一些属性。为了兼容其他插件,你最好再方法最后调用父类方法,这样可以让其他插件也调用这个方法。
function ModifyPlayer(Pawn Other)
{
Other.GiveWeapon("KFMOD.SCARMK17AssaultRifle");
Super.ModifyPlayer(Other);
}
- Mutate
测试和调用神器,这个方法可以在玩家在控制台里写mutate <参数string>
时调用,在多人联机时,不管这个插件是不是在客户端机器上运行,都会调用。注意和上面的方法类似的是,你都要调用父类方法来确保其他插件也能调用。
function Mutate(string MutateString, PlayerController Sender)
{
if (Caps(MutateString) == "HYPER")
Sender.Pawn.Health = 999;
Super.Mutate(MutateString, Sender);
}
- CheckReplacement
只要actor实例化,就会调用这个方法(当且仅当actor的bGameRelevant属性为false,这个方法不会在client-side actor中调用,比如本地的特效和效果)。允许你修改或者替换一个actor,很有用因为你可以在actor实例化时对其修改,且改完不需再关注。除非你是个高手,否则最好永远返回父类方法。
function bool CheckReplacement(Actor Other, out byte bSuperRelevant)
{
if (KFMonster(Other) != None)
{
KFMonster(Other).Health = 1;
}
return Super.CheckReplacement(Other, bSuperRelevant);
}
- FillPlayInfo
如果当你声明全局变量时使用了config
标识符,你就可以在插件参数定义面板上修改他们了。当然也需要一些方法来告诉游戏这些参数的存在,并贴上一些参数描述。
FillPlayerInfo会在当游戏想要获取参数设置列表时调用,你必须在写代码前调用弗雷方法,因为这样才会允许你添加参数的设定。
static function FillPlayInfo(PlayInfo PlayInfo)
{
Super.FillPlayInfo(PlayInfo);
PlayInfo.AddSetting(default.RulesGroup, "variableName", "settingDescription", 0, 0, "settingType");
}
上述如你所见,要添加一个参数设定,你必须调用PlayInfo.AddSetting()
,并将描述设置的参数们传进去。variableName
属性必须匹配全局变量的名字。settingDescription
属性允许你提供一个输入框的信息简述。settingType
属性描述是哪种类型的设置,最好是text(string或数字)、check(bool)或者select(下拉菜单)。
当使用string
类型时你可以用在settingType
之后的一个另外的参数来定义string长度和/或值的范围,比如
PlayInfo.AddSetting(default.RulesGroup, "bEnabled", "Enabled?", 0, 0, "check");
PlayInfo.AddSetting(default.RulesGroup, "HealthMax", "Maximum Health", 0, 0, "text");
PlayInfo.AddSetting(default.RulesGroup, "HealthMax", "Maximum Health", 0, 0, "text", "3;100:200"); // 最长3个字符(3位),数值在100-200之间
当使用select
类型时你必须在settingType属性后面再提供一个参数,格式为"Value1;Text1;Value2;Text2",比如
PlayInfo.AddSetting(default.RulesGroup, "StartWep", "Starting weapon", 0, 0, "select", "0;Deagle;1;Bullpup;2;LAR"); // 展示一个武器和相关数据的列表
- GetDescriptionText
这个方法用来获取一些特定参数的描述(当你鼠标放到设置上的时候)。你必须调用返回父类方法,这样才能让其他设置也能返回他们相应的描述。
static function string GetDescriptionText(string SettingName)
{
switch (SettingName)
{
case "ExampleVar":
return "This is the 'long' description for the setting.";
}
return Super.GetDescriptionText(SettingName);
}
非常多,论坛里只给介绍了一小部分,更多的你需要结合LegacyWiki和SDK源码中的注释来了解每个方法都是做什么用的。同样我们也可以如法炮制阅读KFHumanPawn
的代码。
但是却发现KFHumanPawn
里并没有Health
属性,因为这个属性是继承自Pawn
类,我们需要到Pawn
的代码里阅读,自然就找到了。接下来找护甲,因为我们不知道护甲具体的变量名称,那么就需要猜,假设为"Armor",从KFHumanPawn
一直搜到Pawn
,都没有找到该变量,但是在Pawn
里能看到
if( DamageType.default.bArmorStops && (actualDamage > 0) )
actualDamage = ShieldAbsorb(actualDamage);
......
function int ShieldAbsorb( int damage )
{
return damage;
}
所以我们猜测,ShieldAbsorb()
方法应该跟护甲的伤害减免换算有关,那么下一步顺着该方法名搜索,按下ShiftCtrlR,全局查找ShieldAbsorb()
方法。
在KFPawn
中找到如下方法:
function int ShieldAbsorb(int dam)
{
local float Interval, damage, Remaining;
//local int PainSound;
damage = dam;
if ( ShieldStrength == 0 )
return damage;
if ( KFPlayerReplicationInfo(PlayerReplicationInfo) != none && KFPlayerReplicationInfo(PlayerReplicationInfo).ClientVeteranSkill != none )
{
damage *= KFPlayerReplicationInfo(PlayerReplicationInfo).ClientVeteranSkill.static.GetBodyArmorDamageModifier(KFPlayerReplicationInfo(PlayerReplicationInfo));
}
// Super.ShieldAbsorb(dam);
//SetOverlayMaterial( ShieldHitMat, ShieldHitMatTime, false );
// Randomize Painsounds on Armor hit
// PainSound = rand(6);
// if (PainSound == 0)
// PlaySound(sound'KFPlayerSound.hpain3', SLOT_Pain,2*TransientSoundVolume,,400);
// else if (PainSound == 1)
// PlaySound(sound'KFPlayerSound.hpain2', SLOT_Pain,2*TransientSoundVolume,,400);
// else if (PainSound == 2)
// PlaySound(sound'KFPlayerSound.hpain1', SLOT_Pain,2*TransientSoundVolume,,400);
// else if (PainSound == 3)
// PlaySound(sound'KFPlayerSound.hpain3', SLOT_Pain,2*TransientSoundVolume,,400);
// else if (PainSound == 4)
// PlaySound(sound'KFPlayerSound.hpain2', SLOT_Pain,2*TransientSoundVolume,,400);
// else if (PainSound == 5)
// PlaySound(sound'KFPlayerSound.hpain1', SLOT_Pain,2*TransientSoundVolume,,400);
if ( ShieldStrength > SmallShieldStrength )
{
Interval = ShieldStrength - SmallShieldStrength;
if ( Interval >= 0.75 * damage )
{
ShieldStrength -= 0.75 * damage;
if ( ShieldStrength < SmallShieldStrength )
SmallShieldStrength = ShieldStrength;
return (0.25 * Damage);
}
else
{
ShieldStrength = SmallShieldStrength;
damage -= Interval;
Remaining = 0.33 * Interval;
if ( Remaining <= damage )
return damage;
damage -= Remaining;
}
}
if ( ShieldStrength >= 0.5 * damage )
{
ShieldStrength -= damage ;
SmallShieldStrength = ShieldStrength;
return Remaining + (0.25 * damage); // 0.5
}
else
{
damage -= ShieldStrength;
ShieldStrength = 0;
SmallShieldStrength = 0;
}
return damage + Remaining;
}
可以看到他是根据ShieldStrength
来计算伤害减免的,而ShieldStrength
是在Pawn
类中定义的,那就找到护甲了。因此在你的第一行代码中,可以修改Player.ShieldStrength
,自己尝试找一找最大护甲值在哪里定义。
ShieldStrengthMax在xPawn中定义。
更多关于虚幻引擎常用类的介绍
常用类的说明:
· Object:Object是虚幻引擎的其他类的基类。它不能被加入到世界(无法在关卡中生成或放置),但它带有数据和函数。Object是一个不需要放入关卡的类,Draw Call在Object不会发生。目前,不幸的是(4.7版本)不能在蓝图使用Object。对于这样的行为,你应该用我将描述在后面描述Actor组件。
· Actor: Actor可以在关卡中生成。它可以通过像静态网格组件这种方式通过图像来呈现。如果你想获得Actor,你需要将它放入构建一个在关卡中。你会从Actor中调用Draw Call。假如您需要的类只是由数据组成,那么不要从Actor去扩展。
· Pawn:简单的说Pawn是一个Actor,但是可以使用PlayerController或AIController控制。例如Pawn可以是由玩家控制的一条狗或者RTS游戏由AI在控制的单位。
· PlayerController:PlayerController中最重要的部分是,它可以从获得玩家的输入信息(键盘,鼠标,pad等),它拥有Pawn和Character。你可以有不同的PlayerControllers当你需要有不同的移动规则。例如,你控制车的CarPlayerController需要由不同的PlayerController去用于控制飞机。同样对于主菜单的控制或控制游戏性也是如此。让我们创建新的PlayerController并将其命名为GameplayPlayerController。
现在我们配合阅读sdk源码可以用Mutator修改一些简单的参数了,修改完之后我们可能想获得一些输出结果,就像printf(“hello world!”),怎么办呢?
四.输出显示
- Log
应该是最简单的输出手段,结果会保存到log文件中,你无法实时查看,适合事后分析。
Log("All checks OK");
结果:
ScriptLog:All Checks OK
如果你想给一个代码块计时输出,可以标记一个精确的时间戳,用StopWatch()精确控制,比如。
StopWatch(false);
Log("Generating world");
// ... world generation code ...
Log("Finished generating world");
StopWatch(true);
结果:
ScriptLog: 0.00 ms: Generating world
ScriptLog: 9.33 ms: Finished generating world
- 屏幕消息
一行代码就可以方便的显示消息。
Level.Game.Broadcast(Self, "Hello World!");
//或者
PlayerController.ClientMessage("Hello World!");
输出的效果跟玩家在屏幕中打字聊天一样。
- 控制台输出
如果你需要让输出结果显示的更久,可以输出到控制台上,保存的数据更多,还可以用滚轮翻看。
办法:
function Timer()
{
local PlayerController PC;
PC = Level.GetLocalPlayerController();
if (PC != none)
PC.Player.Console.Message("Timer called", 0);
}
如果你在用interaction,可以通过ViewportOwner
调用Player
ViewportOwner.Player.Console.Message("Humans must die!", 0);
- 直接屏幕绘制
比其他方法更灵活跟复杂,允许你直接在屏幕上draw文字和图片。如果你在写mutator,用interaction是最简单的方法。
例子在这里
但是,光一个Mutator远远不够,我们希望能修改更多的部分。
五.替换原有类
别人发布的插件,如果没有加密的话,你是可以看到源码的。两种方法,反编译和修改文件后缀。我建议用后者,因为大部分反编译都属于未经原作者同意的破解行为。把文件后缀修改为txt,就可以在一堆乱码的后面看到源码了。
比如我们打开KFStatsX,一个用来统计玩家的各种数据的插件。可以看到一部分代码:
class KFSXGameRules extends GameRules;
GameRules
也是类似Mutator的一种方便插件开发者修改游戏数据的类,同时他也提供了很多方法可以重写(KF2好像将二者合并了?我忘了),具体查阅SDK。
Mutator
和GameRules
的存储结构类似,都是链式存储,分别以BaseMutator
和GameRulesModifiers
存在GameInfo
类中。
具体GameRules
如何添加到游戏中,查源码,这个类在插件中非常常见。
此外,我们还能看到一部分代码:
class KFSXPlayerController extends KFPlayerController;
PlayerController是在Pawn中都会保存一个引用,同时其内部还定义有ReplicationInfo,是一个非常重要的类。但是这个类跟Mutator和GameRules作用的原理不同,前者只需要添加到集合中,而后者需要替换原有的类。
比如Level.Game.PlayerControllerClassName="StatsScoreboard.KFSSPlayerController";
(来自其他插件)
通过阅读可以发现他是通过获得类名,来动态实例化对象并赋给相应的变量。
官方也提供了一个类替换的例子,实际上几乎所有的kf本身的类都可以替换,方法不一,仔细读其他插件的源码,基本都能找到。
class MutZEDReplace extends Mutator; //替换怪物类
function PostBeginPlay()
{
setTimer(5.0,false); //是为了让插件在怪物class等等其他东西异步加载完之后再发挥作用,false是标记替换Timer只调用一次。
}
function Timer()
{
local KFGameType KF;
local byte i,j;
local class<KFMonster> MC;
KF = KFGameType(Level.Game); //Level.Game是当前游戏类型 GameType
if( KF==None )
Error("This mutator is only for KFGameType!"); //如果当前游戏类型不是KFGameType,取消。
else
{
//normal squads
for( i=0; i<KF.InitSquads.Length; i++)
{
for( j=0; j < KF.InitSquads[i].MSquad.Length; j++ )
{
if ( String(KF.InitSquads[i].MSquad[j]) == "KFChar.ZEDTOREPLACE" ) //ZEDTOREPLACE可以是原版游戏或者自定义沙盘中怪物的任何一种,比如KFChar.ZombieStalker
{
MC = Class<KFMonster>(DynamicLoadObject("PACKAGENAME.NEWZED", class'class'));
KF.InitSquads[i].MSquad[j] = MC; //PACKAGENAME.NEWZED可以是你想替换的怪物的类名
}
}
}
//special squads
for (i=0;i<KF.SpecialSquads.Length;i++)
{
for (j=0; j<KF.SpecialSquads[i].ZedClass.Length;j++)
{
if (KF.SpecialSquads[i].ZedClass[j] == "KFChar.ZEDTOREPLACE")
KF.SpecialSquads[i].ZedClass[j]= "PACKAGENAME.NEWZED";
}
}
//final squads
for (i=0;i<KF.FinalSquads.Length;i++)
{
for (j=0; j<KF.FinalSquads[i].ZedClass.Length;j++)
{
if (KF.FinalSquads[i].ZedClass[j] == "KFChar.ZEDTOREPLACE")
KF.FinalSquads[i].ZedClass[j]= "PACKAGENAME.NEWZED";
}
}
}
}
翻译都在注释里了。
一定要注意代码的调用顺序!在类名实例化之后替换显然是无效的。
if ( PlayerControllerClass == None )
PlayerControllerClass = class<PlayerController>(DynamicLoadObject(PlayerControllerClassName, class'Class'));
NewPlayer = spawn(PlayerControllerClass,,,StartSpot.Location,StartSpot.Rotation);
这就给插件提供了非常大的方便,同时也引出了其他问题。
六.兼容性
Mutator插件就是小mod。方法必须在插件中定义,且功能有限。插件还要符合一些规则,如果你不想按照规则做,你可能应该去写GameType的mod。
第一条规则是插件必须兼容其他插件,无论是在代码安全上还是在游戏玩法上。
第二条规则是插件应该只是稍微修改玩法。话糙理不糙,你得严格限制你的插件,别去大规模的破坏游戏玩法。这样更有利于兼容其他插件的玩法以及减少维护量。
第三条规则是插件应该与其他插件共享资源。如果你的插件实现了ModifyPlayer方法,你需要在方法的其他地方执行NextMutator.ModifyPlayer方法。这样确保了排在你插件列表后面的其他插件也能处理这个方法调用。否则就是很low的代码风格。
翻译自Mod Authoring
一旦你替换了游戏中原有的类,那就要小心了,因为别的插件可能也想替换,可能会引起一些未知的BUG。这也是一部分开发者只将自己的插件用于自己的服务器,并不想上传创意工坊的原因,因为修改的地方太多,很难跟其他插件兼容。
如果你真的很想写个大工程,建议编写GameType,就像游戏菜单中默认的那两个—killing floor 和 killing floor objective ,甚至是后来的Gun Game和Scrn TotalBalance,其中Scrn属于TotalConversion,因为他几乎将除了引擎和框架之外的所有KF1的类都替换了,并进行了自己的大量修改。
如果你有很强的开发热情和技术水平,可以尝试写一个GameType,制作一个完全符合你的想法的游戏模式,否则我建议要么写写小而精的小插件,要么在Scrn的基础上修改,Scrn是开源的。
或者你只想玩玩新武器和新职业?试试ServerPerks,下载完源码之后进行自己的修改,然后编译,记得看看开发者都说了什么。
开发者说禁止拿等级数据卖钱!!!这句话让一部分国内的服主都当空气了!!!
七.网络部分
参考虚幻引擎网络架构
目前博主还没看到过有需要大量网络同步代码的插件,如果你接触到了,那么这篇博文其实也不用看了,我还得向你请教。
还是以刚才的护甲值为例,ShieldStrength
他在Pawn中有如下代码:
reliable if (Role==ROLE_Authority)
ShieldStrength, HitFx, HitFxTicker;
或者我们换一个例子,比如KFScoreBoard,里面有这样的一段代码
// draw kills
if( bDisplayWithKills )
{
Canvas.StrLen(KFPRI.Kills, KillWidthX, YL);
Canvas.SetPos(KillsXPos - 0.5 * KillWidthX, (PlayerBoxSizeY + BoxSpaceY) * i + BoxTextOffsetY);
Canvas.DrawText(KFPRI.Kills, true);
// Draw Kill Assists
Canvas.StrLen(KFPRI.KillAssists, AssistsWidthX, YL);
Canvas.SetPos(AssistsXPos - 0.5 * AssistsWidthX, (PlayerBoxSizeY + BoxSpaceY) * i + BoxTextOffsetY);
Canvas.DrawText(KFPRI.KillAssists, true);
}
// draw cash
CashString = "�"@string(int(TeamPRIArray[i].Score)) ;
if(TeamPRIArray[i].Score >= 1000)
{
CashString = "�"@string(TeamPRIArray[i].Score/1000.f)$"K" ;
}
可以看出,显示在积分版上的击杀生命值等属性都是存储在KFPRI(KFPlayerReplicationInfo)中的,粗略看一下他的代码
// Custom KF Player Rep info. Now including experience levels.
class KFPlayerReplicationInfo extends xPlayerReplicationInfo;
//var float CurrentExperience, ExperienceLevel, ExpThreshold,
var float MGAmmo; //, CurrentWeight;
var bool bBuyingStuff, bStartingEquipmentChosen;
var int ThreeSecondScore; // Total of the points accrued in the last 3 seconds. Cleared every 3 seconds.
var int PlayerHealth; // How much heal the player has. Used by the KFscoreboard.
var bool bViewingMatineeCinematic;
var string SubTitle[5];
var bool bWideScreenOverlay;
var class<KFVeterancyTypes> ClientVeteranSkill;
var int ClientVeteranSkillLevel;
var int CashThrowAmount; // Amount of cash a player throws per keypress. Set in the player settings menu
var FakePlayerPawn BlamePawn;
var int KillAssists;
replication
{
// Things the server should send to the client.
reliable if ( bNetDirty && (Role == Role_Authority) )
bWideScreenOverlay,SubTitle,PlayerHealth,bBuyingStuff,bStartingEquipmentChosen,
ThreeSecondScore,bViewingMatineeCinematic,ClientVeteranSkill,ClientVeteranSkillLevel,
KillAssists;
}
可以看出他存储了相当多的数据变量,那么他是做什么的?参考UDK的ReplicationInfo
UDK|ReolicationInfos,而且有很多种不同的ReplicationInfo子类,但是需要考虑的不多。可以阅读以下Scrn的源码,我一般用它来处理计分板的显示,另外他还有一个好处就是每个玩家的Controller身上都只有一个PlayerReplicationInfo,所以如果你想对每个玩家存储一些重要信息,可以存到这些PlayerReplicationInfo里。当然你也可以在服务器的插件那里维护一个数组,存着玩家的信息,都可以。如果你希望某个数据被所有的PlayerReplicationInfo所同步,一定要添加在replication代码块中。
除了变量需要添加同步标记外,有的方法也需要。不过好像对于普通插件开发者基本不用考虑,因为都是调用框架提供的方法。
除了这些框架中封装好的消息同步机制外,UnrealScript还提供基本的TCP或者UDP通信,你可以根据Legacy Unreal Wiki中提供的方法,进行通信的尝试。比如你可以建立一个solo服务器,将玩家通关或死亡时游戏的一些数据发送出来。基本的通信框架需要的代码很少。别忘了用其他语言写一个简单的通信服务器来接收你发送出来的数据。
简单的科普:TCP流式传输,比较麻烦(需要处理粘包),但是可靠。UDP数据报传输,不用处理粘包,代码更短,但是不可靠。不过一般来说都很可靠。
注意:
1.UT2004发送出来的数据格式为ASCII,所以你在用通信客户端接收消息的时候注意把编码格式一起做相应修改。
2.尽量不要尝试用玩家的SteamID来标识,因为还有很多的游侠玩家。博主曾经想写一个UnitedBan(仅做技术尝试,毕竟玩家也不多),不过后来一想还是放弃了。
这样一来,你的插件就可以处理更多复杂的数据,但是,数据一多,看起来就会很拥挤,光在控制台向玩家显示出来是远远不够的,你需要更多的美化。
八.图形化
目前可以用Canvas绘图,或者用ScriptedTexture可编程纹理
Canvas绘图的代码在源码里出现的很多,仿照着用即可,ScriptedTexture的学习可以参考传送枪插件的代码。
九.美术素材
图片,声音,模型,理论上都可以添加,把素材找到对应的目录放进去。
代码类似于
#exec OBJ LOAD FILE=WeaponSounds.uax
可以用地图编辑器打包成uax文件。
如何显示和播放,翻源码。
官方教程写的很详细了。
素材可以用sdk新建一个package,添加导入再导出即可。
十.编译和上传
如何编译在T社官方插件最简教程已经有介绍了
在killingfloor.ini中搜索EditPackages
,并仿照其格式在最后加上EditPackages=ExampleMutator
,然后再创建一个.bat文件,里面写
del “D:\Program Files (x86)\Steam\steamapps\common\KillingFloor\System\ExampleMutator .u”
“D:\Program Files (x86)\Steam\steamapps\common\KillingFloor\System\UCC.exe” make
del “D:\Program Files (x86)\Steam\steamapps\common\KillingFloor\System\steam_appid.txt”
pause`
别忘了把目录改成自己的目录,最后打开.bat文件执行编译即可
下面是如何上传到创意工坊。
打开地图编辑器,左上角Tools->最下面两个一个是Upload一个是Update,如果你第一次上传,点Upload,如果你是更新之前的插件,点Update。如果更新失败,那就用下特殊工具(你懂得)。上传完之后别忘了去工坊编辑简介。
另外KF1的创意工坊有问题,自动下载插件非常困难,所以最好给外国玩家们提供一个Google Drive的下载地址,国内的百度网盘,改下链接,否则网盘会被Steam吞掉。
最后
其实只要能入了门,后面的学习就是个时间和兴趣问题,毕竟你还是得不停地从外网找资料。
比如可以找一个非常厉害的kf1插件:Client-Side Hit Detection ,本地爆头计算插件,T社论坛的下载地址已经失效了,但是这个插件最初是来自于UT2004,要是能拿到源码,也许能改成适合KF1的。