Soya/Python game skel-5

# -*- indent-tabs-mode: t -*-

#! /usr/bin/python -O

# Game Skeleton
# Copyright (C) 2003-2004 Jean-Baptiste LAMY

# Un jeu avec Soya, leçon 5
# Ajout de la fonction saut.

# Nouvel élément dans les classes Controller Character 


# Importations
import sys, os, os.path, math
import soya
import soya.widget as widget
import soya.sdlconst as sdlconst

# Initialisation de Soya
soya.init()

# On définit le répertoire des données (lieux où l'on trouve les modèles, textures...)
HERE = os.path.dirname(sys.argv[0])
soya.path.append(os.path.join(HERE, "data"))


class Level(soya.World):
	"""Un niveau de jeu. Level est une sous classe de soya.World."""


class Action:
	"""Une action que le personnage peut faire."""
	def __init__(self, action):
		self.action = action

# Actions disponibles
ACTION_WAIT          = 0
ACTION_ADVANCE       = 1
ACTION_ADVANCE_LEFT  = 2
ACTION_ADVANCE_RIGHT = 3
ACTION_TURN_LEFT     = 4
ACTION_TURN_RIGHT    = 5
ACTION_GO_BACK       = 6
ACTION_GO_BACK_LEFT  = 7
ACTION_GO_BACK_RIGHT = 8
ACTION_JUMP          = 9


class KeyboardController:
	"""Un controlleur est un objet qui donne des ordres aux personnages.
Ici, nous définissons le clavier comme controleur de base, mais il y a aussi les controleurs basés sur la souris ou sur une Intelligence Artificielle (IA).
Notez que l'unique méthode appellée est "next", avec laquelle nous allons utiliser le générateur de controlleur python."""
	def __init__(self):
		self.left_key_down = self.right_key_down = self.up_key_down = self.down_key_down = 0
		
	def next(self):
		"""Returns the next action"""
		jump = 0
		
		for event in soya.process_event():
			if   event[0] == sdlconst.KEYDOWN:
				if   (event[1] == sdlconst.K_q) or (event[1] == sdlconst.K_ESCAPE):
					sys.exit() # Quit the game
					
				elif event[1] == sdlconst.K_LSHIFT:
					# La touche shift déclanche le saut
					# Contrairement aux autres actions, le saut est seulment appellé au début du saut.
					jump = 1
					
				elif event[1] == sdlconst.K_LEFT:  self.left_key_down  = 1
				elif event[1] == sdlconst.K_RIGHT: self.right_key_down = 1
				elif event[1] == sdlconst.K_UP:    self.up_key_down    = 1
				elif event[1] == sdlconst.K_DOWN:  self.down_key_down  = 1
				
			elif event[0] == sdlconst.KEYUP:
				if   event[1] == sdlconst.K_LEFT:  self.left_key_down  = 0
				elif event[1] == sdlconst.K_RIGHT: self.right_key_down = 0
				elif event[1] == sdlconst.K_UP:    self.up_key_down    = 0
				elif event[1] == sdlconst.K_DOWN:  self.down_key_down  = 0
		
		if jump: return Action(ACTION_JUMP)
		
		# Tout le monde dit que Python n'a pas de switch/select case, c'est absolument faux...
		# Souvenez vous en quand vous programmez un jeu de combat !
		return Action({
			(0, 0, 1, 0) : ACTION_ADVANCE,
			(1, 0, 1, 0) : ACTION_ADVANCE_LEFT,
			(0, 1, 1, 0) : ACTION_ADVANCE_RIGHT,
			(1, 0, 0, 0) : ACTION_TURN_LEFT,
			(0, 1, 0, 0) : ACTION_TURN_RIGHT,
			(0, 0, 0, 1) : ACTION_GO_BACK,
			(1, 0, 0, 1) : ACTION_GO_BACK_LEFT,
			(0, 1, 0, 1) : ACTION_GO_BACK_RIGHT,
			}.get((self.left_key_down, self.right_key_down, self.up_key_down, self.down_key_down), ACTION_WAIT))


class Character(soya.World):
	"""A character in the game."""
	def __init__(self, parent, controller):
		soya.World.__init__(self, parent)

		# Chargement d'un modèle Cal3D
		balazar = soya.AnimatedModel.get("balazar")
		
		# Création d'un Body Cal3D affichant le modèle "balazar"
		# (NB: Balazar est le nom d'un sorcier).
		self.perso = soya.Body(self, balazar)
		
		# Appel de l'animation du personnage qui attend.
		self.perso.animate_blend_cycle("attente")
		
		# Animation actuelle
		self.current_animation = "attente"
		
		# Désactivation du raypicking du personnage !!!
		self.solid = 0
		
		self.controller     = controller
		self.speed          = soya.Vector(self)
		self.rotation_speed = 0.0
		
		# Nous avons besoin de radius * sqrt(2)/2 < max speed (ici, 0.35)
		self.radius         = 0.5
		self.radius_y       = 1.0
		self.center         = soya.Point(self, 0.0, self.radius_y, 0.0)
		
		self.left   = soya.Vector(self, -1.0,  0.0,  0.0)
		self.right  = soya.Vector(self,  1.0,  0.0,  0.0)
		self.down   = soya.Vector(self,  0.0, -1.0,  0.0)
		self.up     = soya.Vector(self,  0.0,  1.0,  0.0)
		self.front  = soya.Vector(self,  0.0,  0.0, -1.0)
		self.back   = soya.Vector(self,  0.0,  0.0,  1.0)

		# Vrai si le personnage saute, par exemple, speed.y > 0.0
		self.jumping = 0
		
	def play_animation(self, animation):
		if self.current_animation != animation:
			# Arrêt de l'animation précédente
			self.perso.animate_clear_cycle(self.current_animation, 0.2)
			
			# Début de la nouvelle animation
			self.perso.animate_blend_cycle(animation, 1.0, 0.2)
			
			self.current_animation = animation
			
	def begin_round(self):
		self.begin_action(self.controller.next())
		soya.World.begin_round(self)
		
	def begin_action(self, action):
		# Réinitialisation
		self.speed.x = self.speed.z = self.rotation_speed = 0.0
		
		# Si le personnage saute, nous ne pouvons pas remettre speed.y à 0.0 !!!
		if (not self.jumping) and self.speed.y > 0.0: self.speed.y = 0.0
		
		animation = "attente"
		
		# Détermination de la rotation du personnage
		if   action.action in (ACTION_TURN_LEFT, ACTION_ADVANCE_LEFT, ACTION_GO_BACK_LEFT):
			self.rotation_speed = 4.0
			animation = "tourneG"
		elif action.action in (ACTION_TURN_RIGHT, ACTION_ADVANCE_RIGHT, ACTION_GO_BACK_RIGHT):
			self.rotation_speed = -4.0
			animation = "tourneD"
			
		# Determination de la vitesse du personnage
		if   action.action in (ACTION_ADVANCE, ACTION_ADVANCE_LEFT, ACTION_ADVANCE_RIGHT):
			self.speed.z = -0.25
			animation = "marche"
		elif action.action in (ACTION_GO_BACK, ACTION_GO_BACK_LEFT, ACTION_GO_BACK_RIGHT):
			self.speed.z = 0.06
			animation = "recule"
			
		new_center = self.center + self.speed
		context = scene.RaypickContext(new_center, max(self.radius, 0.1 + self.radius_y))
		
		# On prend le sol, et on vérifie si le personnage tombe.
		r = context.raypick(new_center, self.down, 0.1 + self.radius_y, 1, 1)

		if r and not self.jumping:
			# Placement du personnage sur le sol
			# Si le personnage saute, nous ne pouvons pas le mettre SUR le sol.
			ground, ground_normal = r
			ground.convert_to(self)
			self.speed.y = ground.y
			
			# Le saut est possible seulement si le personnage est sur le sol.

			if action.action == ACTION_JUMP:
				self.jumping = 1
				self.speed.y = 0.5
				
		else:
			# Pas de sol => le personnage tombe
			# Vous pouvez tester en allant sur le toit de la 2ème maison.
			self.speed.y = max(self.speed.y - 0.02, -0.25)
			animation = "chute"
			
			# Si la vitesse verticale est négative, le saut est arrêté.
			if self.speed.y < 0.0: self.jumping = 0
			
		new_center = self.center + self.speed
		
		# Le mouvement (définit par le vecteur de vitesse) peut être impossible si le 
		# personnage rencontre un mur.
		
		for vec in (self.left, self.right, self.front, self.back, self.up):
			r = context.raypick(new_center, vec, self.radius, 1, 1)
			if r:
				# Si le ray rencontre un mur => le personnage ne peut pas avancer.
				# Nous effectuons une correction sur le vecteur, et nous l'ajoutons au vecteur de vitesse, pour obtenir un
				# nouveau centre : new_center (pour le raypicking suivant ; souvenez vous en)
				# new_center = self.center + self.speed, alors si speed est changé, nous devons le mettre à jour).
				
				collision, wall_normal = r
				hypo = vec.length() * self.radius - (new_center >> collision).length()
				correction = wall_normal * hypo
				
				# Formules théoriques, mais plus complexes avec un résultat identique.
				#angle = math.radians(180.0 - vec.angle_to(wall_normal))
				#correction = wall_normal * hypo * math.cos(angle)
				
				self.speed.add_vector(correction)
				new_center.add_vector(correction)
				
		self.play_animation(animation)
			
	def advance_time(self, proportion):
		soya.World.advance_time(self, proportion)
		
		self.add_mul_vector(proportion, self.speed)
		self.rotate_y(proportion * self.rotation_speed)

		
# Création d'une scène (un World sans parent)
scene = soya.World()

# Chargement du Level, on le place ensuite sur la scène.
try:
	level = soya.World.get("level_demo")
except ValueError:
	print>>sys.stderr, 'the level of this demo is not yet generated, please run the game_skel-1.py tutorial'
	sys.exit(1)
scene.add(level)

# Création d'un personnage dans le Leavel, avec un controlleur clavier.
character = Character(level, KeyboardController())
character.set_xyz(216.160568237, -7.93332195282, 213.817764282)

# Création d'une caméra à la Tomb Raider dans la scène
camera = soya.TravelingCamera(scene)
traveling = soya.ThirdPersonTraveling(character)
traveling.distance = 5.0
camera.add_traveling(traveling)
camera.zap()
camera.back = 70.0

# Création d'un groupe de widget, contenant la caméra et un label qui montre les FPS.
soya.set_root_widget(widget.Group())
soya.root_widget.add(camera)
soya.root_widget.add(widget.FPSLabel())

# Création et démarrage de la "main_loop" (=un objet qui gère le temps et qui régule les FPS)
# Par défaut, le nombre de FPS est bloqué à 40.
soya.MainLoop(scene).main_loop()