Chapter 5.3: Improved Data Types - Enhanced Entities
Every entity you interact with in Portal 2—doors, cubes, buttons, the player—is represented by a CBaseEntity handle. Vanilla VScript provides basic methods, but manipulating entities often requires verbose, error-prone code. PCapture-Lib's pcapEntity wrapper transforms entity handles into powerful, easy-to-use objects.
The Problem: Vanilla Entity Code is Clumsy
Let's look at a simple task: make a cube invisible, red, and disable its collision.
// Vanilla VScript
local cube = Entities.FindByName(null, "my_cube")
if (cube) {
EntFireByHandle(cube, "Alpha", "0", 0, null, null) // Make invisible
EntFireByHandle(cube, "Color", "255 0 0", 0, null, null) // Make red
EntFireByHandle(cube, "DisableDraw", "", 0, null, null) // Disable rendering
EntFireByHandle(cube, "SetSolidType", "0", 0, null, null) // Disable collision
}
This works, but it's hard to read, hard to remember, and easy to get wrong. What if cube is null? What if you typo an input name? Errors only appear at runtime, often cryptically.
The Solution: pcapEntity
pcapEntity is a wrapper that extends CBaseEntity with intuitive methods, built-in validation, and chaining support.
// With PCapture-Lib
local cube = entLib.FindByName("my_cube")
if (cube) {
cube.SetAlpha(0) // Make invisible
cube.SetColor("255 0 0") // Make red
cube.SetDrawEnabled(false) // Disable rendering
cube.SetCollision(0) // Disable collision
}
Not only is this shorter, but each method is self-documenting. You immediately understand what's happening without consulting documentation.
All Vanilla Methods Still Work: pcapEntity is a wrapper, not a replacement. You can still call standard CBaseEntity methods like GetOrigin(), GetAngles(), and GetClassname(). PCapture simply adds more.
How to Get a pcapEntity
PCapture-Lib provides the entLib class for finding and creating entities. All entLib methods return pcapEntity objects.
// Finding entities (covered in detail in Chapter 5.4)
local door = entLib.FindByName("exit_door")
local cube = entLib.FindByClassname("prop_weighted_cube")
local nearbyButton = entLib.FindByNameWithin("button", playerPos, 100)
// Wrapping an existing CBaseEntity
local vanillaEntity = Entities.FindByName(null, "something")
local wrapped = entLib.FromEntity(vanillaEntity)
// GetPlayerEx returns a pcapEntity
local player = GetPlayerEx() // Enhanced player handle
Categories of pcapEntity Methods
The pcapEntity class has over 80 methods. They're organized into logical categories:
1. Appearance
cube.SetColor("0 255 0") // Green
cube.SetAlpha(128) // Semi-transparent
cube.SetSkin(2) // Change texture variant
cube.SetModelScale(2.0) // Double size
cube.SetDrawEnabled(false) // Make invisible
// Get current values
local currentColor = cube.GetColor() // Returns: "0 255 0"
local currentAlpha = cube.GetAlpha() // Returns: 128
2. Transform & Hierarchy
// Position
cube.SetOrigin(Vector(0, 0, 100))
cube.SetCenter(Vector(0, 0, 100)) // Set by bounding box center
cube.SetAbsCenter(Vector(0, 0, 100)) // Ignore parent hierarchy
// Rotation
cube.SetAngles(0, 90, 0) // Pitch, Yaw, Roll
cube.SetAngles2(Vector(0, 90, 0)) // Same, but using Vector
// Parenting
cube.SetParent(door)
local children = door.GetChildren() // Direct children only
local allDescendants = door.GetAllChildrenRecursively() // Recursive
3. Lifecycle & State
// Validation
if (entity.IsValid()) {
// Entity exists and hasn't been destroyed
}
if (entity.IsPlayer()) {
// This is the player entity
}
// Destruction
entity.Kill() // Trigger "Kill" input
entity.Dissolve() // Fancy dissolve effect
entity.Destroy() // Immediate removal
// Enable/Disable
entity.Disable() // Makes invisible + non-solid
entity.Enable() // Reverses Disable()
4. Collision & Physics
// Collision
entity.SetCollision(SOLID_NONE) // No collision
entity.SetCollisionGroup(COLLISION_GROUP_DEBRIS)
entity.SetTraceIgnore(true) // Traces ignore this entity
// Bounding Box
entity.SetBBox(Vector(-10,-10,-10), Vector(10,10,10))
local aabb = entity.GetAABB() // { min, max, center }
local isCube = entity.IsSquareBbox() // Check if all sides equal
5. Player-Specific Methods
local player = GetPlayerEx()
// View information
local eyePos = player.EyePosition()
local eyeAngles = player.EyeAngles()
local lookDir = player.EyeForwardVector()
// Calculate where the player is looking
local lookTarget = eyePos + (lookDir * 1000)
6. Sound (Enhanced)
// Standard sound
entity.EmitSound("Portal.button_down")
// Enhanced sound with volume control and looping
entity.EmitSoundEx("ambient/alarms/alarm1.wav", 5, true) // Vol 5, looped
// Stop a looped sound
entity.StopSoundEx("ambient/alarms/alarm1.wav")
How EmitSoundEx Works: This method creates a hidden prop_physics attached to your entity and uses it as a sound emitter. Volume is controlled by adjusting the hidden prop's Z-position (distance attenuation). Looping is achieved by scheduling repeated EmitSound calls.
7. Outputs & Inputs
// Add an output
button.AddOutput("OnPressed", door, "Open", "", 0, -1)
// Connect output to a function
button.ConnectOutputEx("OnPressed", function() {
printl("Button was pressed!")
})
// Hook an input (intercept and optionally cancel)
door.SetInputHook("Close", function() {
printl("Attempted to close door")
return false // Cancel the close action
})
8. User Data & KeyValues
// Store arbitrary data on any entity
cube.SetUserData("puzzle_id", 42)
cube.SetUserData("custom_state", { active = true, count = 10 })
// Retrieve it later
local puzzleID = cube.GetUserData("puzzle_id") // 42
local state = cube.GetUserData("custom_state") // { active = true, count = 10 }
// Set entity keyvalues
cube.SetKeyValue("health", 100)
cube.SetKeyValue("targetname", "special_cube")
Delayed Execution: Built-in Timing
Many pcapEntity methods accept optional fireDelay and eventName parameters, allowing you to schedule actions without manual EntFire calls.
local cube = entLib.FindByName("my_cube")
// Execute immediately
cube.SetColor("255 0 0")
// Execute after 2 seconds
cube.SetColor("0 255 0", 2)
// Execute after 2 seconds, tied to a specific event scheduler
cube.SetColor("0 0 255", 2, "puzzle_sequence")
The eventName parameter ties the scheduled action to PCapture's ActionScheduler (covered in Chapter 5.5), allowing for coordinated timing and cancellation.
Practical Example: Smart Door Controller
SmartDoor <- class {
door = null
isLocked = true
openSound = "doors/door_metal_medium_open1.wav"
closeSound = "doors/door_metal_medium_close1.wav"
function Init(doorName) {
door = entLib.FindByName(doorName)
if (!door) {
printl("ERROR: Door '" + doorName + "' not found!")
return false
}
// Set initial state
door.SetColor("255 0 0") // Red when locked
door.SetUserData("door_controller", this)
// Hook the "Open" input to enforce lock
door.SetInputHook("Open", function() {
if (isLocked) {
printl("Door is locked!")
return false // Cancel the open
}
return true // Allow it
}.bindenv(this))
return true
}
function Unlock() {
if (!isLocked) return
isLocked = false
door.SetColor("0 255 0", 0.5) // Fade to green over 0.5s
door.EmitSound("buttons/button9.wav")
printl("Door unlocked!")
}
function Open() {
if (isLocked) {
printl("Cannot open locked door!")
return
}
door.AddOutput("OnFullyOpen", "!self", "RunScriptCode", "GetUserData('door_controller').OnFullyOpen()", 0, 1)
EntFireByHandle(door, "Open", "", 0, null, null)
door.EmitSound(openSound)
}
function OnFullyOpen() {
printl("Door is now fully open.")
// Auto-close after 5 seconds
EntFireByHandle(door, "Close", "", 5, null, null)
}
}
// Usage
local mainDoor = SmartDoor
mainDoor.Init("exit_door")
// Later, when puzzle is solved:
mainDoor.Unlock()
mainDoor.Open()
Important Limitations & Warnings
Method Chaining is Not Supported: Unlike some libraries, you cannot chain pcapEntity methods. Each method call is a separate statement:
// ❌ This will NOT work
cube.SetColor("255 0 0").SetAlpha(128).SetOrigin(Vector(0,0,100))
// ✅ Do this instead
cube.SetColor("255 0 0")
cube.SetAlpha(128)
cube.SetOrigin(Vector(0,0,100))
Best Practices
- Always use
GetPlayerEx()instead ofGetPlayer()for the enhanced entity - Use
entLibmethods for finding entities to getpcapEntityobjects automatically - Validate entities with
.IsValid()before using them - ⚠️ Remember that delayed methods use the ActionScheduler—understand its lifecycle