Soya/Python game skel-5

Un livre de Wikilivres.
Aller à : navigation, rechercher
# -*- 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 Itelligence Artificelle.
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()