# part 1
import japanize_kivy # 日本語化
import random
import math # 角度計算用
from kivy.app import App
from kivy.uix.widget import Widget
from kivy.clock import Clock
from kivy.graphics import Color, Ellipse, Rectangle, Line, Mesh, Rotate, PushMatrix, PopMatrix
from kivy.core.audio import SoundLoader # 効果音再生用
# --- 砲弾クラス ---
class Torpedo(Widget):
def __init__(self, x, y, is_enemy=False, owner=None, target_pos=None, **kwargs):
super().__init__(**kwargs)
self.size = (6, 6)
self.pos = (x - 3, y)
self.is_enemy = is_enemy
self.owner = owner
self.bubble_timer = 0
base_speed = 6.5 if is_enemy else 6.0
if target_pos:
dx = target_pos[0] - x
dy = target_pos[1] - y
self.target_dist = math.hypot(dx, dy)
angle = math.atan2(dy, dx)
self.vx = base_speed * math.cos(angle)
self.vy = base_speed * math.sin(angle)
else:
self.vx = 0
self.vy = -base_speed if is_enemy else base_speed
self.target_dist = 9999
self.distance_traveled = 0
self.draw_graphics()
def draw_graphics(self):
self.canvas.clear()
with self.canvas:
if self.is_enemy:
Color(1, 0.3, 0.1, 1)
else:
Color(1, 0.9, 0.2, 1)
Ellipse(pos=self.pos, size=self.size)
def move(self):
self.x += self.vx
self.y += self.vy
self.distance_traveled += math.hypot(self.vx, self.vy)
self.draw_graphics()
# --- 煙(雲)エフェクト ---
class Bubble(Widget):
def __init__(self, x, y, **kwargs):
super().__init__(**kwargs)
self.start_size = random.randint(4, 7)
self.size = (self.start_size, self.start_size)
self.pos = (x - self.start_size/2, y)
self.speed_y = random.uniform(-0.1, 0.1)
self.speed_x = random.uniform(-0.2, 0.2)
self.alpha = random.uniform(0.5, 0.7)
self.draw_graphics()
def draw_graphics(self):
self.canvas.clear()
with self.canvas:
Color(0.85, 0.85, 0.88, self.alpha)
Ellipse(pos=self.pos, size=self.size)
def move(self):
self.size = (self.size[0] + 0.2, self.size[1] + 0.2)
self.y += self.speed_y
self.x += self.speed_x
self.alpha -= 0.02
self.pos = (self.x, self.y)
self.draw_graphics()
# part 2
# --- 水の輪エフェクト ---
class Splash(Widget):
def __init__(self, x, y, **kwargs):
super().__init__(**kwargs)
self.size = (4, 2)
self.pos = (x - 2, y - 1)
self.alpha = 1.0
self.draw_graphics()
def draw_graphics(self):
self.canvas.clear()
with self.canvas:
Color(0.9, 0.95, 1, self.alpha)
Ellipse(pos=self.pos, size=self.size)
def move(self):
self.size = (self.size[0] + 1.5, self.size[1] + 0.4)
self.x -= 0.75
self.y -= 0.2
self.alpha -= 0.06
self.pos = (self.x, self.y)
self.draw_graphics()
# --- 大水柱エフェクト ---
class WaterPillar(Widget):
def __init__(self, x, y, **kwargs):
super().__init__(**kwargs)
self.particles = []
for _ in range(12):
angle = random.uniform(math.pi * 0.3, math.pi * 0.7)
speed = random.uniform(2.5, 5.5)
self.particles.append({
'x': x, 'y': y,
'vx': speed * math.cos(angle),
'vy': speed * math.sin(angle),
'size_w': random.uniform(4, 9),
'size_h': random.uniform(8, 16),
'alpha': 1.0
})
self.alive = True
def move(self):
self.canvas.clear()
active_count = 0
with self.canvas:
for p in self.particles:
if p['alpha'] > 0:
p['vy'] -= 0.15
p['x'] += p['vx']
p['y'] += p['vy']
p['alpha'] -= 0.04
Color(0.95, 0.98, 1.0, p['alpha'])
Ellipse(pos=(p['x'], p['y']), size=(p['size_w'], p['size_h']))
active_count += 1
if active_count == 0:
self.alive = False
# --- 火花エフェクト ---
class Spark(Widget):
def __init__(self, x, y, **kwargs):
super().__init__(**kwargs)
self.particles = []
for _ in range(8):
angle = random.uniform(0, math.pi * 2)
speed = random.uniform(1.0, 3.0)
self.particles.append({
'x': x, 'y': y,
'vx': speed * math.cos(angle),
'vy': speed * math.sin(angle),
'size': random.randint(2, 4),
'alpha': 1.0
})
self.alive = True
def move(self):
self.canvas.clear()
active_count = 0
with self.canvas:
for p in self.particles:
if p['alpha'] > 0:
p['x'] += p['vx']
p['y'] += p['vy']
p['alpha'] -= 0.08
Color(1, random.uniform(0.5, 0.9), 0.1, p['alpha'])
Ellipse(pos=(p['x'], p['y']), size=(p['size'], p['size']))
active_count += 1
if active_count == 0:
self.alive = False
# --- 爆発エフェクト ---
class Explosion(Widget):
def __init__(self, x, y, **kwargs):
super().__init__(**kwargs)
self.size = (6, 6)
self.pos = (x - 3, y - 3)
self.alpha = 1.0
self.draw_graphics()
def draw_graphics(self):
self.canvas.clear()
with self.canvas:
Color(1, 0.2, 0.1, self.alpha)
Ellipse(pos=self.pos, size=self.size)
Color(1, 0.8, 0.2, self.alpha)
Ellipse(pos=(self.x + self.width*0.15, self.y + self.height*0.15), size=(self.width*0.7, self.height*0.7))
def move(self):
self.size = (self.size[0] + 3.5, self.size[1] + 3.5)
self.x -= 1.75
self.y -= 1.75
self.alpha -= 0.04
self.pos = (self.x, self.y)
self.draw_graphics()
# part 3
# --- 敵(軍艦イメージ)のクラス ---
class Enemy(Widget):
COLLISION_WIDTH = 55
COLLISION_HEIGHT = 20
def __init__(self, x, y, **kwargs):
super().__init__(**kwargs)
self.size = (50, 16)
self.pos = (x - 25, y)
self.speed = 2.0
self.turn_speed = 3.5
self.wander_angle = random.uniform(0, math.pi * 2)
self.angle_deg = 180.0
self.is_alive = True # 生存フラグを追加
self.draw_graphics()
def draw_graphics(self):
self.canvas.clear()
if not self.is_alive:
return # 沈没中(消滅を待つ間)は船体を本体を描画しない
with self.canvas:
cx = self.x + self.width / 2
cy = self.y + self.height / 2
PushMatrix()
Rotate(angle=self.angle_deg, origin=(cx, cy))
Color(0, 0.05, 0.1, 0.25)
Rectangle(pos=(cx - 27, cy - 10), size=(54, 20))
Color(0.45, 0.25, 0.25, 1)
Rectangle(pos=(cx - 20, cy - 7), size=(37, 14))
Mesh(mode='triangles', vertices=[
cx - 25, cy, 0, 0,
cx - 19, cy + 7, 0, 0,
cx - 19, cy - 7, 0, 0
])
Ellipse(pos=(cx + 10, cy - 7), size=(11, 14))
Color(0.35, 0.2, 0.2, 1)
Rectangle(pos=(cx - 10, cy - 5), size=(20, 10))
Color(0.25, 0.15, 0.15, 1)
Rectangle(pos=(cx - 3, cy - 3), size=(8, 6))
Color(0.6, 0.2, 0.2, 1)
Rectangle(pos=(cx - 3, cy - 1), size=(2, 2))
Color(0.15, 0.15, 0.15, 1)
Ellipse(pos=(cx + 6, cy - 2), size=(4, 4))
Color(0.2, 0.2, 0.2, 1)
Ellipse(pos=(cx - 14, cy - 3), size=(6, 6))
Line(points=[cx - 14, cy, cx - 21, cy], width=2)
Ellipse(pos=(cx + 12, cy - 3), size=(6, 6))
Line(points=[cx + 15, cy, cx + 22, cy], width=2)
PopMatrix()
def evade_and_move(self, bullets, game_width, game_height, home_pos, other_enemies=[]):
if not self.is_alive:
return # 沈没中は移動や衝突判定をスキップ
target_vx = 0
target_vy = 0
closest_bullet = None
min_dist = 600
for bullet in bullets:
if not bullet.is_enemy:
dist = math.hypot((bullet.x + 3) - (self.x + 25), (bullet.y + 3) - (self.y + 8))
if dist < min_dist:
closest_bullet = bullet
min_dist = dist
if closest_bullet and min_dist < 130:
if (self.x + 25) >= (closest_bullet.x + 3): target_vx = 1
else: target_vx = -1
if (self.y + 8) >= (closest_bullet.y + 3): target_vy = 1
else: target_vy = -1
desired_angle = math.degrees(math.atan2(target_vy, target_vx))
else:
if random.random() < 0.02:
self.wander_angle += random.uniform(-1, 1)
target_vx = math.cos(self.wander_angle)
target_vy = math.sin(self.wander_angle)
desired_angle = math.degrees(math.atan2(target_vy, target_vx))
margin = 40
wall_vx = 0
wall_vy = 0
near_wall = False
if self.x < margin:
wall_vx = 1
near_wall = True
elif self.x > game_width - self.width - margin:
wall_vx = -1
near_wall = True
if self.y < margin:
wall_vy = 1
near_wall = True
elif self.y > game_height - self.height - margin:
wall_vy = -1
near_wall = True
if near_wall:
desired_angle = math.degrees(math.atan2(wall_vy if wall_vy != 0 else target_vy, wall_vx if wall_vx != 0 else target_vx))
self.wander_angle = math.radians(desired_angle)
angle_diff = (desired_angle - self.angle_deg + 180) % 360 - 180
if angle_diff > 0:
self.angle_deg += min(self.turn_speed, angle_diff)
elif angle_diff < 0:
self.angle_deg -= min(self.turn_speed, -angle_diff)
self.angle_deg %= 360
move_rad = math.radians(self.angle_deg)
self.x += self.speed * math.cos(move_rad)
self.y += self.speed * math.sin(move_rad)
for other in other_enemies:
if other != self and other.is_alive:
dist_x = (self.x + 25) - (other.x + 25)
dist_y = (self.y + 8) - (other.y + 8)
if abs(dist_x) < self.COLLISION_WIDTH and abs(dist_y) < self.COLLISION_HEIGHT:
if abs(dist_x) > 0: self.x += (self.COLLISION_WIDTH - abs(dist_x)) * 0.25 * (1 if dist_x > 0 else -1)
if abs(dist_y) > 0: self.y += (self.COLLISION_HEIGHT - abs(dist_y)) * 0.25 * (1 if dist_y > 0 else -1)
if self.x < 0: self.x = 0
elif self.x > game_width - self.width: self.x = game_width - self.width
if self.y < 0: self.y = 0
elif self.y > game_height - self.height: self.y = game_height - self.height
self.draw_graphics()
# part 4
# --- 自機(味方軍艦)のクラス ---
class Player(Widget):
COLLISION_WIDTH = 55
COLLISION_HEIGHT = 20
def __init__(self, x, y, **kwargs):
super().__init__(**kwargs)
self.size = (50, 16)
self.pos = (x - 25, y)
self.speed = 2.0
self.turn_speed = 3.5
self.wander_angle = random.uniform(0, math.pi * 2)
self.angle_deg = 0.0
self.is_alive = True # 生存フラグを追加
self.draw_graphics()
def draw_graphics(self):
self.canvas.clear()
if not self.is_alive:
return # 沈没中(消滅を待つ間)は船体を描画しない
with self.canvas:
cx = self.x + self.width / 2
cy = self.y + self.height / 2
PushMatrix()
Rotate(angle=self.angle_deg, origin=(cx, cy))
# 船体陰(影効果)
Color(0, 0.05, 0.1, 0.25)
Rectangle(pos=(cx - 27, cy - 10), size=(54, 20))
# 船体ベース(味方のグレー・ブルー系)
Color(0.4, 0.45, 0.5, 1)
Rectangle(pos=(cx - 20, cy - 7), size=(37, 14))
# 船首(左側を尖らせる)
Mesh(mode='triangles', vertices=[
cx - 25, cy, 0, 0,
cx - 19, cy + 7, 0, 0,
cx - 19, cy - 7, 0, 0
])
# 船尾(右側の丸み)
Ellipse(pos=(cx + 10, cy - 7), size=(11, 14))
# 中央甲板構造物
Color(0.48, 0.53, 0.58, 1)
Rectangle(pos=(cx - 10, cy - 5), size=(20, 10))
# 艦橋(操舵室)
Color(0.55, 0.6, 0.65, 1)
Rectangle(pos=(cx - 3, cy - 3), size=(8, 6))
Color(0.3, 0.7, 1, 1)
Rectangle(pos=(cx - 3, cy - 1), size=(2, 2))
# 煙突
Color(0.2, 0.2, 0.2, 1)
Ellipse(pos=(cx + 6, cy - 2), size=(4, 4))
# 主砲塔(前部)
Color(0.3, 0.35, 0.4, 1)
Ellipse(pos=(cx - 14, cy - 3), size=(6, 6))
Line(points=[cx - 14, cy, cx - 21, cy], width=2)
# 主砲塔(後部)
Ellipse(pos=(cx + 12, cy - 3), size=(6, 6))
Line(points=[cx + 15, cy, cx + 22, cy], width=2)
PopMatrix()
def evade_and_move(self, bullets, game_width, game_height, home_pos, other_players=[]):
if not self.is_alive:
return # 沈没中は移動や衝突判定をスキップ
target_vx = 0
target_vy = 0
closest_bullet = None
min_dist = 600
for bullet in bullets:
if bullet.is_enemy:
dist = math.hypot((bullet.x + 3) - (self.x + 25), (bullet.y + 3) - (self.y + 8))
if dist < min_dist:
closest_bullet = bullet
min_dist = dist
if closest_bullet and min_dist < 130:
if (self.x + 25) >= (closest_bullet.x + 3): target_vx = 1
else: target_vx = -1
if (self.y + 8) >= (closest_bullet.y + 3): target_vy = 1
else: target_vy = -1
desired_angle = math.degrees(math.atan2(target_vy, target_vx))
else:
if random.random() < 0.02:
self.wander_angle += random.uniform(-1, 1)
target_vx = math.cos(self.wander_angle)
target_vy = math.sin(self.wander_angle)
desired_angle = math.degrees(math.atan2(target_vy, target_vx))
margin = 40
wall_vx = 0
wall_vy = 0
near_wall = False
if self.x < margin:
wall_vx = 1
near_wall = True
elif self.x > game_width - self.width - margin:
wall_vx = -1
near_wall = True
if self.y < margin:
wall_vy = 1
near_wall = True
elif self.y > game_height - self.height - margin:
wall_vy = -1
near_wall = True
if near_wall:
desired_angle = math.degrees(math.atan2(wall_vy if wall_vy != 0 else target_vy, wall_vx if wall_vx != 0 else target_vx))
self.wander_angle = math.radians(desired_angle)
angle_diff = (desired_angle - self.angle_deg + 180) % 360 - 180
if angle_diff > 0:
self.angle_deg += min(self.turn_speed, angle_diff)
elif angle_diff < 0:
self.angle_deg -= min(self.turn_speed, -angle_diff)
self.angle_deg %= 360
move_rad = math.radians(self.angle_deg)
self.x += self.speed * math.cos(move_rad)
self.y += self.speed * math.sin(move_rad)
for other in other_players:
if other != self and other.is_alive:
dist_x = (self.x + 25) - (other.x + 25)
dist_y = (self.y + 8) - (other.y + 8)
if abs(dist_x) < self.COLLISION_WIDTH and abs(dist_y) < self.COLLISION_HEIGHT:
if abs(dist_x) > 0: self.x += (self.COLLISION_WIDTH - abs(dist_x)) * 0.25 * (1 if dist_x > 0 else -1)
if abs(dist_y) > 0: self.y += (self.COLLISION_HEIGHT - abs(dist_y)) * 0.25 * (1 if dist_y > 0 else -1)
if self.x < 0: self.x = 0
elif self.x > game_width - self.width: self.x = game_width - self.width
if self.y < 0: self.y = 0
elif self.y > game_height - self.height: self.y = game_height - self.height
self.draw_graphics()
# part 5
# --- ゲーム画面全体のクラス ---
class GameWidget(Widget):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.bind(size=self.draw_background, pos=self.draw_background)
self.players = []
self.enemies = []
self.bullets = []
self.bubbles = []
self.splashes = []
self.water_pillars = []
self.sparks = []
self.explosions = []
self.home_positions = {}
# 爆発音の読み込み (Android標準の通知音や短いogg/mp3があれば指定可能)
# ここではPydroidで動くように、何処かに「explosion.mp3」という音源ファイルがある前提
self.explosion_sound = SoundLoader.load('/storage/emulated/0/Download/explosion.mp3')
Clock.schedule_interval(self.update, 1.0 / 60.0)
Clock.schedule_interval(self.shoot_check, 0.2)
def draw_background(self, *args):
self.canvas.before.clear()
with self.canvas.before:
color_bottom = [0.0, 0.1, 0.25, 1.0]
color_top = [0.1, 0.35, 0.6, 1.0]
mesh_vertices = [
self.x, self.y, 0, 0, *color_bottom,
self.x + self.width, self.y, 0, 0, *color_bottom,
self.x + self.width, self.y + self.height, 0, 0, *color_top,
self.x, self.y + self.height, 0, 0, *color_top
]
indices = [0, 1, 2, 2, 3, 0]
Mesh(vertices=mesh_vertices, indices=indices, mode='triangles')
def on_size(self, *args):
count = 6
for p in self.players: self.remove_widget(p)
for e in self.enemies: self.remove_widget(e)
self.players.clear()
self.enemies.clear()
self.home_positions.clear()
for i in range(count):
spawn_x = self.width * (i + 1) / (count + 1)
player_home = (spawn_x, 100)
p = Player(player_home[0], player_home[1])
self.add_widget(p)
self.players.append(p)
self.home_positions[p] = player_home
enemy_home = (spawn_x, self.height - 150)
e = Enemy(enemy_home[0], enemy_home[1])
self.add_widget(e)
self.enemies.append(e)
self.home_positions[e] = enemy_home
def shoot_check(self, dt):
for p in self.players:
if not p.is_alive: continue
my_bullets = sum(1 for b in self.bullets if b.owner == p)
if my_bullets < 2 and self.enemies and random.random() < 0.2:
active_enemies = [e for e in self.enemies if e.is_alive]
if active_enemies:
target = min(active_enemies, key=lambda e: math.hypot(e.x - p.x, e.y - p.y))
target_pos = (target.x + target.width/2, target.y + target.height/2)
bullet = Torpedo(p.x + p.width/2, p.y + p.height/2, is_enemy=False, owner=p, target_pos=target_pos)
self.add_widget(bullet)
self.bullets.append(bullet)
for e in self.enemies:
if not e.is_alive: continue
my_bullets = sum(1 for b in self.bullets if b.owner == e)
if my_bullets < 2 and self.players and random.random() < 0.2:
active_players = [p for p in self.players if p.is_alive]
if active_players:
target = min(active_players, key=lambda p: math.hypot(p.x - e.x, p.y - e.y))
target_pos = (target.x + target.width/2, target.y + target.height/2)
bullet = Torpedo(e.x + e.width/2, e.y + e.height/2, is_enemy=True, owner=e, target_pos=target_pos)
self.add_widget(bullet)
self.bullets.append(bullet)
def handle_hit(self, bullet, ship):
hit_offset_x = (bullet.x + bullet.width / 2) - ship.x
if hit_offset_x < 10 or hit_offset_x > 40:
spark = Spark(bullet.x, bullet.y)
self.add_widget(spark)
self.sparks.append(spark)
return False
else:
explosion = Explosion(bullet.x, bullet.y)
self.add_widget(explosion)
self.explosions.append(explosion)
# --- 修正部分:音量を最大に設定してから再生 ---
if self.explosion_sound:
self.explosion_sound.volume = 1.0 # 音量を最大(100%)に指定
self.explosion_sound.play()
ship.bound_explosion = explosion
return True
def update(self, dt):
for bullet in self.bullets[:]:
bullet.move()
bullet.bubble_timer += 1
if bullet.bubble_timer % 3 == 0:
bubble = Bubble(bullet.x + bullet.width/2, bullet.y)
self.add_widget(bubble)
self.bubbles.append(bubble)
if (bullet.y > self.height or bullet.y < -40 or
bullet.x > self.width or bullet.x < -40 or
bullet.distance_traveled >= bullet.target_dist):
if bullet.distance_traveled >= bullet.target_dist:
splash = Splash(bullet.x, bullet.y)
self.add_widget(splash)
self.splashes.append(splash)
pillar = WaterPillar(bullet.x, bullet.y)
self.add_widget(pillar)
self.water_pillars.append(pillar)
self.remove_widget(bullet)
if bullet in self.bullets: self.bullets.remove(bullet)
for bubble in self.bubbles[:]:
bubble.move()
if bubble.y > self.height or bubble.y < -40 or bubble.alpha <= 0:
self.remove_widget(bubble)
if bubble in self.bubbles: self.bubbles.remove(bubble)
for splash in self.splashes[:]:
splash.move()
if splash.alpha <= 0:
self.remove_widget(splash)
if splash in self.splashes: self.splashes.remove(splash)
for pillar in self.water_pillars[:]:
pillar.move()
if not pillar.alive:
self.remove_widget(pillar)
if pillar in self.water_pillars: self.water_pillars.remove(pillar)
for spark in self.sparks[:]:
spark.move()
if not spark.alive:
self.remove_widget(spark)
if spark in self.sparks: self.sparks.remove(spark)
for exp in self.explosions[:]:
exp.move()
if exp.alpha <= 0:
self.remove_widget(exp)
if exp in self.explosions: self.explosions.remove(exp)
for p in self.players[:]:
default_pos = (self.width / 2, 100)
p.evade_and_move(self.bullets, self.width, self.height, self.home_positions.get(p, default_pos), self.players)
if not p.is_alive and hasattr(p, 'bound_explosion') and p.bound_explosion.alpha <= 0:
self.remove_widget(p)
if p in self.players: self.players.remove(p)
for e in self.enemies[:]:
default_pos = (self.width / 2, self.height - 150)
e.evade_and_move(self.bullets, self.width, self.height, self.home_positions.get(e, default_pos), self.enemies)
if not e.is_alive and hasattr(e, 'bound_explosion') and e.bound_explosion.alpha <= 0:
self.remove_widget(e)
if e in self.enemies: self.enemies.remove(e)
for bullet in self.bullets[:]:
if bullet not in self.bullets: continue
if not bullet.is_enemy:
for e in self.enemies[:]:
if not e.is_alive: continue
if (bullet.x < e.x + e.width and bullet.x + bullet.width > e.x and
bullet.y < e.y + e.height and bullet.y + bullet.height > e.y):
is_destroyed = self.handle_hit(bullet, e)
self.remove_widget(bullet)
if bullet in self.bullets: self.bullets.remove(bullet)
if is_destroyed:
e.is_alive = False
e.draw_graphics()
break
else:
for p in self.players[:]:
if not p.is_alive: continue
if (bullet.x < p.x + p.width and bullet.x + bullet.width > p.x and
bullet.y < p.y + p.height and bullet.y + bullet.height > p.y):
is_destroyed = self.handle_hit(bullet, p)
self.remove_widget(bullet)
if bullet in self.bullets: self.bullets.remove(bullet)
if is_destroyed:
p.is_alive = False
p.draw_graphics()
break
class ShootingGameApp(App):
def build(self):
return GameWidget()
if __name__ == '__main__':
ShootingGameApp().run()