求生之路2关卡设计/清道夫结局
本页面由大康翻译于2021年9月25日,相对于原文有小改动(例如输出)并修复了一些内容,但没有删减内容。部分内容由机器翻译。
清道夫结局需要一个结局区域,感染者会在此区域试图阻止生还者捡起散落在地图上的汽油桶,以及用它们给逃生载具加油。对抗模式 的得分取决于在一个回合中给逃生载具加了油的汽油桶数量。与标准结局 相比,清道夫结局需要一些额外的实体和脚本 ,才可以正常进行。
组成内容
c1m4_atrium 的反编译地图 将作为案例。另一张地图是 c6m3_port,其中还有求生之路1的生还者。这是与清道夫结局相关的组成内容的粗略列表:
- 清道夫模式专用的实体:
- 其他实体:
- logic_timer
- info_director
- trigger_finale 的结局类型设置为
Scavenge
- VScript 脚本
- c1m4_atrium.nut
- c1m4_atrium_finale.nut
- c1m4_delay.nut
地图
一般来说,你需要在生还者和汽油桶的空间之间设置障碍物。障碍物通常是加油口(point_prop_use_target)和汽油桶之间的距离/地形。
武器
考虑到生还者在开始终局时需要武器,他们仍然需要能够为面前的挑战做好准备,所以路上还应该有武器和物品,以便生还者可以坚持一段时间。玩你的地图的人会有很多不同的方式,例如,喜欢花时间游玩的人与快速通关的人相比。
环境
这些只是建议:
- 制作多条路径,让生还者可以进出建筑物以获取补给品,甚至可以在终局中使用汽油桶(译者注:应该不是灌油用的汽油)。
- 请记住,玩家并不总是想要走狭窄的走廊,而是大型开放区域,例如生还者可以走在街道上,但会被附近建筑物中生成的感染者或从屋顶攻击他们的特殊感染者攻击。
- 汽油桶和灌油口之间的距离是两队优势的平衡。
清道夫结局实体
- 你需要一个 trigger_finale 以便生还者可以开始结局。在该实体中,你将找到各种选项(即键值)。其中之一是结局类型。它的默认设置为“Standard”(标准)。你要把它设置为“Scavenge”(清道夫)。现在保持 Use Delay 键值不变,但将 First Use Delay 设置为 5 秒。这可以让生还者通过播放音频知道他们在做什么。(这不是必需的,但确实有帮助)
- 你要放置一个 game_scavenge_progress_display 实体。在它的属性界面上,你可以将(加油的)最大值更改为你想要的任何值。出于本教程的目的,我们将其设为 8。然后将其目标名(Name, 不是 Classname)设置为唯一的名称。在本教程里,我们设置为 scav_progshower。接下来,你需要一个 math_counter 实体。将其目标名设置为 scav_counter,然后将其 Initial Value (初始值)保留为 0,最大值与你在 game_scavenge_progress_display 中设置的最大值相同。在本教程中,我们将其设置为 8。将其最小值保留为 0。
- 你需要确定要灌油的位置。模型可以是你选择的任何模型。在本教程中,我们将使用灌油口模型 radio_generator_fillup.mdl。你需要放置一个 prop_dynamic 实体,然后在模型浏览器中查找 radio_generator_fillup。将其放置在距离玩家可以操作的范围内(玩家高度约为 64 个单位,因此最好的高度为 45 到 50 个单位)。将其轮廓颜色设置为你选择的任何颜色。
- 你需要在加油口上放置一个 point_prop_use_target 实体。
转到你的加油口模型实体并打开其属性并将其目标名设置为 scav_nozzle。然后返回你的 point_prop_use_target 并打开属性界面切换到输出菜单。添加具有以下设置的新输出:
My Output | Target Entity | Target Input | Parameter | Delay | Only Once | |
---|---|---|---|---|---|---|
OnUseFinished | scav_counter | Add | +1 (或者是1) | 0.00 | No |
这意味着当玩家用汽油桶在加油口加了油后,point_prop_use_target 将向 scav_counter 添加一个值。
汽油桶
在实体列表中查找 weapon_scavenge_item_spawn 实体。将它放置于地图并打开其属性界面。将其目标名更改为 scav_gascans。 然后回到你的 trigger_finale 实体,打开它的属性界面,转到输出选项卡并单击添加(Add)
输入以下内容:
My Output | Target Entity | Target Input | Parameter | Delay | Only Once | |
---|---|---|---|---|---|---|
UseStart | scav_gascans | TurnGlowsOn | <none> | 0.00 | No |
(以上意味着当结局开始时,所有名为 scav_gascans 的实体的轮廓会开始发光。)
添加另一个输出,如下所示:
My Output | Target Entity | Target Input | Parameter | Delay | Only Once | |
---|---|---|---|---|---|---|
UseStart | scav_nozzle | StartGlowing | <none> | 0.00 | No |
(与上面类似,让加油口的模型的轮廓发光。)
接下来放置实体 logic_auto 并转到其属性的输出选项卡,然后单击 Add。
添加以下输出:
(以上两个输出确保它们在我们开始结局之前不会发光,最后的输出可确保加油进度的 HUD 在我们启用之前不会处于活动状态。)
救援载具
这通常是一个带有动画的 prop_dynamic,但实际上它可以是任何东西。
出于本教程的目的,我们仅将 C130 用于 prop_dynamic 实体作为救援载具。
转到纹理浏览器并查找"trigger"。它看起来像这样:
制作一个和你的逃离载具一样大的固体,空间大小是 C130 的内部空间。按 ↵ Enter 键创建它,然后按 Ctrl+T 转换为实体。在 Classname 下输入 trigger_multiple 并将其目标名改为 escape_trigger。然后将 Entire Team Number 设置为 Survivor,并将 Start Disabled 设置为 Yes。然后关闭它的属性界面。
返回你的 math_counter 并转到输出选项卡。添加如下输出:
然后返回到你的 trigger_finale 并将其目标名更改为 Scav_finale_starter。
安全地带
你需要在地图范围之外制作一个带有 4 个info_survivor_position实体的区域。在实体属性界面中,将第一个 info_survivor_position 的 Order 更改为 1,将第二个更改为 2,依此类推,直到第四个。然后将它们的目标名更改为你放置它们的任何顺序。(例如:survivor_pos1、survivor_pos2 等)
转到你的 trigger_escape(你在救援载具中制作的 trigger 笔刷)并添加具有以下内容的输出:
然后返回到纹理浏览器。寻找 Clip (空气墙);它看起来像这样:
将这个固体环绕在地图范围之外的幸存者位置(info_survivor_position)周围,然后按 ↵ Enter 创建它。然后单击 Ctrl+T 转换为实体。将 Classname 更改为 func_brush。将 Solid BSP 键值设置为 Yes,然后把 Solidity 键值设置为 Always Solid。最后,将目标名设置为 clip_scavend,点击应用并关闭其属性界面。
返回你的 trigger_multiple 添加输出:
My Output | Target Entity | Target Input | Parameter | Delay | Only Once | |
---|---|---|---|---|---|---|
OnEntireTeamStartTouch | clip_scavend | Kill | <none> | 0.00 | No |
用 nodraw 纹理的固体覆盖逃离载具的地板,这样你就可以在上面行走并在上面创建导航网格。
然后放置 point_veiwcontrol_multiplayer 实体,作为救援载具逃离动画的相机。然后打开其属性并将其命名为 outro_cam。然后放置 2 个 env_fade 实体,将第一个命名为 fade1,并将其 Hold Time (保持时间)键值设置为1。然后将其 Fade In/Out 键值设置为 0.15。
(这样做不是看到玩家和快速切换到相机,而是允许淡入淡出。在此期间,生还者将传送到他们的位置,然后视图将返回到相机。)
在第一个淡入淡出的实体的输出选项卡中添加输出:
My Output | Target Entity | Target Input | Parameter | Delay | Only Once | |
---|---|---|---|---|---|---|
OnFade | fade2 | Fade | <none> | 6.00 | No |
然后返回到逃离载具内的 trigger_escape 并添加输出:
My Output | Target Entity | Target Input | Parameter | Delay | Only Once | |
---|---|---|---|---|---|---|
OnEntireTeamStartTouch | outro_cam | Enable | <none> | 0.00 | No |
转到 fade2 实体并添加如下输出:
My Output | Target Entity | Target Input | Parameter | Delay | Only Once | |
---|---|---|---|---|---|---|
OnFade | outro_stats | RollStatsCrawl | <none> | 0.00 | No |
然后在实体选项卡中查找 env_outtro_stats 。 在其属性中将其目标名设置为 outro_stats。
导航网格
运行你的地图,进入关卡后按波浪号键 ~ 打开开发者控制台,然后输入
- sv_cheats 1——这允许在游戏中启用作弊
- noclip——允许在地图上自由移动,你不会被任何东西挡住
- nav_edit 1——启用导航网格编辑
- z_debug 1——允许你在地图上查看导航属性和所有丧尸
- director_stop——阻止导演生成丧尸
- nb_delete_all——在地图上删除所有的 NPC
然后去你的结局区域并打开开发者控制台并输入
- nav_mark_walkable——在地图上标记一个可步行的点,由紫色金字塔标记,可以生成导航网格
- nav_generate_incremental——生成距 nav_mark_walkable 一定距离的导航网格
然后确保你在结局触发器周围选择了你想要的区域,以及丧尸从中产生的距离区域。然后在开发者控制台中输入以下内容:
- mark Finale——这是在战役的最终关卡中使用的导航网格的属性,并与 trigger_finale 结合使用。
然后去你的玩家起始区域并用 CHECKPOINT 标记这些区域。 最后,你需要去你的逃离载具并用 RESCUE_VEHICLE 标记里面的导航区域。(如右图所示)
完成后,在开发者控制台中输入以下内容:
- nav_analyze——这会分析所有导航网格并在 left4dead2 文件夹的地图文件夹中写入一个名为“<你的地图名称>.nav”的文件。
这是你制作清道夫结局所需的全部内容,希望对你有所帮助
脚本(VScript)
此次清道夫结局需要三个脚本。请注意,有些代码是多余的或被注释掉的,可能是开发过程的标记。包括补充意见:
c1m4_atrium.nut
该脚本需要作为导演脚本使用 info_director 实体的 BeginScript 输入手动在 OnMapSpawn 事件加载。它设置汽油桶的数量、默认的 CommonLimit、解锁救援车辆导航区域,并且(出于某种原因)声明函数 GasCanPoured()。
- 如果是单人游戏,汽油桶的数量会减少
- 由于人类玩家将在救援车辆上活动,因此救援车辆导航区域是不能被阻挡的。
- GasCanPoured() 将由导演 (OnTeamScored) 或 point_prop_use_target (OnUsedFinished) 调用,只要成功倒入汽油桶。
Msg(" atrium map script "+"\n")
// number of cans needed to escape.
if ( Director.IsSinglePlayerGame() )
{
NumCansNeeded <- 8
}
else
{
NumCansNeeded <- 13
}
// This script is called on MapSpawn, so the CommonLimit is for play before the finale start.
DirectorOptions <-
{
CommonLimit = 15
}
NavMesh.UnblockRescueVehicleNav() // Unblock so humans can be rescued when incapped near nozzle
EntFire( "progress_display", "SetTotalItems", NumCansNeeded ) //Set number of cans with game_scavenge_progress_display
function GasCanPoured(){} // Declaration of function, but was moved to main finale script
c1m4_atrium_finale.nut
一旦结局强制开始(当电梯门在中庭的最低层打开时),就会加载此脚本。它包含大多数结局设置和逻辑。
- 有多个结局阶段,ONSLAUGHT (猛攻)、PANIC (尸潮)或 TANK。第一阶段是 ONSLAGUT,本阶段的 DirectorOptions 将使用 InitialOnslaughtOptions。一旦汽油桶被捡起四次或一个汽油桶成功倒出,本阶段就会结束,导致 PANIC 阶段
- 其余的猛攻从 DirectorOptions 的 c1m4_delay.nut 运行。这种猛攻与时间有关,并且在进入下一阶段之前对汽油桶接触次数的容忍度较低。
Msg("----------------------FINALE SCRIPT------------------\n")
//-----------------------------------------------------
// Stage type enumerations
PANIC <- 0
TANK <- 1
DELAY <- 2
ONSLAUGHT <- 3
//-----------------------------------------------------
// Initialized tables along with stage settings
SharedOptions <-
// Base DirectorOptions
{
A_CustomFinale1 = ONSLAUGHT //Will be stopped with input to director, EndCustomScriptedStage
A_CustomFinaleValue1 = "" //InitialOnslaughtOptions is slightly different from c1m4_delay
A_CustomFinale2 = PANIC
A_CustomFinaleValue2 = 1 //1 PANIC wave
A_CustomFinale3 = ONSLAUGHT
A_CustomFinaleValue3 = "c1m4_delay" //This onslaught also depends on timer
A_CustomFinale4 = PANIC
A_CustomFinaleValue4 = 1
A_CustomFinale5 = ONSLAUGHT
A_CustomFinaleValue5 = "c1m4_delay"
A_CustomFinale6 = TANK
A_CustomFinaleValue6 = 1
A_CustomFinale7 = ONSLAUGHT
A_CustomFinaleValue7 = "c1m4_delay"
A_CustomFinale8 = PANIC
A_CustomFinaleValue8 = 1
A_CustomFinale9 = ONSLAUGHT
A_CustomFinaleValue9 = "c1m4_delay"
A_CustomFinale10 = PANIC
A_CustomFinaleValue10 = 1
A_CustomFinale11 = ONSLAUGHT
A_CustomFinaleValue11 = "c1m4_delay"
A_CustomFinale12 = PANIC
A_CustomFinaleValue12 = 1
A_CustomFinale13 = ONSLAUGHT
A_CustomFinaleValue13 = "c1m4_delay"
A_CustomFinale14 = TANK
A_CustomFinaleValue14 = 1 //1 TANK
A_CustomFinale15 = ONSLAUGHT
A_CustomFinaleValue15 = "c1m4_delay"
A_CustomFinale16 = PANIC
A_CustomFinaleValue16 = 1
A_CustomFinale17 = ONSLAUGHT
A_CustomFinaleValue17 = "c1m4_delay"
A_CustomFinale18 = PANIC
A_CustomFinaleValue18 = 1
A_CustomFinale19 = ONSLAUGHT
A_CustomFinaleValue19 = "c1m4_delay"
A_CustomFinale20 = PANIC
A_CustomFinaleValue20 = 1
A_CustomFinale21 = ONSLAUGHT
A_CustomFinaleValue21 = "c1m4_delay"
A_CustomFinale22 = TANK
A_CustomFinaleValue22 = 1
A_CustomFinale23 = ONSLAUGHT
A_CustomFinaleValue23 = "c1m4_delay"
A_CustomFinale24 = PANIC
A_CustomFinaleValue24 = 1
A_CustomFinale25 = ONSLAUGHT
A_CustomFinaleValue25 = "c1m4_delay"
A_CustomFinale26 = PANIC
A_CustomFinaleValue26 = 1
A_CustomFinale27 = ONSLAUGHT
A_CustomFinaleValue27 = "c1m4_delay"
A_CustomFinale28 = PANIC
A_CustomFinaleValue28 = 1
A_CustomFinale29 = ONSLAUGHT
A_CustomFinaleValue29 = "c1m4_delay"
A_CustomFinale30 = PANIC
A_CustomFinaleValue30 = 1
A_CustomFinale31 = ONSLAUGHT
A_CustomFinaleValue31 = "c1m4_delay"
// End of finale, regardless of gas cans filled
//-----------------------------------------------------
// More Default DirectorOptions
PreferredMobDirection = SPAWN_LARGE_VOLUME
PreferredSpecialDirection = SPAWN_LARGE_VOLUME
// BoomerLimit = 0
// SmokerLimit = 2
// HunterLimit = 1
// SpitterLimit = 1
// JockeyLimit = 0
// ChargerLimit = 1
ProhibitBosses = true
ZombieSpawnRange = 3000
MobRechargeRate = 0.5
HordeEscapeCommonLimit = 15
BileMobSize = 15
MusicDynamicMobSpawnSize = 8
MusicDynamicMobStopSize = 2
MusicDynamicMobScanStopSize = 1
}
InitialOnslaughtOptions <-
// DirectorOptions for first onslaught
{
LockTempo = 0
IntensityRelaxThreshold = 1.1
RelaxMinInterval = 2
RelaxMaxInterval = 4
SustainPeakMinTime = 25
SustainPeakMaxTime = 30
MobSpawnMinTime = 4
MobSpawnMaxTime = 8
MobMinSize = 2
MobMaxSize = 6
CommonLimit = 5
SpecialRespawnInterval = 100
}
PanicOptions <-
// DirectorOptions when in a PANIC stage
{
MegaMobSize = 0 // randomized in OnBeginCustomFinaleStage
MegaMobMinSize = 20
MegaMobMaxSize = 40
CommonLimit = 15
SpecialRespawnInterval = 40
}
TankOptions <-
// DirectorOptions when in a TANK stage
{
ShouldAllowMobsWithTank = true
ShouldAllowSpecialsWithTank = true
MobSpawnMinTime = 10
MobSpawnMaxTime = 20
MobMinSize = 3
MobMaxSize = 5
CommonLimit = 7
SpecialRespawnInterval = 60
}
DirectorOptions <- clone SharedOptions
// Start with SharedOptions
{
}
//-----------------------------------------------------
// number of cans needed to escape. again. (Later moved to c1m4_atrium.nut)
NumCansNeeded <- 13
// fewer cans in single player since bots do not help much
if ( Director.IsSinglePlayerGame() )
{
NumCansNeeded <- 8
}
// duration of delay stage.
DelayMin <- 10
DelayMax <- 20
// Number of touches and/or pours allowed before a delay is aborted.
DelayPourThreshold <- 1
DelayTouchedOrPouredThreshold <- 2
// Once the delay is aborted, amount of time before it progresses to next stage.
AbortDelayMin <- 1
AbortDelayMax <- 3
// Number of touches and pours it takes to transition out of c1m4_finale_wave_1
GimmeThreshold <- 4
// console overrides
if ( Director.IsPlayingOnConsole() )
{
DelayMin <- 20
DelayMax <- 30
// Number of touches and/or pours allowed before a delay is aborted.
DelayPourThreshold <- 2
DelayTouchedOrPouredThreshold <- 4
TankOptions.ShouldAllowSpecialsWithTank = false
}
//-----------------------------------------------------
// INIT
//-----------------------------------------------------
GasCansTouched <- 0
GasCansPoured <- 0
DelayTouchedOrPoured <- 0
DelayPoured <- 0
EntFire( "timer_delay_end", "LowerRandomBound", DelayMin )
EntFire( "timer_delay_end", "UpperRandomBound", DelayMax )
EntFire( "timer_delay_abort", "LowerRandomBound", AbortDelayMin )
EntFire( "timer_delay_abort", "UpperRandomBound", AbortDelayMax )
// this is occurs too late. moved to c1m4_atrium.nut
//EntFire( "progress_display", "SetTotalItems", NumCansNeeded )
function AbortDelay(){} // only defined during a delay, in c1m4_delay.nut
function EndDelay(){} // only defined during a delay, in c1m4_delay.nut
NavMesh.UnblockRescueVehicleNav() // This is redundant since it was already done once
//-----------------------------------------------------
function GasCanTouched()
// This is called by weapon_scavenge_item_spawn OnItemPickedUp
{
GasCansTouched++
Msg(" Touched: " + GasCansTouched + "\n")
EvalGasCansPouredOrTouched()
}
function GasCanPoured()
// In this map, it is called by the director OnTeamScored
{
GasCansPoured++
DelayPoured++
Msg(" Poured: " + GasCansPoured + "\n")
if ( GasCansPoured == NumCansNeeded )
{
Msg(" needed: " + NumCansNeeded + "\n")
EntFire( "relay_car_ready", "trigger" )
}
EvalGasCansPouredOrTouched()
}
function EvalGasCansPouredOrTouched()
// Evaluate the number of times gas cans poured or touched
{
TouchedOrPoured <- GasCansPoured + GasCansTouched
Msg(" Poured or touched: " + TouchedOrPoured + "\n")
DelayTouchedOrPoured++
Msg(" DelayTouchedOrPoured: " + DelayTouchedOrPoured + "\n")
Msg(" DelayPoured: " + DelayPoured + "\n")
if (( DelayTouchedOrPoured >= DelayTouchedOrPouredThreshold ) || ( DelayPoured >= DelayPourThreshold ))
// This is for c1m4_delay.nut (c1m4_delay.nut also resets the counter for Poured and TouchOrPoured)
{
AbortDelay()
}
switch( TouchedOrPoured ) //For stopping the InitialOnslaught (first stage)
{
case GimmeThreshold:
EntFire( "@director", "EndCustomScriptedStage" )
break
}
}
//-----------------------------------------------------
function AddTableToTable( dest, src )
// This function is used to move table keys and values to other tables
{
foreach( key, val in src )
{
dest[key] <- val
}
}
function OnBeginCustomFinaleStage( num, type )
// Special Function every time a finale stage starts.
// Instructions in this function set DirectorOptions for PANIC and TANK
{
printl( "Beginning custom finale stage " + num + " of type " + type );
local waveOptions = null
if ( num == 1 )
{
waveOptions = InitialOnslaughtOptions
}
else if ( type == PANIC )
{
waveOptions = PanicOptions
waveOptions.MegaMobSize = PanicOptions.MegaMobMinSize + rand()%( PanicOptions.MegaMobMaxSize - PanicOptions.MegaMobMinSize )
Msg("*************************" + waveOptions.MegaMobSize + "\n")
}
else if ( type == TANK )
{
waveOptions = TankOptions
}
//---------------------------------
MapScript.DirectorOptions.clear()
AddTableToTable( MapScript.DirectorOptions, SharedOptions );
if ( waveOptions != null )
{
AddTableToTable( MapScript.DirectorOptions, waveOptions );
}
Director.ResetMobTimer()
if ( developer() > 0 )
{
Msg( "\n*****\nMapScript.DirectorOptions:\n" );
foreach( key, value in MapScript.DirectorOptions )
{
Msg( " " + key + " = " + value + "\n" );
}
if ( LocalScript.rawin( "DirectorOptions" ) )
{
Msg( "\n*****\nLocalScript.DirectorOptions:\n" );
foreach( key, value in LocalScript.DirectorOptions )
{
Msg( " " + key + " = " + value + "\n" );
}
}
}
}
//-----------------------------------------------------
if ( Director.GetGameMode() == "coop" )
{
}
else if ( Director.GetGameMode() == "versus" ) // Allow tanks and witches in VS
{
SharedOptions.ProhibitBosses = false
}
c1m4_delay.nut
每当开始猛攻阶段时都会加载此脚本,但第一个阶段除外。
- 调用 logic_timer 实体启动计时器以进入下一阶段
- 阶段在结束时间(timer_delay_end)或汽油桶可以灌油和拾取(timer_delay_abort)时结束
Msg("**Delay started**\n")
DirectorOptions <-
{
MobMinSize = 2
MobMaxSize = 3
BoomerLimit = 0
SmokerLimit = 0
HunterLimit = 0
SpitterLimit = 0
JockeyLimit = 0
ChargerLimit = 0
MinimumStageTime = 15
CommonLimit = 5
}
Director.ResetMobTimer() //Start the above onslaught settings immediately
// start the delay timer
EntFire( "timer_delay_end", "enable" )
//reset for this stage (PANIC and TANK depend on other factors)
DelayTouchedOrPoured <- 0
DelayPoured <- 0
//-------------------------------------------------
// abort the delay if a survivor picks up or pours a gas can
// Function called by c1m4_atrium_finale.nut
function AbortDelay()
{
Msg("**Delay aborted early**\n")
EntFire( "timer_delay_abort", "enable" )
}
// called by the timers themselves
// These EntFired timers MUST be the same targetname in the map!
function EndDelay()
{
Msg("**Delay ended**\n")
EntFire( "timer_delay_end", "Disable" )
EntFire( "timer_delay_end", "ResetTimer" )
EntFire( "timer_delay_abort", "Disable" )
EntFire( "timer_delay_abort", "ResetTimer" )
EntFire( "@director", "EndCustomScriptedStage" )
}