Aller au contenu

Soya/Python game skel-3

Un livre de Wikilivres.
# -*- indent-tabs-mode: t -*-

#! /usr/bin/python -O

# Game Skeleton

# Soya gaming tutorial, lesson 3
# Adding character-level interaction (collision, ...)

# New stuff is in the Character class


# A bunch of import
import sys, os, os.path, math
import soya
import soya.widget as widget
import soya.sdlconst as sdlconst

# Inits Soya
soya.init()

# Definition du répertoire (=ou se trouve models, 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:
	"""Les actions que le personnage peut faire."""
	def __init__(self, action):
		self.action = action

# Actions possibles.
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


class KeyboardController:
	"""Un controleur est un objet donnant des ordres à un personnage.
Ici, nous utilisons un controlleur clavier, mais il pourrait très bien être controlé par
la souris ou l'IA.Notez que la seul méthode s'appelle "next", avec laquelle nous utilisons le
controleur."""
	def __init__(self):
		self.left_key_down = self.right_key_down = self.up_key_down = self.down_key_down = 0
		
	def next(self):
		"""retourne l'action futur."""
		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_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
		
		# Les gens disant que Python n'a pas de switch/select se trompent...
		# Souvenez vous en quand vous coderez 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))

	print "foo"
class Character(soya.World):
	print "foo"
	"""Un personnage dans votre jeu."""
	def __init__(self, parent, controller):
		soya.World.__init__(self, parent)
		self.set_model(soya.Model.get("cube"))
		
		# Désactivation du raypicking sur le personnage !!!
		self.solid = 0
		
		self.controller     = controller
		self.speed          = soya.Vector(self)
		self.rotation_speed = 0.0

		# Cette variable est utilisé pour la détection des collisions.
		# radius est le rayon approximative d'une sphère autour du personnage,
		# et radius_y est pareil mais dans l'axe Y (vertical) seulement (le personnage
		# est souvent plus grand que large ;-).
		# Nous avons besoin de radius * sqrt(2)/2 < vitesse maximale (ici, 0.35)
		self.radius         = 0.5
		self.radius_y       = 1.0
		
		# Center est le point central du personnage (centré sur la sphère mentionnée
		# précédemment)
		self.center         = soya.Point(self, 0.0, self.radius_y, 0.0)
		
		# Ces vecteurs sont les directions -X, X, -Y, Y, -Z, Z du personnage.
		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)
		
	def begin_round(self):
		self.begin_action(self.controller.next())
		soya.World.begin_round(self)
		
	def begin_action(self, action):
		"""Cette méthode amorce l'action ACTION. Il n'effectue pas l'action
(pour cela, voir  advance_time). Mais ceci ne fait que décoder l'action et maintenant, il vérifie
les collisions du personnage. Il ne fait que vérifier si le personnage est bien sur le sol
ground, et sinon, il le replace au bon endroit."""
		# Reset
		self.rotation_speed = self.speed.x = self.speed.z = 0.0
		if self.speed.y > 0.0: self.speed.y = 0.0
		# Si self.speed.y < 0.0, le personnage tombe et nous cherchons si il doit
		# continuer à tomber, et tombe de plus en plus vite,
		#mais nous ne remettons pas self.speed.y à 0.0.
		
		# Détermination de la rotation du personnage
		if   action.action in (ACTION_TURN_LEFT, ACTION_ADVANCE_LEFT, ACTION_GO_BACK_LEFT):
			self.rotation_speed = 5.0
		elif action.action in (ACTION_TURN_RIGHT, ACTION_ADVANCE_RIGHT, ACTION_GO_BACK_RIGHT):
			self.rotation_speed = -5.0
			
		# Détermination de la vitesse du personnage
		if   action.action in (ACTION_ADVANCE, ACTION_ADVANCE_LEFT, ACTION_ADVANCE_RIGHT):
			self.speed.z = -0.35
		elif action.action in (ACTION_GO_BACK, ACTION_GO_BACK_LEFT, ACTION_GO_BACK_RIGHT):
			self.speed.z = 0.2

		# Calcul du nouveau centre de la sphère du personnage après le mouvement
		# (C'est un point et une addition de vecteur, une translation)
		new_center = self.center + self.speed
		
		# Création d'un contexte 'raypicking', provenant d'un centre et d'un rayon.
		# Le contexte Raypicking est plus rapide si vous en avez plusieurs
		# dans la même région, en selectionnant tous les objets succeptibles
		# de se heurter au rayon.
		context = scene.RaypickContext(new_center, max(self.radius, 0.1 + self.radius_y))
		
		# Création du sol, et on verifie si le personnage tombe.
		# Dans ce cas, nous faisons un raypicking vers le bas.
		# Si totu se passe bien, la méthode raypick retourne un 
		# tuple : (collision point, collision normal).
		r = context.raypick(new_center, self.down, 0.1 + self.radius_y, 1, 1)
		if r:
			# Le personnage a ses pieds sur ou en dessous du sol
			# donc nous le positionnons sur le sol.
			ground, ground_normal = r
			ground.convert_to(self)
			self.speed.y = ground.y
		else:
			# Pas de sol => début de la chute
			# Testez la chute avec la crevasse derrière la deuxième maison
			# Nous incrémentons self.speed.y de 0.0 à -0.5 avec -0.05 par étapes.
			self.speed.y = max(self.speed.y - 0.05, -0.5)
			
		# Le mouvement (défini par le vecteur de vitesse) peut être impossible si
		# le personnage rencontre un mur.
		
		# collisions personnage - level
		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:
				# Le rayon recontre un mur => le personnage ne peut pas continuer le mouvement
				# planifié. Nous calculons le nouveau vecteur et nous l'ajoutons au vecteur
				# vitesse si bien que new_center (pour le raypick suivant, souvenez vous que
				# new_center = self.center + self.speed, ainsi si la vitesse a changé,
				# nous devons l'actualiser.
				
				collision, wall_normal = r
				hypo = vec.length() * self.radius - new_center.distance_to(collision)
				correction = wall_normal * hypo
				
				# Formule théorique, mais assez complexe et identique au résultat
				# angle = math.radians(180.0 - vec.angle_to(wall_normal))
				#correction = wall_normal * hypo * math.cos(angle)

				# Ajout du vecteur de correction au vecteur de vitesse et à new_center
				# (Point + addition vectorielle, donc translation)
				self.speed += correction
				new_center += correction
				
	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 de la scène (un World sans parent)
scene = soya.World()

# Chargement du level, puis on le place dans la scène
try:
	level = soya.World.get("level_demo")
except ValueError:
	print>>sys.stderr, 'Le level n\'est pas encoregénéré, veuillez lancer le tutorial game_skel-1.py'
	sys.exit(1)
scene.add(level)

# Création du personnage dans le level avec le control par le clavier
character = Character(level, KeyboardController())
character.set_xyz(216.160568237, -7.93332195282, 213.817764282)

# Création d'une camera à 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 camera et un label montrant les FPS.
soya.set_root_widget(widget.Group())
soya.root_widget.add(camera)
soya.root_widget.add(widget.FPSLabel())

# Création et execution de "main_loop" (=objet gérant le temps et régulant les FPS)
# Par défaut, les FPS sont lockés à 40.
soya.MainLoop(scene).main_loop()