skin deep modding guide 1
getting started
Recently, I’ve been playing the fantastic comedy imsim, Skin Deep by Blendo Games. It’s made in a spinoff of the Doom 3 engine, and has a built-in mod loader on the main menu, so I decided to crack open the game’s existing maps and figure out how to make my own! Read on for the things I found out, and how to get started yourself.
My Experimental Mod
I made a bunch of weird little test thingimajigs in a big boxy map as I was learning how the levels worked. You can see a video of it on YouTube here:
As you can see, the map is full of little text nodes explaining how to make bits of it, so you can download the mod yourself and try editing it yourself! Click here to download the mod.
The Guide
This is a little text guide describing how to set up Skin Deep for modding and how to make your first functioning map.
NOTE: This guide is now outdated. Check out the official Blendo guide on Steam, and the in-game Mod Museum!
Requirements
You will need:
- Skin Deep (of course)
- The latest version of DarkRadiant (This is version 3.9.0 at the time of writing)
- A text editor.
Initial Setup
First, we need to make the mod folder so that Skin Deep can load our custom map, any custom textures or script behaviours we need, and so that you can distribute it to other players.
- Go to your Skin Deep installation directory. It’s usually in
<Steam Installation Folder or SteamLibrary folder>/steamapps/common/Skin Deep/
if you have installed the game from Steam. You should see a folder namedbase
in there; that’s the base game, and you can look at all the scripts and assets there for inspiration! - Create a new folder next to
base
named whatever you like; it should probably not have any spaces in the name though. This is your mod folder! - Inside this folder, create two empty files:
description.txt
andpak000.pk4
. - Write a single line of text in
description.txt
; this is how your mod will display in the ingame mod browser.
Setting Up DarkRadiant
Now we need to set up DarkRadiant so that we can make a map for Skin Deep.
- Open DarkRadiant. If it’s your first time using it, you will be prompted to configure the Game Setup.
- Choose “Doom 3” as the Game Type, and use Skin Deep’s installation directory.
- Enter
base
in the “Mod Base” field. - Enter your mod folder’s name in the “Mod” field. This will let DarkRadiant read your map, but also reference all the existing sounds, models, textures, etc from the base game.
- You can perform the above steps using DarkRadiant’s Game Setup menu option if you already use it - but you already know that, if you do!
Creating your first map
- Create a new map in DarkRadiant, and save it inside
mod-folder/maps/
. Name it whatever you like; in this guide, let’s name itfirst_level.map
. - Then, create a new empty file next to it in
mod-folder/maps/
calledfirst_level.script
. - Write the following in that file:
namespace first_level { void main() { sys.println("loaded map script first_level!"); } }
- This file will be used by your map to perform any kind of custom logic, like tracking objectives or performing special scripted actions. You can read some of the base game map scripts for examples of the kinds of things they do!
- Make a simple box room in DarkRadiant. Exactly how to do this is a little outside the scope of this guide, which will focus on Skin Deep specific setups, but the DarkRadiant User Guide and the DarkMod wiki’s Video Tutorials Page do a great job of how to use the editor to make maps for the community-maintained Thief-like game “The Dark Mod”. If you are familiar with Hammer or TrenchBroom, then a lot of those skills will transfer over to DarkRadiant - I only started using it a few days ago myself!
- Edit the
worldspawn
properties for your map, adding/setting the following keys and values:call: first_level::main cryo_briefing: Welcome to my cool level! shipname: First Level shipdesc: Simple Box Freighter
- Create an
info_location
entity somewhere in the room. This will tell the game that this room is a specific place on the ship; a lot of systems depend on locations existing, such as cats being able to tell you where the nearest key is. - Set the
location
value on theinfo_location
entity to name your room. In this tutorial, we’ll call itMain Room
. - And just so it’s not dark in here, add a light to the room. We’ll discuss destructible lights later.
Spawning Nina and Testing
A level is nothing without Nina Pasadena, our favourite deep-freeze insurance agent! Let’s get her out of cryosleep and into action.
- Create a second box room away from the first; this room will hold Nina’s cryo pod interior, so it needs to be unlit and inaccessible to the player during normal gameplay.
- In this room, add an
env_cryointerior
entity. This is where the player will spawn, and the game will automatically set up lights and interactivity for it. - Back in the first box room, create an
env_cryospawn
and place it on one of the walls, so that its back side is flush with the wall. This is where Nina will pop out at the start, and where the player can peek into the level to get a sense of what to do when they emerge. - Set the
env_cryospawn
’starget0
property to point at theenv_cryointerior
you made. Its name by default will beenv_cryointerior_1
. - Now you’re ready to test your map!
- Open Skin Deep, and load your mod from the mods button on the main menu.
- Then, open the developer console with
Shift+Esc
, the`
key on a UK keyboard, or the~
key on a US keyboard. - Type
dmap first_level
and hit enter. Thedmap
command builds lighting and collision for your map - you will need to run it every time you make changes to your map. - You should spawn into the cryo pod and be able to enter the level! Hooray!
Adding Stuff
It’s a little boring to walk around an empty room with no items, cats, pirates, or anything to do. So let’s change that, shall we?
Cats and Keys
Get meowt of here!
- You can add a cat to rescue by creating an
env_catcage
orenv_catcage_b
entity. They both work identically, but they have different voice acting. - Set the
name_cat
property on theenv_catcage
to give this cat a name! In this tutorial, we’ll call themSparkles
. - Don’t forget to add an
item_cat_key
to the level… otherwise, that cat’s never getting out! - The game will automatically track how many cats are in the map, and the player’s progress.
Necessary Machinery
Many of the ship’s machines provide vital gameplay functionality for players. Let’s get the basics set up!
- You’ll need a health station when you slip on a banana peel, get shot, step in glass, get exploded, and so on. Add one by creating an
env_healthstation
entity, and place it so that the little handle is facing outwards from the wall. - It’s really, really easy to lose items - every map needs a Lost and Found. Also, pirate skulls will complain in the debug console if one doesn’t exist, so it might even be necessary to get them to work right. Create one with an
env_lostandfound
entity. - A healthy ship requires frequent repairs - if you damage a vital system in a pirate-jockeying-related accident, you’ll need a way to get it working again. Create a
func_repairhatch
entity and place it on the ceiling. Then, create anaas32_flood_flybot
entity and place it in the air; this will generate pathfinding for flying objects like swordfish and repair bots. - A ship isn’t a ship without an FTL drive. No, really, a bunch of stuff does not work unless you have an FTL drive - this is a holdover from earlier versions of the game. You can place an
env_ftl_mini
outside the ship somewhere to satisfy the requirement if you don’t want a big internal one. - Players will be lost without an in-game map screen, and need a way to save the game. Details on setting up the visual component of the in-game map screen will be given later in this guide, but for now, place an
env_infostation
on a wall. It will automatically track various kinds of objects for you once the visuals are set up, and you can now save the game and request repairs.
Deadly Weaponry
The tools of the trade.
- Many of the game’s useful items are named
moveable_*
. For example, Soap ismoveable_soap
. Place some around and the player can pick them up and hurl them around. Same goes for bananas and pepper. - Loadable weapons like the shotgun (
moveable_item_shotgun
) can be given initial starting ammo. For the shotgun, use theinv_ammo_shells
value to set how many shells it starts with already loaded.
Pilfering Pirates
Let’s get the Numb Bunch involved.
- Create an
aas_flood_48
object on the floor. This will produce navigational data for the enemies to use to navigate your map. - Create a
monster_thug
entity; this is your first pirate! Name them with theirdisplayname
attribute. We’ll call oursStinky
. - You can attach some (but not all) objects to a pirate’s belt with the
def_beltattach1, def_beltattach2, etc
variables. Try attaching a cat key by settingdef_beltattach2
toitem_cat_key
! - Create an
info_enemyspawnpoint
on the floor somewhere. This will be used by your pirates skulls to respawn; they will automatically pathfind to it. - You can also set up patrols for enemies by making their
target0
point to certain path entities, but I haven’t fully experimented with this yet. Without a path target, they will patrol randomly in rooms and occasionally go through doors.
Doors
One room is simply not enough. It’s getting too busy in here with all this stuff! Let’s make some more rooms, and some doors between them.
- Create a second room joining the first; cut a hole in the wall so you can walk between them. Make sure not to leave any gaps that would cause leaks. Also create a second
info_location
and name it! - Add a
func_static
entity, and set itsmodel
variable tomodels/objects/doorframes/frame_b.ase
. Set itsgui
variable toguis/game/doorframe_infopanel.gui
. Place this in the doorway between the rooms. - Create a
func_door_single
entity and place it in the empty area that thefunc_static
provides; it should fit like a glove. - Set the
autodir
value to1
on thefunc_door_single
. That will allow it to open and close correctly. - Set the
autobutton
value to1
if it is not already set. This will let the door spawn buttons on the frame! - Now, here’s a slightly tricky part. Create a brush the that slightly overlaps the doorframe’s size, and make it 1 unit thick so that it can fit inside the door’s model. Give it a
nodraw
texture on all sides, but then set a single side facing out from the door to thevisportal
texture. This is a visportal, and creating these will be very useful for a huge number of reasons. - Place the visportal inside the door so that it completely seals the hole, and sits inside the door model, dividing one room from the other.
- Create an
info_locationSeparator
and place it so that it overlaps with the visportal. This will tell the game how to split the two locations up, and will help with things like vacuum breaches (we’ll get to setting up outer space later). - Run the map with the
dmap
command and you should be able to walk between the two rooms! - Add
env_sign_marquee96
entities on either side at the top of the door frame to provide useful signage to the player. Set thegui_parm0
variable to set the text of the signs.
Vents
Why take the obvious route, when you can take the sneaky one? Let’s make a vent.
- Create a third room. Position it some distance from the first two, so that there’s room to crawl between the rooms. Don’t forget its
info_location
! - Make a tunnel between the third room and whichever room you want to connect it to; it should be 64 units wide, and 32 units tall. If you have corners in your tunnel, make them a little bigger than 64x64x32, to help clue the player in they can turn around, if you like.
- Add a
func_static
entity, and set itsmodel
variable tomodels/objects/grate/grate_hole_64x32.ase
. This is similar to the door frame model, and gives you a nice frame in which to position the vent itself. Put it just inside the tunnel holes, so that it is flat on the outside and has no lip sticking out. - You can also set its
noclipmodel
value to1
; this will prevent the player bumping into it during gameplay, but may allow things like bullets in and out. If you do not set this to 1, you will need to make a tinyclip
brush with a slope that lets the player crawl over the lip of the model. - Create a
func_ventdoor_inter_64x32
. This behaves much like the door. Set itsmovedir
value to-1
. Place it within the vent frame. Place it so that the handle on the texture faces the room and not the vent itself. - Set
gui_parm0
to choose which text to display on the outside, andgui_parm1
for the text on the inside. - Create a thin visportal, like you did for the door, and place it so that it intersects the vent door. You do not need a locationSeparator for vents.
- Make another vent door with a frame and a visportal at the other end of the tunnel for the other room. Boom! Vent time!
- You should now be able to get into and out of the vent when you compile and run your map! But it’s not dusty inside the vent, and you can turn around easily. To solve this, you need to add a brush inside the vent for the length of the tunnel, a little smaller than the vent itself, and assign it to be a
func_confinedtunnel
. Set its texture to theconfined
texture. - Set the
baseangle
value on thefunc_confinedtunnel
to select which direction to confine the player in. This will usually be either90
or0
. - Set its
confined
value to1
. This will restrict the players movement, shadow them, and make their sneeze meter build as long as they remain inside the trigger volume. - If you have corners in your tunnel, or larger spaces that you would like to remain dusty but not constrict the player’s movement, create new
func_confinedtunnel
brushes and set theirconfined
value to0
. They will keep the player shadowed and keep building the sneeze meter, but won’t stop them from turning. - These volumes will also enable vent purges if a pirate sees the player enter a vent.
Vent Barricades, Keypads, Notes
It’s too easy to get between rooms. Let’s add a simple keypad puzzle.
- Edit the
func_ventdoor_inter_64x32
’s variables and setbarricaded
to1
. This will make the vent locked by a barricade when the level starts. - Add an
env_maintpanel_vent
on a wall somewhere. When the player interacts with this, it will unlock the vent! Compile and play the map to see it for yourself. - Seems a bit easy… let’s lock the maintenance panel. Add a
func_keypad
entity near the panel, and set itstarget0
variable to point to the panel. The panel’s name, by default, will beenv_maintpanel_vent_1
. - Then, set the
func_keypad
’scode
variable to a four-letter word, likebees
. - If you play the level now, you have to type in BEES to unlock the panel and then unlock the vent!
- So far so good - but the player won’t know that, so let’s add a note to the wall.
- Create an
env_note_paperwall
entity and put it on a wall somewhere. Write some useful text in thegui_parm0
variable, something like:Hey, numbskulls!\n Don't forget the password: 'BEES'!
- Take care not to use double-quote marks, as they will terminate the text early in-game.
- Great! But the code is the same every time, and in the base game, it isn’t. So what’s up with that?
- Open the
worldspawn
properties, and set the variablekeypad_random
to1
. This will randomize keypad codes every time the level is played! - But now our note doesn’t make sense any more. BEES will not be the password! To fix this, set the note’s
target0
property to point at thefunc_keypad
(by default, it will befunc_keypad_1
). - Edit the
gui_parm0
text, and replace the password with the magic string%s%s%s%s
, so it’ll look like this:Hey, numbskulls!\n Don't forget the password: '%s%s%s%s'!
- And boom. Now you’re cooking!
- Note, the string subtitution doesn’t work unless
keypad_random
is on. So if you use fixed passwords, you’ll have to write the notes manually. - Forget-me-nots work the same way, they just use a string to display the code. If you make one, just set its
gui_parm0
to%s%s%s%s
. - It’s also possible to mangle the order of the displayed code, like showing it backwards; set the
codeindexes
value on a note to any order other than1234
(e.g, backwards would be4321
) and it’ll automatically rearrange the code when displayed. Good for sneaky pirate op-sec..!
Airlocks and Outer Space
Whew, it’s getting kinda cramped in here. Let’s get some fresh air… wait!!!
- There is currently nowhere for Outer Space to exist. Make a large box around the entire map, and set its texture to one of the sky textures, like
textures/skies/sky_pinksky
. We can set up a more complex skybox using aninfo_portalsky
and theskyportal
texture, but that’s not necessary right now. - Now that the ship is safely inside and the map won’t leak, punch open a hole in one of the rooms.
- If you play the game now, you’ll notice that, uh, you can just… walk outside, and there’s air out there, and you can fall down into nothing with gravity that should not be there.
- Let’s fix this. First, create an
info_vacuum
entity outside the ship somewhere. This tells the game that this location has no air. - Next, create an
info_location
out here in space, and set thelocation
value to exactly#str_00000
. This specific localisation key is required to tell the game that this particular vacuum location is actually outer space itself, and not an interior location in the ship with no air; if you don’t have this location set up correctly, Skullsavers will not correctly disappear when thrown out into space, and the lost and found will not work either. - Make the hole in the ship big enough to fit an
env_airlock
object; make sure to rotate it so that the external door faces outside, and not inside the ship. - Seal the edges of the airlock with walls; this may mean you need to make your room a little longer, or otherwise you can have a kind of tube that the airlock sits inside.
- Place thin visportals inside each of the two door areas of the airlock to seal the entrances to it.
- Now when you run the game, you should be able to open the airlock and exit to space correctly! You can fly around, and dunk that pirate’s skull out here!
Windows
Peering out into the void…
- Windows are a little complex compared to airlocks. Create a window-sized hole in one of your rooms, out to space.
- First, create a patch brush in DarkRadiant. This workflow is a little strange compared to the usual brushes, so you may need to consult the manual a bit. I certainly had to! This will make a one-sided rectangle brush.
- Use the Patch Thicken option; depending on the axis that the window will be placed, select thicken on X, or Y. Set the thicken value to 0, and it should make the patch be a two-sided plane; it will have no depth, but will be solid on two of its sides.
- Place this patch so that it completely fills the window area. Give it one of the glass textures; I recommend
glass1_spacewindow_filter_subtle
so that it’s easy to see through. - Set this patch to be a
func_fracture
entity. - Next, create an
info_vacuumSeparator
entity, and position it so that it overlaps thefunc_fracture
. - Create a visportal, and make sure it wraps around the
func_fracture
and overlaps theinfo_vacuumSeparator
. - Create a thicker brush that surrounds all of these other brushes. Set the texture to
windowseal01_interior
for the face facing into the room, andwindowseal01_exterior
for the face facing outside the ship. - Set that brush to be a
func_windowseal
entity. - Now the tricky part. Make the
func_fracture
’starget0
target theinfo_vacuumSeparator
, and itstarget1
target thefunc_windowseal
. Then, make thefunc_windowseal
’starget0
target theinfo_vacuumSeparator
, and itstarget1
target thefunc_fracture
. Whew..! - Now, when you play, you should be able to see out of the window and break it with something to vent the room to space. If everything is OK on your map, only that room should be vented to space, and other rooms (which you can visit with a vent or with the
noclip
command) should still function normally. You may need to adjust the position of the seal, visportal, and func_fracture to get things to render and behave correctly. I don’t know the exact details, but sometimes it doesn’t work quite right and you have to nudge things around.
Signal Lamps and Pirate Invasion
So, we can rescue cats and dunk pirates. But how do we finish the mission? Let’s get the level’s last setpiece working.
- Create an
env_signalkit
and put it somewhere in your map. Inside, outside, your choice! Usually the base game puts them inside near airlocks and outside near airlocks, windows, and trash chutes. If Outer Space is set up correctly, you should be able to summon a pod the usual way. But first… - Create a closed-off space in your map, like we did way back for the cryopod, then put an
env_catpod_interior
inside it. This will automatically populate with weapons for the invasion phase, and will appear when you summon the pod with the signal lamp. - Now for the pirates. Create a
func_pirateship
somewhere in your level; one of the corners in space is the best idea. It won’t be visible to the player when the game starts, so don’t worry if it would be seen from somewhere. - Create a NURBS curve in DarkRadiant. This is a special spline object; you can fill in an arbitrary number of control points in its
curve_Nurbs
field. Create a path from the pirate ship’s spawn location to a point about 128 units outside your airlock. - Set the curve to be a
func_splinemover
entity. - Create yet another locked-off, unlit area in your map. Make this one far away from the ship - outside of the skybox, even. This is where you’ll place the pirates who come aboard the ship in the invasion; you can use
monster_engineer
for tougher pirates that use swordfish, for example. Put a pirate in here, and set theirhide
variable to1
. - Set
target0
on thefunc_pirateship
to point to yourfunc_splinemover
NURBS curve. - Set
target1
on thefunc_pirateship
to point to the reinforcement pirate inside the dark box. - Set
target2
on thefunc_pirateship
to point to the airlock. - If all went well, when you summon the catpod and rescue the cats, the pirate ship will spawn, travel along the spline curve, and then dock at the airlock. Once docked, it will move the pirate from the locked-off area to the interior of the ship, and they will move through the airlock.
- And that’s it! Your level is done, basically! You can now get the glorious “NINAAAAA!” chant after disposing of the invading pirates! Just one last thing…
The Interactive Map
Information superhighway…
- To get the infoscreen map and the player’s wrist-computer map to work, there’s a little setup that needs to be done for your map.
- First add two
target_null
entities in your map calledloc_topleft
andloc_bottomright
. - Position them so that they form the top left and bottom right corners of a square that covers the boundary of your ship’s floorplan (this is easiest to do from the top view in DarkRadiant). It must be a square, not a rectangle! The game will tell you in the console if the sides aren’t the same length.
- Add two properties to your worldspawn called
map_topleft
andmap_bottomright
, and give them the valuesloc_topleft
andloc_bottomright
respectively. - Add a
.tga
format texture for your map display toguis/assets/mapguis
inside your mod folder. The top-left and bottom-right pixels will be relative to your top-left and bottom-right location markers. Look at the base game maps for examples. - Create a
materials/mapgui.mtr
file inside your mod folder. Using the base game’s file as an example, add a new entry for your new map, referencing the new texture you just made. - In the worldspawn, add a new property called
mtr_image
and make the value the name of the entry inmapgui.mtr
you just made. - Close and re-open Skin Deep if it’s running, to refresh the image cache when you load your mod.
Loading your map from the Menu
It’d kinda suck if players had to open the devconsole to play your map, right?
- Add a new file to your mod folder:
def/maps.def
. - Write the following in it:
mapDef first_level { "name" "first_level" "description" "First Level" "singleplayer" "1" "levelindex" "1" "internalname" "first_level" "levelimage" "" "size0" "805702930" "size1" "805702930" "size2" "805702930" "size3" "805702930" }
Note: the size*
values here relate to the estimated time the loading screen should take, to help inform the progress bar. There is a warning against altering this value in the pre-existing test_scout
map by level designer and Skin Deep developer Suzanne Will, so I definitely won’t touch it!
All Done!
And that’s as far as I’ve learned! There’s so much more to find out - security cameras, turrets, pirate loadouts, window barricades, door keycards… get out there and make something awesome! You can share your map by zipping up your mod folder and giving out to people. At the time of writing, I haven’t yet found the workshop upload tools, but they’re probably not too hard to figure out somewhere.
Thank you for reading this guide. If it helped you, please give this guide a mention when you upload a cool map for other people to play! Adios!