Загрузка данных
from ursina import *
import random
app = Ursina()
# --- ОКРУЖЕНИЕ И ОСВЕЩЕНИЕ ---
window.title = "3D Tanks Prototype"
window.borderless = False
window.fullscreen = False
window.fps_counter.enabled = True
# Большая карта (арена 120x120)
MAP_SIZE = 120
ground = Entity(
model='plane',
scale=(MAP_SIZE, 1, MAP_SIZE),
color=color.rgb(60, 70, 50),
texture='white_cube',
texture_scale=(MAP_SIZE, MAP_SIZE),
collider='box'
)
DirectionalLight(y=10, rotation=(45, -45, 45), shadows=False)
AmbientLight(color=color.rgba(120, 120, 120, 255))
# Границы карты (невидимые стены)
for x, z, sx, sz in [
(0, MAP_SIZE/2, MAP_SIZE, 2),
(0, -MAP_SIZE/2, MAP_SIZE, 2),
(MAP_SIZE/2, 0, 2, MAP_SIZE),
(-MAP_SIZE/2, 0, 2, MAP_SIZE)
]:
Entity(model='cube', position=(x, 2, z), scale=(sx, 4, sz), collider='box', visible=False)
# Разрушаемые укрытия и препятствия
obstacles = []
for _ in range(35):
ox = random.uniform(-MAP_SIZE/2 + 10, MAP_SIZE/2 - 10)
oz = random.uniform(-MAP_SIZE/2 + 10, MAP_SIZE/2 - 10)
if abs(ox) > 8 or abs(oz) > 8: # освобождаем спавн
obs = Entity(
model='cube',
position=(ox, 1.5, oz),
scale=(random.choice([3, 4]), 3, random.choice([3, 4])),
color=color.rgb(130, 90, 60),
texture='white_cube',
collider='box'
)
obs.hp = 2
obstacles.append(obs)
# --- ПУЛИ И ВЗРЫВЫ ---
bullets = []
def create_explosion(pos, color_tint=color.orange):
exp = Entity(model='sphere', position=pos, scale=0.5, color=color_tint)
exp.animate_scale(3.5, duration=0.25, curve=curve.out_expo)
exp.animate_color(color.clear, duration=0.25)
destroy(exp, delay=0.3)
class Shell(Entity):
def __init__(self, pos, direction, is_player=True):
super().__init__(
model='sphere',
scale=0.3,
color=color.yellow if is_player else color.red,
position=pos,
collider='sphere'
)
self.direction = direction
self.is_player = is_player
self.lifetime = 3.0
bullets.append(self)
def update(self):
self.position += self.direction * 50 * time.dt
self.lifetime -= time.dt
if self.lifetime <= 0:
bullets.remove(self)
destroy(self)
return
hit_info = self.intersects()
if hit_info.hit and hit_info.entity != ground:
ent = hit_info.entity
# Попадание в укрытия
if ent in obstacles:
ent.hp -= 1
create_explosion(self.position, color.gray)
if ent.hp <= 0:
obstacles.remove(ent)
destroy(ent)
self.destroy_shell()
return
# Попадание по врагу
if self.is_player and isinstance(ent, EnemyTank):
ent.take_damage(1)
self.destroy_shell()
return
# Попадание по игроку
if not self.is_player and ent in [player.hull, player.turret, player.chassis]:
player.take_damage(1)
self.destroy_shell()
return
def destroy_shell(self):
create_explosion(self.position)
if self in bullets:
bullets.remove(self)
destroy(self)
# --- БАЗОВАЯ МОДЕЛЬ ТАНКА ---
class BaseTank(Entity):
def __init__(self, pos=(0, 0, 0), primary_color=color.rgb(40, 110, 40)):
super().__init__(position=pos)
# Гусеничная база
self.chassis = Entity(parent=self, model='cube', scale=(2.6, 0.5, 3.8), y=0.25, color=color.dark_gray)
# Корпус
self.hull = Entity(parent=self, model='cube', scale=(2.4, 0.7, 3.4), y=0.7, color=primary_color)
# Башня
self.turret = Entity(parent=self, model='cube', scale=(1.6, 0.6, 1.8), y=1.2, color=primary_color)
# Дуло
self.cannon = Entity(parent=self.turret, model='cylinder', scale=(0.25, 2.5, 0.25),
rotation_x=90, z=1.4, y=0.0, color=color.black)
self.recoil_anim = False
self.hp = 3
def trigger_recoil(self):
# Анимация отдачи ствола
self.cannon.z = 0.9
self.cannon.animate('z', 1.4, duration=0.15, curve=curve.out_bounce)
# --- ИГРОК С КАМЕРОЙ ОТ 3-ГО ЛИЦА ---
class PlayerTank(BaseTank):
def __init__(self):
super().__init__(pos=(0, 0, -30), primary_color=color.rgb(45, 120, 60))
self.speed = 9
self.turn_speed = 70
self.shoot_cd = 0.0
# Камера 3-го лица
camera.parent = self
camera.position = (0, 7, -13)
camera.rotation_x = 22
def update(self):
if self.hp <= 0:
return
# Управление движением (W/S — вперед/назад, A/D — поворот корпуса)
move_dir = held_keys['w'] - held_keys['s']
turn_dir = held_keys['d'] - held_keys['a']
self.rotation_y += turn_dir * self.turn_speed * time.dt
if move_dir != 0:
next_pos = self.position + self.forward * move_dir * self.speed * time.dt
# Ограничение по границам арены
if abs(next_pos.x) < MAP_SIZE/2 - 2 and abs(next_pos.z) < MAP_SIZE/2 - 2:
self.position = next_pos
# Поворот башни курсором через луч на плоскость земли
hit = mouse.world_point
if hit:
target = Vec3(hit.x, self.turret.world_y, hit.z)
self.turret.look_at(target)
if self.shoot_cd > 0:
self.shoot_cd -= time.dt
if held_keys['left mouse button'] and self.shoot_cd <= 0:
self.fire()
def fire(self):
self.shoot_cd = 0.5
self.trigger_recoil()
barrel_tip = self.cannon.world_position + self.turret.forward * 1.5
Shell(barrel_tip, self.turret.forward, is_player=True)
def take_damage(self, dmg):
self.hp -= dmg
self.hull.color = color.red
self.hull.animate_color(color.rgb(45, 120, 60), duration=0.2)
if self.hp <= 0:
create_explosion(self.position)
print_on_screen("ТАНК УНИЧТОЖЕН! Нажмите R для перезапуска", position=(-0.4, 0), scale=2, duration=10)
player = PlayerTank()
# --- ВРАЖЕСКИЕ ТАНКИ С AI ---
enemies = []
class EnemyTank(BaseTank):
def __init__(self, pos):
super().__init__(pos=pos, primary_color=color.rgb(150, 40, 40))
self.collider = 'box'
self.speed = 4
self.ai_timer = random.uniform(1.0, 3.0)
self.shoot_timer = random.uniform(2.0, 4.0)
self.target_dir = random.choice([0, 90, 180, 270])
def update(self):
if player.hp <= 0:
return
dist_to_player = distance(self.position, player.position)
# Поведение: смотрим башней на игрока
self.turret.look_at(player.position + Vec3(0, 1, 0))
# Патрулирование / приближение
self.ai_timer -= time.dt
if self.ai_timer <= 0:
self.target_dir = random.choice([0, 90, 180, 270])
self.ai_timer = random.uniform(2.0, 4.0)
self.rotation_y = lerp(self.rotation_y, self.target_dir, time.dt * 2)
if dist_to_player > 12:
self.position += self.forward * self.speed * time.dt
# Стрельба в игрока
self.shoot_timer -= time.dt
if self.shoot_timer <= 0 and dist_to_player < 40:
self.shoot_timer = random.uniform(2.5, 4.5)
self.trigger_recoil()
barrel_tip = self.cannon.world_position + self.turret.forward * 1.5
Shell(barrel_tip, self.turret.forward, is_player=False)
def take_damage(self, dmg):
self.hp -= dmg
self.hull.color = color.white
self.hull.animate_color(color.rgb(150, 40, 40), duration=0.2)
if self.hp <= 0:
create_explosion(self.position)
if self in enemies:
enemies.remove(self)
destroy(self)
# Спавн группы врагов
for _ in range(5):
ex = random.choice([-35, -20, 20, 35])
ez = random.choice([10, 20, 30, 40])
enemies.append(EnemyTank(pos=(ex, 0, ez)))
# --- СИСТЕМА ПЕРЕЗАПУСКА ---
def input(key):
if key == 'r' and player.hp <= 0:
# Перезапуск сцены
scene.clear()
app.restart()
# Интерфейс
Text("Управление: W/A/S/D - Движение корпуса, Мышь - Прицел, ЛКМ - Выстрел", position=(-0.85, 0.45), scale=1)
app.run()