Tutorial; Campaign - HWRM/KarosGraveyard GitHub Wiki
How to Create a Singleplayer Campaign by Wyvern. Discussion
I've had a couple of people ask me how to create a single player game. I'm still learning, myself, so everything I'm going to add here just comes from my personal experience. Feel free to correct me or add stuff that I've missed, etc. As I learn more and confirm/disprove theories about what is going on, I'll update it.
I'm going to assume that you have already completed the design documentation for your mod. If you have not or don't know what that is, I recommend that you head over to www.gamedev.net and start reading some game design/storyline development articles. Most of them are pretty advanced, so they may go into more detail than is necessary for a mod. But, they give a general idea about what is needed.
(Someone has made a simplified version of Chris Taylor's (ala Dungeon Siege, Total Annihilation) Design Document Template, but I can't find the URL.) I'm also going to assume that you understand how LUA scripting works... or at least are smart enough to figure it out.
Because I don't feel very creative, I'm just going to expand upon the "Postmortem" Campaign in the RDN Tools SCAR documentation. Read this documentation before making your campaign. I'm also going to describe how to localize a campaign, though I recommend that you get a native speaker of the language you wish to translate to (Babel Fish ain't that grand - try translating English to German, and then back again - good for a laugh!).
Final note before we begin: we are going to override the single player campaign "Ascension". It is not ideal, but it's the best we've been able to do so far. I highly recommend that you create a new profile before you start testing your SP campaign.
Ok, I lied. This is the Final Note: READ THE ENTIRE DOCUMENT BEFORE YOU BEGIN - TWICE.
Step One: Creating the Mission Folder Hierarchy
Go to your "Homeworld2\Data" directory and create the following directories:
"LevelData"
"LevelData\Campaign"
"LevelData\Campaign\Ascension"
For each level you want to have in the campaign, you need to create a directory, e.g.:
"LevelData\Campaign\Ascension\m01"
"LevelData\Campaign\Ascension\m02"
...
"LevelData\Campaign\Ascension\mNN"
You can name these directories whatever you want. I recommend the "m##" format or something similar. Relic used the following:
"LevelData\Campaign\Ascension\m01_tanis"
"LevelData\Campaign\Ascension\m02_hiigara"
...
"LevelData\Campaign\Ascension\m15_homeworld"
For the Postmortem campaign, we are going to make two missions so that the use of persistent fleets can be demonstrated. So, create the following directories:
"LevelData\Campaign\Ascension\m01"
"LevelData\Campaign\Ascension\m02"
While we are creating directories, we are also going to create the directories required for localizing the campaign. You will need to add the same directories for English, French, Spanish, German, Leet, Braille, or whatever language you decide to support.
In the "Homeworld2\Data" directory create the following directories:
"Locale"
"Locale\<LanguageToSupport>"
"Locale\<LanguageToSupport>\Campaign"
"Locale\<LanguageToSupport>\Campaign\Ascension"
"Locale\<LanguageToSupport>\Campaign\Ascension\m01"
"Locale\<LanguageToSupport>\Campaign\Ascension\m02"
Step Two: Creating the Campaign File
We now need to create the campaign file. So, in the "Leveldata\Campaign" directory create a new file called:
"Ascension.Campaign"
Why "Ascension.Campaign" and not "Postmortem.Campaign"? This is because we are overriding the "Ascension" campaign so that we can actually run our custom campaign.
Open the "Ascension.Campaign" file and enter this code in:
-- This is the name of our campaign. It is displayed to the left of the mission select screen.
displayName = "Postmortem"
-- This creates the structure for the missions
Mission = {}
-- The first mission's details
Mission[1] =
{
-- This tells the game to play the animatic prior to the game starting
-- postload = function ()
-- playAnimatic("data:animatics/A00.lua",1,1);
-- end,
-- Informs the game where to get the mission information from
directory = "M01",
-- The name of the level to load
level = "M01.level",
-- Tells the game what to do when the mission is complete
postlevel = function ( bWin )
if bWin == 1 then
postLevelComplete()
end
end,
-- This is the title of the mission in the mission selection screen
displayName = "Mission One",
-- This is the description of the mission in the mission selection screen
description = "The first Postmortem mission",
}
Mission[2] =
{
directory = "M02",
level = "M02.level",
postlevel = function ()
postLevelComplete()
end,
displayName = "Mission Two",
description = "The second Postmortem mission",
}
Why are the postlevel fields of Mission 1 and Mission 2 different? Well, Mission 1 checks to see if a certain value was set when the mission ended. If it's true it moves on to the next mission. Mission 2 moves on to the next mission regardless. At least that's my understanding of it; please correct me if I'm wrong.
Step Three: More File Creation
For each mission that you have in the campaign file, you absolutely must have the "*.level" and "*.lua" files. The ".level" file has to be the same as the name given in the campaign file. The ".lua" has to have the same name as the ".level" file. It is recommended that you have a "TeamColor.lua" in each mission folder. It controls the colour of each of the players in the campaign (each CPU-controlled fleet counts as a player). Seeing as we are going to localize the campaign, each mission directory also requires a "Datafiles.lua".
For the Postmortem campaign we need the following files.
In "Ascension\m01" we need:
"M01.level"
"M01.lua"
"Teamcolour.lua"
"Datafiles.lua"
In "Ascension\m02" we need:
"M02.level"
"M02.lua"
"Teamcolour.lua"
"Datafiles.lua"
Step Four: ".level" Creation
To create a level that actually looks good, you have to have Maya, as well as the necessary skills to use it. However, you can also make a basic ".level" file using Notepad. So, seeing as this is just a test campaign, and that I have neither Maya nor the skill to use it if I did, we'll be using Notepad.
So, copy the following into "m01.level":
function DetermChunk()
addSquadron("Hgn_Mothership", "Hgn_Mothership", {1614, 141, 5229}, 0, {0, 0, 0}, 0, 1)
addSphere("Mothership_EnterVolume", {1641, 101, 5231}, 488.991)
setWorldBoundsInner({0, 0, 0}, {30000, 30000, 30000})
createSOBGroup("MotherShip")
addToSOBGroup("Hgn_Mothership", "MotherShip")
end -- End of deterministic function
function NonDetermChunk()
fogSetActive(0)
setGlareIntensity(0)
setLevelShadowColour(0, 0, 0, 1)
loadBackground("m03")
setSensorsManagerCameraDistances(12000, 60000)
setDefaultMusic("Data:sound/music/STAGING/STAGING_04")
end -- End of nondeterm function
-- HeaderInfo: max number of players allowed on this level
maxPlayers = 1;
-- PlayerInfo: info on each possible player in this level
player = {};
player[0] =
{
id = 0,
name = , resources = 20000, raceID = 1, startPos = 1, }## In "m02.level" copy this: ##function DetermChunk() -- Player Startpoint. addPoint("PlayerStartPoint", {0, 0, 12300}, {0, 180, 0}) setWorldBoundsInner({0, 0, 0}, {30000, 30000, 30000}) end -- End of deterministic function function NonDetermChunk() fogSetActive(0) setGlareIntensity(0) setLevelShadowColour(0, 0, 0, 1) loadBackground("m03") setSensorsManagerCameraDistances(12000, 60000) setDefaultMusic("Data:sound""/music/STAGING/STAGING_04")
end -- End of nondeterm function
-- HeaderInfo: max number of players allowed on this level
maxPlayers = 1;
-- PlayerInfo: info on each possible player in this level
player = {};
player[0] =
{
id = 0,
name = , resources = 20000, raceID = 1, startPos = 1, }## In "m02.level" we just add a start point. **__Step Five: "TeamColour.lua"__** This sets up the colours that you want each side to have in your mission. While you don't need to have one, it is probably a good idea. This is the code you need: ##teamcolours = { [0] = -- The player number { { -- Team Colour (White) 255.0, 255.0, 255.0, }, { -- Stripe Colour (Black) 0.0, 0.0, 0.0, }, -- Badge to display "DATA:badges""/kiith naabal.tga",
{ -- Engine trail colour (I think. I haven't played with it)
255.0,
255.0,
255.0,
},
"data:/effect/trails/hgn_trail_clr.tga",
},
}
The colours are set up as a Red, Green, Blue format. Each colour goes from 0 to 255. You need to have the '.0' after each number, or else it won't work. In Relic's "SCAR.pdf", it looks like decimals can also be used, instead, although I haven't tried it yet. I've also been having a problem with one team colour in my SP campaign... as soon as I work out what it is I'll update this if it needs it.
Step Six: "m01.lua"
Open up "m01.lua". For this mission we are going to set it up so that if the player builds a scout, they will complete the mission. If they build an intercepter, then they will fail. Pretty sad mission - but it'll demonstrate points. We should also add in objectives so that the player knows what is going on.
Setup:
-- Imports the library files
dofilepath("Data:scripts\SCAR\SCAR_Util.lua")
-- Sets the objectives
obj_prim_newobj_id = 0
We are setting a global constant called ob_prim_newobj_id, so that we can refer to it at any point (just makes code tidier and easier to read).
Entry point:
function OnInit()
-- Adds the Rule_Init
Rule_Add("Rule_Init")
-- OnInit isn't a rule so there is no need to remove it
end
The OnInit function is a required function. This is where the game starts off.
Initialization of gamerules:
function Rule_Init()
-- Does one of those fancy Intel-tell-the-player-whats-going-on
Event_Start( "IntelEvent_Intro" )
-- Makes it easy on the player and gives them fighter production
SobGroup_CreateSubSystem("MotherShip", "FighterProduction")
-- Tells the Mothership to exit hyperspace
SobGroup_ExitHyperSpace ("MotherShip", "Mothership_EnterVolume")
-- Sets up the Win and Lose conditions
Rule_Add( "Rule_Player_Wins" )
Rule_Add( "Rule_Player_loses" )
-- We only want this rule to play the once. So, remove it now.
Rule_Remove( "Rule_Init" )
end
Player Win Conditions:
function Rule_Player_Wins()
-- Checks to see if the player has a squadron of Scouts
if (Player_GetNumberOfSquadronsOfTypeAwakeOrSleeping( 0, "hgn_scout" ) ) > 0 then
-- Updates the objective
Objective_SetState( obj_prim_newobj_id, OS_Complete )
-- Removes the rule
setMissionComplete( 1 )
end
end
setMissionComplete(1) is, as far as I can tell, the variable used in the postMission function for Mission 1 (in the "ascension.campaign" file).
Player Lose Condition:
function Rule_Player_Loses()
if (Player_GetNumberOfSquadronsOfTypeAwakeOrSleeping( 0, "hgn_interceptor" )) > 0 then
-- Updates the objective
Objective_SetState( obj_prim_newobj_id, OS_Fail )
-- Removes the rule
setMissionComplete( 0 )
end
end
The mission will end as soon as a scout or interceptor is built and is under the control of the player.
The next thing to do is to set up the events. Events are generally used for cut scenes and the like.
Introduction Intel Event:
-- The most important line
Events = {}
Events.IntelEvent_Intro =
{
{
{ "Sound_EnableAllSpeech( 1 )", }, { "Sound_EnterIntelEvent()", },
{ "Universe_EnableSkip(1)", }, HW2_LocationCardEvent( "Postmortem Tutorial", 5 ), }, { HW2_Letterbox( 1 ), HW2_Wait( 2 ), }, { HW2_SubTitleEvent( Actor_FleetCommand, "Welcome to my test mission", 5 ), }, { HW2_Wait( 1 ), }, { { "obj_prim_newobj_id = Objective_Add( 'Scout building', OT_Primary )", },
{ "Objective_AddDescription( obj_prim_newobj_id, 'Description')", }, HW2_SubTitleEvent( Actor_FleetIntel, "Build a scout squadron to win the mission", 4 ), }, { HW2_Wait( 1 ), }, { HW2_Letterbox( 0 ), HW2_Wait( 2 ), { "Universe_EnableSkip(0)", },
{ "Sound_ExitIntelEvent()", }, }, }## Once again, my knowledge is pretty limited when it comes to events, ATM. It's a case of playing around with what is there. **__Step Six: "m02.lua"__** Because I am lazy - and because I really only want to show one more thing in the "mission.lua" files - copy and paste the contents of "m01.lua" into "m02.lua". Then, change: ##-- Makes it easy on the player and gives them fighter production SobGroup_CreateSubSystem("MotherShip", "FighterProduction") -- Tells the Mothership to exit hyperspace SobGroup_ExitHyperSpace ("MotherShip", "Mothership_EnterVolume")## to: ##sobGroup_LoadPersistantData("Hgn_Mothership") Players_Flagship = "PlayerStartPoint" SobGroup_Create(Players_Flagship) SobGroup_FillShipsByType(Players_Flagship, "Player_Ships0", "Hgn_Mothership")## This loads the fleet from the last mission into the current mission. The ship name can be ##hgn_mothership##, ##hgn_battlecruiser## or ##hgn_carrier## (probably works for the Vaygr equivalents, as well, but am not sure). This essentially tells the ships to parade around the named vessel. You'll also need to change the following line so that you can see that it actually happens. Change: ##if (Player_GetNumberOfSquadronsOfTypeAwakeOrSleeping( 0, "hgn_scout" ) ) > 0 then## to: ##if (Player_GetNumberOfSquadronsOfTypeAwakeOrSleeping( 0, "hgn_scout" ) ) > 1 then## At this point you should be able to run the SP campaign with the **-overrideBigFile** switch. Just go to the single player menu and play away. ''Note: if you are creating a new campaign instead of overriding the Ascension campaign, then (in order to play the campaign) you will have to change your Homeworld 2 shortcut so that it looks something like this: ..."Homeworld2\Bin\Release\Homeworld2.exe" -campaign -startingLevel The campaign should then immediately start on the specified level.'' **__Step Seven: Localizing Missions__** Earlier I mentioned the "datfiles.lua" in each of the mission directories. In "M01\datfiles.lua", add: ##Dictionaries = { { name = "locale:campaign\ascension\m01\m01.dat", }, }## In "M02\datfiles.lua", add: ##Dictionaries = { { name = "locale:campaign\ascension\m02\m02.dat", }, }## Now that this is done, we need to go to the "locale\English\campaign\ascension\m01\" directory and create the "m01.dat" file. Add these lines to the file: ##filerange 8000 8999 rangestart 8000 8999 ""Strings for m01.dat
8000 Welcome to my localised test mission.
rangeend
The range 8000 to 8999 is reserved for Community mods - so there is no danger of screwing anything else up.
Now, Fleet Command will say (it won't say it literally) "Welcome to my localized test mission" if we change this line in the "m01.lua":
HW2_SubTitleEvent( Actor_FleetCommand, "Welcome to my test mission", 5 ),
to:
HW2_SubTitleEvent( Actor_FleetCommand, "$8000", 5 ),
However, if the locale is set to French, then Fleet Command will say "$8000", instead. This is because, although we set up the English version of the "m01.dat" file with the $8000 string, we haven't (in this case) set up one for the French language. To sort this out, you need to create another "m01.dat" for each language you want to cater to.
Note: for additional information on creating localizations, refer to the Localizations Tutorial.
Step Eight: Localising the Mission Selection
For each language you are supporting, you need to create a "ui.ucs" file in the "locale\English", "locale\French", "locale\Braille" or "locale\whatever" directory.
However, this file also contains the strings necessary to create all of the Main Menu UI components. If we dont have them then the user will have to navigate around the menu system by memory. To get around this we need to copy the entire "ui.ucs" file and then add our mission selection strings (Mission Name, Description, etc) to the end of the file.
The easiest way is to uncompress "English.big" using the archive tool and then copy/alter the file. To do this, follow the following steps:
Copy the "archive.exe" file to your "Homeworld2\Data" directory.
In a command prompt window, change to your "Homeworld2\Data" directory.
Type: archive -a English.big -e <whereYouWantToExtractItTo>
Then, copy & paste the "ui.ucs" file to the "Data\locale\English" directory.
Open it with Notepad (or whatever text editor you use) and add your data strings at the end.
In our case we add the line:
8000 Witty and informative description of the first mission.
In the "ascension.campaign" file, change:
description = "The first Postmortem mission",
to:
description = "$8000",
If you want any of the other supported languages you are going to have to use the archive tool to unpack the "<languagename>.big" file located in the "Data" directory. However, as far as I can tell, Homeworld 2 only installs one language ".big" file.
Step Nine: Coming Soon
Things Left to Do
Editing the AI
Using ReferenceFleets
Conclusion
I hope this is of help to people thinking about starting a single player campaign. If there are any errors (blatent or not) tell me and I'll fix them.
Related Pages
How to Add Language Localizations to HW2
Comments
Page Status
Updated Formatting? Initial
Updated for HWRM? Initial