Загрузка данных
from ursina import *
import random
app = Ursina()
# --- ОКРУЖЕНИЕ И НАСТРОЙКА ЭКРАНА ---
window.title = "3D Battle Tanks"
window.borderless = False
window.fullscreen = False
window.fps_counter.enabled = True
# Небо и мягкое освещение
Sky(color=color.rgb(135, 185, 235))
DirectionalLight(y=20, rotation=(45, -30, 20), shadows=True)
AmbientLight(color=color.rgb(100, 105, 115))
MAP_RADIUS = 70
# Контрастная земля с сеткой для ощущения масштаба
ground = Entity(
model='plane',
scale=(MAP_RADIUS * 2, 1, MAP_RADIUS * 2),
color=color.rgb(55, 75, 45),
texture='white_cube',
texture_scale=(MAP_RADIUS, MAP_RADIUS),
collider='box'
)
# Границы карты
for angle in range(0, 360, 45):
rad = math.radians(angle)
x = math.cos(rad) * MAP_RADIUS
z = math.sin(rad) * MAP_RADIUS
Entity(model='cube', position=(x, 2, z), scale=(15, 5, 2), rotation_y=-angle,
color=color.rgb(100, 40, 40), collider='box')
# Разрушаемые бункеры и кирпичные стены
destructibles = []
for _ in range(25):
rx = random.uniform(-MAP_RADIUS + 15, MAP_RADIUS - 15)
rz = random.uniform(-MAP_RADIUS + 15, MAP_RADIUS - 15)
if abs(rx) > 10 or abs(rz) > 10:
bunker = Entity(
model='cube',
position=(rx, 1.5, rz),
scale=(random.choice([4, 6]), 3, random.choice([4, 6])),
color=color.rgb(130, 95, 65),
texture='white_cube',
collider='box'
)
bunker.hp = 3
destructibles.append(bunker)
# --- СПЕЦЭФФЕКТЫ ---
def spawn_fx(pos, scale=2.0, tint=color.orange):
p = Entity(model='sphere', position=pos, scale=0.3, color=tint)
p.animate_scale(scale, duration=0.2, curve=curve.out_expo)
p.animate_color(color.clear, duration=0.2)
destroy(p, delay=0.25)
# --- СНАРЯД ---
class Shell(Entity):
def __init__(self, pos, direction, is_player=True):
super().__init__(
model='sphere',
scale=0.35,
color=color.yellow if is_player else color.red,
position=pos,
collider='sphere'
)
self.direction = direction.normalized()
self.is_player = is_player
self.speed = 65
self.life = 2.5
def update(self):
self.position += self.direction * self.speed * time.dt
self.life -= time.dt
if self.life <= 0:
destroy(self)
return
hit = self.intersects()
if hit.hit and hit.entity != ground:
ent = hit.entity
spawn_fx(self.position, scale=2.5, tint=color.orange)
# Урон постройкам
if ent in destructibles:
ent.hp -= 1
ent.color = color.rgb(180, 70, 50)
if ent.hp <= 0:
destructibles.remove(ent)
destroy(ent)
destroy(self)
return
# Урон врагам
if self.is_player and hasattr(ent, 'tank_ref') and isinstance(ent.tank_ref, EnemyTank):
ent.tank_ref.take_damage(1)
destroy(self)
return
# Урон игроку
if not self.is_player and hasattr(ent, 'tank_ref') and ent.tank_ref == player:
player.take_damage(1)
destroy(self)
return
destroy(self)
# --- БАЗОВЫЙ СОСТАВНОЙ ТАНК ---
class DetailedTank(Entity):
def __init__(self, pos, camo_color):
super().__init__(position=pos)
self.max_hp = 3
self.hp = 3
# Корпус
self.hull = Entity(parent=self, model='cube', scale=(3.2, 0.75, 4.8), y=0.6,
color=camo_color, texture='white_cube')
self.hull.collider = 'box'
self.hull.tank_ref = self
# Левая и правая гусеницы с фальшбортами
for side in (-1.7, 1.7):
Entity(parent=self, model='cube', scale=(0.7, 0.9, 5.2), position=(side, 0.45, 0),
color=color.rgb(30, 30, 30))
# Катки
for z in (-1.8, -0.9, 0, 0.9, 1.8):
Entity(parent=self, model='cylinder', scale=(0.4, 0.72, 0.4), rotation_z=90,
position=(side, 0.4, z), color=color.rgb(70, 70, 70))
# Башня
self.turret = Entity(parent=self, model='cube', scale=(2.2, 0.65, 2.6), y=1.2,
color=camo_color, texture='white_cube')
self.turret.collider = 'box'
self.turret.tank_ref = self
# Маска орудия + командирская башенка
Entity(parent=self.turret, model='cube', scale=(0.8, 0.4, 0.5), position=(0, 0.05, 1.3), color=color.black)
Entity(parent=self.turret, model='cylinder', scale=(0.6, 0.3, 0.6), position=(0.5, 0.4, -0.4), color=color.dark_gray)
# Ствол орудия и дульный тормоз
self.barrel = Entity(parent=self.turret, model='cylinder', scale=(0.22, 3.2, 0.22),
rotation_x=90, position=(0, 0.05, 2.8), color=color.rgb(20, 20, 20))
Entity(parent=self.barrel, model='cube', scale=(1.8, 0.25, 1.5), position=(0, 1.55, 0), color=color.black)
# Индикатор здоровья (над башней)
self.hp_bar_bg = Entity(parent=self, model='quad', scale=(2.0, 0.2), position=(0, 2.8, 0),
billboard=True, color=color.black)
self.hp_bar = Entity(parent=self.hp_bar_bg, model='quad', scale=(1.0, 0.8), position=(0, 0, -0.01),
color=color.green)
def trigger_recoil(self):
# Анимация отката ствола и пороховой вспышки
muzzle_world = self.barrel.world_position + self.turret.forward * 1.8
spawn_fx(muzzle_world, scale=1.2, tint=color.yellow)
self.barrel.z = 2.4
self.barrel.animate('z', 2.8, duration=0.2, curve=curve.out_bounce)
def update_hp_bar(self):
ratio = max(0, self.hp / self.max_hp)
self.hp_bar.scale_x = ratio
if ratio > 0.5:
self.hp_bar.color = color.green
elif ratio > 0.25:
self.hp_bar.color = color.orange
else:
self.hp_bar.color = color.red
# --- ТАНК ИГРОКА ---
class PlayerTank(DetailedTank):
def __init__(self):
super().__init__(pos=(0, 0, -35), camo_color=color.rgb(40, 110, 50))
self.speed = 10
self.turn_speed = 65
self.shoot_cd = 0.0
def update(self):
if self.hp <= 0:
return
# Управление корпусом
move = held_keys['w'] - held_keys['s']
turn = held_keys['d'] - held_keys['a']
self.rotation_y += turn * self.turn_speed * time.dt
if move != 0:
new_pos = self.position + self.forward * move * self.speed * time.dt
if new_pos.length() < MAP_RADIUS - 3:
self.position = new_pos
# Наведение башни по курсору мыши (луч в плоскость Y = высоте башни)
hit = mouse.world_point
if hit:
target = Vec3(hit.x, self.turret.world_y, hit.z)
self.turret.look_at(target)
# Камера третьего лица (следует плавно, не вращаясь рывками вместе с башней)
target_cam_pos = self.position - self.forward * 14 + Vec3(0, 7.5, 0)
camera.position = lerp(camera.position, target_cam_pos, time.dt * 6)
camera.look_at(self.position + Vec3(0, 1.5, 0))
# Стрельба
if self.shoot_cd > 0:
self.shoot_cd -= time.dt
if held_keys['left mouse button'] and self.shoot_cd <= 0:
self.shoot()
def shoot(self):
self.shoot_cd = 0.45
self.trigger_recoil()
muzzle_pos = self.barrel.world_position + self.turret.forward * 2.0
Shell(muzzle_pos, self.turret.forward, is_player=True)
def take_damage(self, dmg):
self.hp -= dmg
self.update_hp_bar()
self.hull.color = color.red
self.hull.animate_color(color.rgb(40, 110, 50), duration=0.2)
if self.hp <= 0:
spawn_fx(self.position, scale=5.0)
ui_status.text = "ТАНК ПОДБИТ! Нажмите R для рестарта"
ui_status.color = color.red
# --- ВРАЖЕСКИЙ ТАНК ---
enemies = []
class EnemyTank(DetailedTank):
def __init__(self, pos):
super().__init__(pos=pos, camo_color=color.rgb(140, 45, 45))
self.speed = 4.5
self.ai_cd = random.uniform(1.0, 2.5)
self.shoot_cd = random.uniform(2.0, 4.0)
self.target_dir = random.choice([0, 90, 180, 270])
def update(self):
if player.hp <= 0:
return
dist = distance(self.position, player.position)
# Башня всегда держит игрока на мушке
self.turret.look_at(player.position + Vec3(0, 1.2, 0))
# Перемещение
self.ai_cd -= time.dt
if self.ai_cd <= 0:
self.target_dir = random.choice([0, 90, 180, 270])
self.ai_cd = random.uniform(2.0, 4.0)
self.rotation_y = lerp(self.rotation_y, self.target_dir, time.dt * 2)
if dist > 14:
new_pos = self.position + self.forward * self.speed * time.dt
if new_pos.length() < MAP_RADIUS - 4:
self.position = new_pos
# Огонь
self.shoot_cd -= time.dt
if self.shoot_cd <= 0 and dist < 50:
self.shoot_cd = random.uniform(2.2, 4.0)
self.trigger_recoil()
muzzle_pos = self.barrel.world_position + self.turret.forward * 2.0
Shell(muzzle_pos, self.turret.forward, is_player=False)
def take_damage(self, dmg):
self.hp -= dmg
self.update_hp_bar()
self.hull.color = color.white
self.hull.animate_color(color.rgb(140, 45, 45), duration=0.2)
if self.hp <= 0:
spawn_fx(self.position, scale=4.0)
if self in enemies:
enemies.remove(self)
destroy(self)
global score
score += 1
ui_score.text = f"Уничтожено: {score}"
# Инициализация сущностей
player = PlayerTank()
camera.position = player.position + Vec3(0, 10, -15)
for ep in [(-30, 0, 25), (0, 0, 35), (30, 0, 20), (20, 0, 45)]:
enemies.append(EnemyTank(pos=ep))
# Интерфейс
score = 0
ui_score = Text(text="Уничтожено: 0", position=(-0.85, 0.45), scale=1.4, color=color.yellow)
ui_status = Text(text="", position=(-0.4, 0.1), scale=1.8, color=color.red)
Text(text="W/S - Ход | A/D - Поворот корпуса | Мышь - Прицел | ЛКМ - Залп",
position=(-0.85, -0.45), scale=1.0, color=color.white)
def input(key):
if key == 'r' and player.hp <= 0:
scene.clear()
app.restart()
app.run()