Applications
IA et Jeux Vidéo
L'intelligence artificielle dans l'industrie du jeu vidéo
L'IA dans les jeux vidéo est l'un des domaines les plus anciens et les plus innovants de l'intelligence artificielle appliquée. Des PNJ aux mondes procéduraux, l'IA transforme la façon dont les jeux sont créés et joués.
Domaines d'application
IA DANS LES JEUX VIDÉO :
┌─────────────────────────────────────────────────────────────┐
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ COMPORTEMENT │ │ GÉNÉRATION │ │ ANALYSE │ │
│ │ (Gameplay) │ │ (Contenu) │ │ (Testing) │ │
│ ├──────────────┤ ├──────────────┤ ├──────────────┤ │
│ │ • PNJ │ │ • Niveaux │ │ • QA auto │ │
│ │ • Ennemis │ │ • Textures │ │ • Équilibrage│ │
│ │ • Pathfinding│ │ • Dialogues │ │ • Analytics │ │
│ │ • Difficulté │ │ • Musique │ │ • Anti-cheat │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ GRAPHISMES │ │ OUTILS │ │ JOUEUR │ │
│ │ (Rendu) │ │ (Dev) │ │ (Assistance) │ │
│ ├──────────────┤ ├──────────────┤ ├──────────────┤ │
│ │ • Upscaling │ │ • Animation │ │ • Matchmaking│ │
│ │ • Denoising │ │ • Motion cap │ │ • Recommand. │ │
│ │ • Super Res │ │ • Localisation│ │ • Accessibil.│ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘IA pour les PNJ (Personnages Non-Joueurs)
Machines à états finis (FSM)
L'approche classique encore très utilisée :
from enum import Enum, auto
class State(Enum):
IDLE = auto()
PATROL = auto()
CHASE = auto()
ATTACK = auto()
FLEE = auto()
class EnemyFSM:
def __init__(self):
self.state = State.IDLE
self.health = 100
self.detection_range = 10
self.attack_range = 2
def update(self, player_position, self_position):
distance = self.calculate_distance(player_position, self_position)
# Transitions basées sur les conditions
if self.health < 20:
self.state = State.FLEE
elif distance < self.attack_range:
self.state = State.ATTACK
elif distance < self.detection_range:
self.state = State.CHASE
elif self.state == State.CHASE:
self.state = State.PATROL
else:
self.state = State.IDLE
# Exécuter l'action correspondante
return self.execute_state()
def execute_state(self):
actions = {
State.IDLE: self.idle,
State.PATROL: self.patrol,
State.CHASE: self.chase,
State.ATTACK: self.attack,
State.FLEE: self.flee
}
return actions[self.state]()
def idle(self):
return {"action": "wait", "animation": "idle"}
def patrol(self):
return {"action": "move", "target": self.get_next_waypoint()}
def chase(self):
return {"action": "move", "target": "player", "speed": 1.5}
def attack(self):
return {"action": "attack", "damage": 10}
def flee(self):
return {"action": "move", "direction": "away_from_player", "speed": 2.0}Behavior Trees
Plus flexibles et modulaires que les FSM :
from enum import Enum
from abc import ABC, abstractmethod
class NodeStatus(Enum):
SUCCESS = 1
FAILURE = 2
RUNNING = 3
class Node(ABC):
@abstractmethod
def tick(self, context: dict) -> NodeStatus:
pass
class Sequence(Node):
"""Exécute les enfants en séquence jusqu'à échec."""
def __init__(self, children: list[Node]):
self.children = children
def tick(self, context: dict) -> NodeStatus:
for child in self.children:
status = child.tick(context)
if status != NodeStatus.SUCCESS:
return status
return NodeStatus.SUCCESS
class Selector(Node):
"""Exécute les enfants jusqu'au premier succès."""
def __init__(self, children: list[Node]):
self.children = children
def tick(self, context: dict) -> NodeStatus:
for child in self.children:
status = child.tick(context)
if status != NodeStatus.FAILURE:
return status
return NodeStatus.FAILURE
class Condition(Node):
"""Vérifie une condition."""
def __init__(self, check_func):
self.check_func = check_func
def tick(self, context: dict) -> NodeStatus:
if self.check_func(context):
return NodeStatus.SUCCESS
return NodeStatus.FAILURE
class Action(Node):
"""Exécute une action."""
def __init__(self, action_func):
self.action_func = action_func
def tick(self, context: dict) -> NodeStatus:
return self.action_func(context)
# Exemple : Comportement d'un garde
def build_guard_behavior():
return Selector([
# Priorité 1: Combattre si ennemi visible
Sequence([
Condition(lambda ctx: ctx.get("enemy_visible")),
Selector([
Sequence([
Condition(lambda ctx: ctx.get("in_attack_range")),
Action(lambda ctx: attack(ctx))
]),
Action(lambda ctx: chase_enemy(ctx))
])
]),
# Priorité 2: Investiguer les bruits
Sequence([
Condition(lambda ctx: ctx.get("heard_noise")),
Action(lambda ctx: investigate(ctx))
]),
# Priorité 3: Patrouiller
Action(lambda ctx: patrol(ctx))
])GOAP (Goal-Oriented Action Planning)
Pour des PNJ plus autonomes et adaptatifs :
from dataclasses import dataclass
from typing import Callable
import heapq
@dataclass
class ActionDef:
name: str
preconditions: dict # État requis
effects: dict # Changements d'état
cost: float # Coût de l'action
class GOAPPlanner:
def __init__(self, actions: list[ActionDef]):
self.actions = actions
def plan(self, current_state: dict, goal: dict) -> list[str]:
"""Trouve une séquence d'actions pour atteindre le goal."""
# A* sur l'espace des états
open_list = [(0, current_state, [])] # (cost, state, actions)
closed = set()
while open_list:
cost, state, actions = heapq.heappop(open_list)
# Vérifier si goal atteint
if self._goal_reached(state, goal):
return actions
state_tuple = tuple(sorted(state.items()))
if state_tuple in closed:
continue
closed.add(state_tuple)
# Explorer les actions possibles
for action in self.actions:
if self._preconditions_met(state, action.preconditions):
new_state = self._apply_effects(state.copy(), action.effects)
new_cost = cost + action.cost
heuristic = self._estimate_cost(new_state, goal)
heapq.heappush(open_list, (
new_cost + heuristic,
new_state,
actions + [action.name]
))
return [] # Pas de plan trouvé
def _preconditions_met(self, state: dict, preconditions: dict) -> bool:
return all(state.get(k) == v for k, v in preconditions.items())
def _apply_effects(self, state: dict, effects: dict) -> dict:
state.update(effects)
return state
def _goal_reached(self, state: dict, goal: dict) -> bool:
return all(state.get(k) == v for k, v in goal.items())
def _estimate_cost(self, state: dict, goal: dict) -> float:
return sum(1 for k, v in goal.items() if state.get(k) != v)
# Exemple : PNJ qui doit manger
actions = [
ActionDef("find_food", {"has_food": False}, {"found_food": True}, 2),
ActionDef("pick_food", {"found_food": True}, {"has_food": True}, 1),
ActionDef("eat", {"has_food": True, "hungry": True}, {"hungry": False, "has_food": False}, 1),
]
planner = GOAPPlanner(actions)
state = {"hungry": True, "has_food": False, "found_food": False}
goal = {"hungry": False}
plan = planner.plan(state, goal)
# Résultat: ["find_food", "pick_food", "eat"]PNJ avec LLM
La nouvelle frontière : des PNJ conversationnels intelligents :
from anthropic import Anthropic
class LLMCharacter:
def __init__(self, character_data: dict):
self.client = Anthropic()
self.name = character_data["name"]
self.personality = character_data["personality"]
self.backstory = character_data["backstory"]
self.knowledge = character_data["knowledge"]
self.conversation_history = []
def get_system_prompt(self) -> str:
return f"""Tu es {self.name}, un personnage dans un jeu vidéo de fantasy.
PERSONNALITÉ : {self.personality}
HISTOIRE : {self.backstory}
CE QUE TU SAIS :
{self.knowledge}
RÈGLES :
- Reste dans ton personnage à tout moment
- Réponds de manière concise (2-3 phrases max)
- Utilise un langage approprié à l'époque médiévale
- Tu peux donner des quêtes ou des indices si pertinent
- Ne révèle pas d'informations que ton personnage ne connaîtrait pas"""
def chat(self, player_input: str, context: dict = None) -> str:
"""Génère une réponse du personnage."""
# Ajouter le contexte de jeu si disponible
game_context = ""
if context:
game_context = f"\n[Contexte: Le joueur est niveau {context.get('level', 1)}, "
game_context += f"a complété {context.get('quests_done', 0)} quêtes, "
game_context += f"heure du jeu: {context.get('time', 'jour')}]"
self.conversation_history.append({
"role": "user",
"content": player_input + game_context
})
response = self.client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=150,
system=self.get_system_prompt(),
messages=self.conversation_history
)
npc_response = response.content[0].text
self.conversation_history.append({
"role": "assistant",
"content": npc_response
})
# Limiter l'historique pour économiser les tokens
if len(self.conversation_history) > 20:
self.conversation_history = self.conversation_history[-10:]
return npc_response
# Exemple d'utilisation
blacksmith = LLMCharacter({
"name": "Thorin le Forgeron",
"personality": "Bourru mais bienveillant, fier de son travail, amateur de bière",
"backstory": "Ancien soldat devenu forgeron après une blessure. A forgé l'épée du roi.",
"knowledge": "Expert en armes et armures. Sait qu'un dragon a été vu près des montagnes."
})
response = blacksmith.chat(
"Salut l'ami ! Tu aurais une épée pour un aventurier débutant ?",
context={"level": 1, "quests_done": 0, "time": "matin"}
)Génération procédurale
Génération de niveaux (Wave Function Collapse)
import random
import numpy as np
from typing import Optional
class WFCGenerator:
"""Génération de niveaux par Wave Function Collapse simplifié."""
def __init__(self, tiles: dict, rules: dict):
self.tiles = tiles # {"grass": "🌿", "water": "💧", ...}
self.rules = rules # {"grass": {"right": ["grass", "path"], ...}}
def generate(self, width: int, height: int) -> list[list[str]]:
# Initialiser avec toutes les possibilités
grid = [[set(self.tiles.keys()) for _ in range(width)] for _ in range(height)]
while not self._is_collapsed(grid):
# Trouver la cellule avec le moins d'entropie (choix)
x, y = self._find_min_entropy(grid)
if grid[y][x]:
# Choisir un tile aléatoirement
chosen = random.choice(list(grid[y][x]))
grid[y][x] = {chosen}
# Propager les contraintes
self._propagate(grid, x, y)
# Convertir en résultat final
return [[list(cell)[0] if cell else "?" for cell in row] for row in grid]
def _is_collapsed(self, grid) -> bool:
return all(len(cell) == 1 for row in grid for cell in row)
def _find_min_entropy(self, grid) -> tuple[int, int]:
min_entropy = float('inf')
min_pos = (0, 0)
for y, row in enumerate(grid):
for x, cell in enumerate(row):
if len(cell) > 1 and len(cell) < min_entropy:
min_entropy = len(cell)
min_pos = (x, y)
return min_pos
def _propagate(self, grid, start_x: int, start_y: int):
"""Propage les contraintes aux voisins."""
stack = [(start_x, start_y)]
height, width = len(grid), len(grid[0])
while stack:
x, y = stack.pop()
current_tiles = grid[y][x]
# Vérifier chaque voisin
neighbors = [
(x+1, y, "right"), (x-1, y, "left"),
(x, y+1, "down"), (x, y-1, "up")
]
for nx, ny, direction in neighbors:
if 0 <= nx < width and 0 <= ny < height:
# Calculer les tiles valides pour le voisin
valid = set()
for tile in current_tiles:
if tile in self.rules:
valid.update(self.rules[tile].get(direction, []))
# Réduire les possibilités du voisin
old_len = len(grid[ny][nx])
grid[ny][nx] &= valid
if len(grid[ny][nx]) < old_len:
stack.append((nx, ny))
# Exemple d'utilisation
tiles = {"grass": "🌿", "water": "💧", "sand": "🏖️", "tree": "🌲"}
rules = {
"grass": {"right": ["grass", "sand", "tree"], "left": ["grass", "sand", "tree"],
"up": ["grass", "tree"], "down": ["grass", "sand"]},
"water": {"right": ["water", "sand"], "left": ["water", "sand"],
"up": ["water", "sand"], "down": ["water", "sand"]},
"sand": {"right": ["sand", "water", "grass"], "left": ["sand", "water", "grass"],
"up": ["sand", "grass"], "down": ["sand", "water"]},
"tree": {"right": ["grass", "tree"], "left": ["grass", "tree"],
"up": ["grass"], "down": ["grass"]}
}
generator = WFCGenerator(tiles, rules)
level = generator.generate(10, 10)Génération de quêtes
from anthropic import Anthropic
import json
class QuestGenerator:
def __init__(self):
self.client = Anthropic()
def generate_quest(
self,
player_level: int,
location: str,
available_npcs: list[str],
world_state: dict
) -> dict:
"""Génère une quête adaptée au contexte."""
prompt = f"""Génère une quête de jeu vidéo RPG au format JSON.
CONTEXTE :
- Niveau du joueur : {player_level}
- Lieu actuel : {location}
- PNJ disponibles : {', '.join(available_npcs)}
- État du monde : {json.dumps(world_state, ensure_ascii=False)}
FORMAT DE SORTIE (JSON strict) :
{{
"title": "Titre de la quête",
"description": "Description narrative",
"giver": "Nom du PNJ qui donne la quête",
"type": "main/side/daily",
"objectives": [
{{"type": "kill/collect/talk/explore", "target": "...", "count": 1}}
],
"rewards": {{
"xp": 100,
"gold": 50,
"items": ["item_id"]
}},
"dialogue_intro": "Ce que dit le PNJ",
"dialogue_complete": "Ce que dit le PNJ à la fin"
}}
Génère une quête appropriée au niveau et au contexte."""
response = self.client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=500,
messages=[{"role": "user", "content": prompt}]
)
return json.loads(response.content[0].text)
# Exemple
generator = QuestGenerator()
quest = generator.generate_quest(
player_level=5,
location="Village de Millefeuilles",
available_npcs=["Thorin le Forgeron", "Elara la Guérisseuse", "Gareth le Garde"],
world_state={"menace_gobelin": True, "saison": "automne", "guerre_proche": False}
)Pathfinding
A* (A-Star)
L'algorithme de référence pour le pathfinding :
import heapq
from typing import Optional
class AStar:
def __init__(self, grid: list[list[int]]):
"""grid: 0 = libre, 1 = obstacle"""
self.grid = grid
self.height = len(grid)
self.width = len(grid[0])
def find_path(
self,
start: tuple[int, int],
goal: tuple[int, int]
) -> Optional[list[tuple[int, int]]]:
"""Trouve le chemin le plus court."""
def heuristic(a, b):
# Distance de Manhattan
return abs(a[0] - b[0]) + abs(a[1] - b[1])
open_set = [(0, start)]
came_from = {}
g_score = {start: 0}
while open_set:
_, current = heapq.heappop(open_set)
if current == goal:
# Reconstruire le chemin
path = [current]
while current in came_from:
current = came_from[current]
path.append(current)
return path[::-1]
for dx, dy in [(0, 1), (0, -1), (1, 0), (-1, 0)]:
neighbor = (current[0] + dx, current[1] + dy)
# Vérifier les limites et obstacles
if not (0 <= neighbor[0] < self.width and
0 <= neighbor[1] < self.height):
continue
if self.grid[neighbor[1]][neighbor[0]] == 1:
continue
tentative_g = g_score[current] + 1
if tentative_g < g_score.get(neighbor, float('inf')):
came_from[neighbor] = current
g_score[neighbor] = tentative_g
f_score = tentative_g + heuristic(neighbor, goal)
heapq.heappush(open_set, (f_score, neighbor))
return None # Pas de chemin
# Exemple
grid = [
[0, 0, 0, 0, 0],
[0, 1, 1, 1, 0],
[0, 0, 0, 1, 0],
[0, 1, 0, 0, 0],
[0, 0, 0, 0, 0]
]
pathfinder = AStar(grid)
path = pathfinder.find_path((0, 0), (4, 4))
# [(0, 0), (0, 1), (0, 2), (1, 2), (2, 2), (2, 3), (3, 3), (4, 3), (4, 4)]Navigation Mesh (NavMesh)
Pour des environnements 3D complexes :
import numpy as np
from scipy.spatial import Delaunay
class NavMesh:
def __init__(self, walkable_points: list[tuple[float, float]]):
"""Crée un navmesh à partir de points marchables."""
self.points = np.array(walkable_points)
self.triangulation = Delaunay(self.points)
def find_path(self, start: tuple, goal: tuple) -> list[tuple]:
"""Pathfinding sur le navmesh."""
# Trouver les triangles contenant start et goal
start_tri = self.triangulation.find_simplex(start)
goal_tri = self.triangulation.find_simplex(goal)
if start_tri == -1 or goal_tri == -1:
return [] # Points hors du navmesh
# A* sur les triangles adjacents
# (Simplifié - en production, utiliser les centers des triangles)
path_triangles = self._astar_triangles(start_tri, goal_tri)
# Convertir en waypoints (centres des triangles)
path = [start]
for tri_idx in path_triangles:
center = self.points[self.triangulation.simplices[tri_idx]].mean(axis=0)
path.append(tuple(center))
path.append(goal)
return path
def _astar_triangles(self, start: int, goal: int) -> list[int]:
"""A* sur le graphe de triangles adjacents."""
# Utiliser self.triangulation.neighbors pour les adjacences
# Implémentation similaire à A* classique
passApprentissage par renforcement
Agent simple avec Q-Learning
import numpy as np
import random
class QLearningAgent:
def __init__(
self,
state_size: int,
action_size: int,
learning_rate: float = 0.1,
discount_factor: float = 0.99,
epsilon: float = 1.0,
epsilon_decay: float = 0.995
):
self.q_table = np.zeros((state_size, action_size))
self.lr = learning_rate
self.gamma = discount_factor
self.epsilon = epsilon
self.epsilon_decay = epsilon_decay
self.epsilon_min = 0.01
self.action_size = action_size
def choose_action(self, state: int) -> int:
"""Politique epsilon-greedy."""
if random.random() < self.epsilon:
return random.randrange(self.action_size)
return np.argmax(self.q_table[state])
def learn(self, state: int, action: int, reward: float, next_state: int, done: bool):
"""Mise à jour Q-Learning."""
target = reward
if not done:
target += self.gamma * np.max(self.q_table[next_state])
self.q_table[state, action] += self.lr * (target - self.q_table[state, action])
# Décroissance de l'exploration
if self.epsilon > self.epsilon_min:
self.epsilon *= self.epsilon_decay
# Exemple : Entraîner un agent de jeu
def train_game_agent(env, episodes: int = 1000):
agent = QLearningAgent(
state_size=env.observation_space.n,
action_size=env.action_space.n
)
for episode in range(episodes):
state = env.reset()
total_reward = 0
while True:
action = agent.choose_action(state)
next_state, reward, done, _ = env.step(action)
agent.learn(state, action, reward, next_state, done)
state = next_state
total_reward += reward
if done:
break
if episode % 100 == 0:
print(f"Episode {episode}, Reward: {total_reward}, Epsilon: {agent.epsilon:.2f}")
return agentTechnologies graphiques IA
DLSS / FSR (Super Resolution)
SUPER RÉSOLUTION IA :
SANS UPSCALING :
┌─────────────┐ ┌─────────────┐
│ Rendu │ │ Affichage │
│ 1080p │ ───→ │ 1080p │
│ (100% GPU) │ │ │
└─────────────┘ └─────────────┘
AVEC DLSS/FSR :
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Rendu │ │ IA Super │ │ Affichage │
│ 540p │ ───→ │ Resolution │ ───→ │ 1080p │
│ (25% GPU) │ │ (Neural) │ │ (Qualité+) │
└─────────────┘ └─────────────┘ └─────────────┘
Avantages :
• 2-4x plus de FPS
• Qualité souvent supérieure au rendu natif
• Utilise les Tensor Cores (NVIDIA) ou shaders (AMD)Ray Tracing Denoising
DENOISING IA :
RAY TRACING BRUITÉ APRÈS DENOISING IA
(1 rayon par pixel) (Résultat propre)
░▒█░▒░░█▒░ ███████████
▒░█▒░▒░░█▒ █████████████
░▒░░█░▒░░▒ ───→ █████████████
▒░▒░░█░▒▒░ ███████████
░░▒░░░▒░░█ █████████████
Le modèle IA apprend à :
• Distinguer bruit de détail
• Préserver les bords nets
• Interpoler les zones sous-échantillonnéesOutils et frameworks
Unity ML-Agents
# Configuration d'un agent Unity avec ML-Agents (Python side)
from mlagents_envs.environment import UnityEnvironment
from mlagents_envs.base_env import ActionTuple
import numpy as np
# Connecter à l'environnement Unity
env = UnityEnvironment(file_name="Build/MyGame", seed=42)
env.reset()
# Obtenir les specs du comportement
behavior_name = list(env.behavior_specs)[0]
spec = env.behavior_specs[behavior_name]
for episode in range(100):
env.reset()
decision_steps, terminal_steps = env.get_steps(behavior_name)
while len(decision_steps) > 0:
# Récupérer les observations
observations = decision_steps.obs[0]
# Décider des actions (ici aléatoire, normalement réseau de neurones)
actions = np.random.randn(len(decision_steps), spec.action_spec.continuous_size)
action_tuple = ActionTuple(continuous=actions)
# Envoyer les actions
env.set_actions(behavior_name, action_tuple)
env.step()
decision_steps, terminal_steps = env.get_steps(behavior_name)
env.close()Résumé des outils
FRAMEWORKS ET OUTILS :
COMPORTEMENT IA
├── Unity ML-Agents : RL pour Unity
├── Unreal AI : Behavior Trees natifs
├── RAIN AI : Plugin Unity complet
└── NodeCanvas : Visual scripting IA
GÉNÉRATION PROCÉDURALE
├── Houdini : Assets procéduraux
├── World Machine : Terrains
├── SpeedTree : Végétation
└── Substance : Textures
GRAPHISMES IA
├── NVIDIA DLSS : Super resolution
├── AMD FSR : Alternative ouverte
├── Intel XeSS : Cross-platform
└── NVIDIA RTX : Ray tracing + denoising
LLM POUR JEUX
├── Inworld AI : PNJ conversationnels
├── Convai : Voix + personnalité
├── Replica Studios : Voix IA
└── Charisma.ai : Narratif interactifRésumé
IA DANS LES JEUX VIDÉO :
COMPORTEMENT PNJ
├── FSM : Simple, prévisible
├── Behavior Trees : Modulaire, flexible
├── GOAP : Autonome, adaptatif
└── LLM : Conversationnel, émergent
GÉNÉRATION PROCÉDURALE
├── WFC : Niveaux cohérents
├── L-Systems : Végétation, donjons
├── Perlin/Simplex : Terrains
└── LLM : Quêtes, dialogues
PATHFINDING
├── A* : Standard, efficace
├── NavMesh : 3D complexe
├── Flow Fields : Foules
└── Hierarchical : Grandes cartes
APPRENTISSAGE
├── Q-Learning : Simple, discret
├── DQN : États complexes
├── PPO : Continue, stable
└── Imitation : Apprendre des joueurs
GRAPHISMES
├── Super Resolution (DLSS/FSR)
├── Ray Tracing Denoising
├── Frame Generation
└── Asset GenerationL'IA dans les jeux vidéo évolue rapidement avec l'intégration des LLM pour des PNJ véritablement conversationnels et des mondes qui s'adaptent dynamiquement aux joueurs.