> For the complete documentation index, see [llms.txt](https://docs.nursultan.fun/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.nursultan.fun/scripting-api-ru/primery/esp-cherez-steny.md).

# ESP через стены

Подсвечивает игроков коробками, тянет к ним трейсеры и подписывает здоровье. Пример того, как правильно делить работу между тиком и кадром.

```kotlin
name("ESP")
description("Подсвечивает игроков сквозь стены")

key(Key.R)

val range = slider("Дальность", 64, 8, 128)
val boxes = checkBox("Коробки", true)
val tracers = checkBox("Трейсеры", true)
val names = checkBox("Подписи", true)
val skipAllies = checkBox("Не трогать своих", true)
val color = colorPicker("Цвет", Colors.rgba(255, 85, 85, 255))

var targets: List<Entity> = emptyList()

onEnable { targets = emptyList() }
onDisable { targets = emptyList() }

on<ClientTickEvent> {
    whenInGame {
        targets = world.entitiesNear(player.position(), range.value().toDouble()) { target ->
            target.isPlayer()
                && target.alive()
                && !target.isSelf()
                && !(skipAllies.value() && target.isAlly())
        }
    }
}

on<Render3DEvent> { e ->
    val r = e.render()
    for (target in targets) {
        if (boxes.value()) {
            r.entityBox(target, color.value(), true)
        }
        if (tracers.value()) {
            val middle = target.renderPosition().add(0.0, target.height() / 2.0, 0.0)
            r.tracer(middle, color.value(), true)
        }
    }
}

on<Render2DEvent> { e ->
    if (!names.value()) return@on
    val r = e.render()
    for (target in targets) {
        val living = target.asLiving() ?: continue
        val head = target.renderPosition().add(0.0, target.height() + 0.35, 0.0)
        val point = r.project(head)
        if (!point.visible()) continue

        val text = target.name() + "  " + living.health().toInt()
        val width = r.textWidth(text, 8f)
        val left = point.x() - width / 2f
        r.roundedRect(left - 3f, point.y() - 2f, width + 6f, 12f, 3f, Colors.rgba(0, 0, 0, 140))
        r.text(text, left, point.y(), 8f, Colors.WHITE)
    }
}
```

## Что тут стоит заметить

**Поиск целей — раз в тик, рисование — каждый кадр.** `world.entitiesNear` в обработчике отрисовки означал бы обход мира 60+ раз в секунду. Список собирается в `ClientTickEvent` и лежит в переменной.

**`renderPosition()`, а не `position()`.** Первая уже сглажена между тиками, поэтому коробки и подписи не дёргаются.

**Подписи — это HUD, а не мир.** Текст в 3D не рисуется. Позиция над головой переводится в экранные координаты через `project`, а нарисовать её нужно уже в `Render2DEvent`.

**`visible()` обязателен.** Без него точки, оказавшиеся за спиной, нарисуются в случайных местах экрана.

**Список чистится при выключении.** Иначе после выключения в `targets` останутся сущности из старого мира.
