Moritz Wundke - Lead Programmer Tragnarion Studios
Podemos clasificar las necesidades de análisis de un juego en tres puntos de vista diferentes:
Punto de vista de PRODUCTO
Punto de vista de PROYECTO
Punto de vista de RENDIMIENTO
Una vez que tengamos un producto tenemos que conocer su estado durante su ciclo de vida.
Durante el desarrollo del proyecto es muy importante saber que hacemos mal y también que hacemos bien.
Desde el punto de vista del rendimiento nos concentramos en la estabilidad y los aspectos técnicos del juego.
El eterno dilema XD. Vamos a ver un poco en detalle los pros y los cons de hacer nuestro propio sistema.
También es posible usar un sistema comercial ya existente. Tales como:
Pero bueno, ¿aquí no estábamos para hacerlo nosotros mismos? Pues sí :D
Todo tiene su lado malo, en este caso es solo algo gris ya que tampoco es para tanto XD.
Cuando creamos un sistema de análisis tenemos que tener conocer las cuatro definiciones básicas:
El proceso de descubrir y comunicar patrones en los datos que nos ayuden resolver problemas de negocio.
El proceso de captura de datos en un sistema y el posterior envío hacia otro.
En matemáticas se define la métrica como la distancia entre elementos en un espacio. En nuestro caso sería una medida cuantitativa e interpretable de los atributos de uno o más objetos en un contexto conocido.
La monetización es el proceso de convertir un producto en dinero. En el caso de los juegos es un pilar sumamente importante.
La normalización de datos el proceso de procesar los datos RAW a algo que este dentro de nuestro rango de conocimiento.
Cuando detectamos valores corruptos o ausentes tenemos que tomar ciertas medidas. Melissa Humphries de la universidad de Texas nos proporciona un buen estudio sobre ello.
Simplemente eliminamos los datos inválidos.
Ejemplo:
La eliminación en lista consiste en eliminar la fila entera si al menos un atributo de ella es considerado inválido.
Ejemplo:
Reemplazamos valores inválidos de la columna de atributos por la moda o la media.
from collections import Counter
def mode_missing(l):
"""
Substitude missing values '?' using the mode of the set
"""
cl = Counter([l[i] for i in range(len(l)) if (l[i] != '?')])
moda = float(cl.most_common()[0][0])
return list(map(lambda x: moda if x=='?' else float(x), l))
from numpy import average
def mean_missing(l):
"""
Substitude missing values '?' using the mean of the set
"""
mean = average([l[i] for i in range(len(l)) if (l[i] != '?')])
return list(map(lambda x: mean if x=='?' else float(x), l))
Cuando analizamos diferentes datos a menudo intentamos comparar peras con manzanas, o al menos encontrar una relación entre sus atributos.
Con una matriz de atributos ajustaremos cada columna para que puedan ser comparada con otra.
from numpy import average, std
def standardize_matrix(m):
"""
Standardize each colum of the matrix 'm'
"""
averages = list(map(average, m))
stds = list(map(std, m))
return [list(map(lambda x: x if stds[i] == 0.0 else
(x - averages[i]) / stds[i], m[i]))
for i in range(len(m))]
Los recomendadores intentan proporcionar una recomendación fiable para un usuario. En un análisis lo podemos ver como una predicción de usuario que nos puede indicar mucha información útil.
Los agrupadores intentan crear grupos de elementos pero respetándo cierta similitud entre los elementos de cada grupo.
En ambos casos es necesario definir esa similitud entre elementos. Los métodos de similitud más comunes son la distancia euclidiana y el coeficiente de Pearson.
También conocido como la generalización a n dimensiones del teorema de Pitágoras. útil para valores lineales.
Tiene problemas con valores extremos pero es barato por lo que se muy usado. Si sólo comparamos distancias podemos obviar la raíz cuadrada.
la distancia euclidiana entre dos puntos P = (p1,p2,...,pn) y Q = (q1,21,...,qn) en un espacio de dimensión n ℝn.
La operación extra para la similitud nos resuelve cualquier posible división por cero.
def euclidean_dist(dic1, dic2):
"""Compute the sum of squares of the elements common
to both dictionaries"""
return sqrt(sum([pow(dic1[elem]-dic2[elem], 2)
for elem in dic1 if elem in dic2]))
def euclidean_similarity(dic1, dic2):
"""Calculate the euclidean similarity."""
return 1/(1+euclidean_dist(dic1, dic2))
Resuelve los problemas de la distancia euclidiana con los valores extremos ya que computa correlaciones entre todo los elementos usando medias.
EL coeficiente de Pearson tiene un rango de [-1,1] el cual nos indica la correlación entre las variables.
def pearson_coeff(dic1, dic2):
"""Retrieve the elements common to both dictionaries"""
commons = [x for x in dic1 if x in dic2]
nCommons = float(len(commons))
# If there are no common elements, return zero; otherwise
# compute the coefficient
if nCommons==0:
return 0
# Compute the means of each dictionary
mean1 = sum([dic1[x] for x in commons])/nCommons
mean2 = sum([dic2[x] for x in commons])/nCommons
# Compute numerator and denominator
num = sum([(dic1[x]-mean1)*(dic2[x]-mean2) for x in commons])
den1 = sqrt(sum([pow(dic1[x]-mean1, 2) for x in commons]))
den2 = sqrt(sum([pow(dic2[x]-mean2, 2) for x in commons]))
den = den1*den2
# Compute the coefficient if possible or return zero
if den==0:
return 0
return num/den
Un recomendador ponderado se basa en similitudes considerados pesos.
def weightedRating(dictio, item, similarity = pearson_coeff):
simils = {x: similarity(dictio[user], dictio[x], term)
for x in dictio if x != user}
numerator = {}
denominator = {}
# The ratings dictionary is traversed, while filling the
# auxiliary dictionaries with the values found.
for id1 in simils:
for id2 in dictio[id1]:
if not numerator.has_key(id2):
numerator [id2] = []
denominator[id2] = []
s = simils[id1]
numerator [id1].append(dictio[id1][id2])
denominator[id1].append(s)
# Compute and sort weighted ratings
result = []
for id2 in numerator:
s1 = sum(numerator [id2])
s2 = sum(denominator[id2])
if s2 == 0:
mean = 0.0
else:
mean = s1/s2
result.append((id2,mean))
result.sort(key = lambda x: x[1], reverse=True)
return result
El algoritmo de k-means busca particiones de datos tales que cada punto este asignado a un centro llamado centroide del grupo.
La regla es simple: datos = señal + ruido
Tampoco todos los datos son necesarios y a veces no nos dejan ver la realidad.
Se conoce como reducción de la dimensionalidad
Princiapl Component Analysis
Princiapl Component Analysis
Lineal Discrimination Analysis
Multi Dimensional Scaling
Busca un espacio de dimensión reducida donde las distancias relativas entre los elementos se mantengan.
Las muertes (azul) están estrechamente relacionadas con las variables analizadas pero los kills (rojo) no.
Principalmente consiste en reconocer patrones y poner etiquetas a cada individuo a partir de ciertas propiedades que lo identifiquen.
En juegos los clasificadores se suelen usar para identificar jugadores hard-core, casuales, etc...
k-Nearets-Neighbours
Consiste en clasificar un elemento contando los k vecinos más cercanos.
Es la diferencia estadística de los datos clasificados significativa?
Dos implementaciones del test de McNemar. Personalmente prefiere la implementación que se encuentra en Wikipeda.
import collections
compare = lambda x, y: collections.Counter(x) == collections.Counter(y)
def McNemar(predF, predG, classes):
if compare(predF, predG):
return False
f = predF != classes
g = predG == classes
A = float(sum(map(lambda x, y: x and y, f, g)))
f = predG != classes
g = predF == classes
B = float(sum(map(lambda x, y: x and y, f, g)))
t = (abs(A - B) - 1) ** 2 / (A + B)
return t > 3.842
def McNemarWikipedia(predF, predG, classes):
if compare(predF, predG):
return False
b = float(len(list(filter(lambda x: (predG[x]=='0') and (predF[x]=='1'), range(len(predG))))))
c = float(len(list(filter(lambda x: (predG[x]=='1') and (predF[x]=='0'), range(len(predG))))))
t = (b - c) ** 2 / (b + c)
return t > 3.842
El sistema que proponemos es bastante sencillo de implementar.
startSession: function(userid, platform, build, callback)
endSession: function(sessionid, callback)
addGamePlayEvent: function(sessionid, basedata, eventdata, callback)
addDebugEvent: function(sessionid, basedata, eventdata, callback)
addMonetaryEvent: function(sessionid, basedata, eventdata, callback)
Estamos trabajando en un pequeño juego para plataformas móviles para el cual hemos implementado el sistema que os acabo de presentar.
Los diseñadores de niveles han de tener claro como crear cada parte y necesitan datos detallados de como se comporta el jugador.
Descubrimos que los primeros niveles son mucho más difíciles que los últimos.
En qué nivel muere un jugador y más importante en que parte del nivel ha estado.
Otro elemento importante es saber en cada nivel que ha pasado. Datos acumulados nos proporcionan mucha información al respecto: Balas usadas, recargas realizadas, pickups encontrados, vida perdida, etc...
Aunque a primera vista parecía que el número de pickups era suficiente los jugadores seguían muriendo en nivel muy tempranos. La solución fue aumentar el número de pickups.
Nos os puedo decir si ahora mismo hay suficientes ya que el juego que tenéis en el stand captura los eventos para ese fin! Así que si nos queréis echar una mano jugad un poco!
Otro ejemplo muy útil es conocer el movimiento de los jugadores. El sistema de análisis DNA usado en la serie Assassin's Creed se inspira en Google Maps para representar movimientos de los jugadores
Saber por dónde pasan los jugadores o incluso la IA nos indica muchas veces fallos en el diseño graves y muy difíciles de depurar en un estado tardío del desarrollo.
Los heatmaps son otra visualización fundamental de cualquier análisis, no sólo para juegos. En el mundo web o UI tenemos heatmaps de clicks para saber si los usuarios encuentran las secciones, los banners son efectivos etc.
En juegos se suelen general mapas de muertes o de kills pero es útil converger ambos en uno para poder visualizar bien lo que pasa en un mapa.
Pero podemos incluso general heatmaps de consumo de memoria en ciertas partes de juego, de rendimiento, etc...
El siguiente ejemplo nos muestra un mapa balanceado y un mapa de flujo del conjunto de muertes/deaths (el que muere y su asesino). El mapa balanceado es normalizado.
Incluso los más modernos sistemas de matchmaking intentar generar grupos para crear partidas más divertidas o equilibradas.
Un ejemplo es el sistema de matchmaking de Xbox Live llamado True Skill.
La idea es crear un ranking o score del jugador que va variando a medida que juega y después crear grupos equilibrados de jugadores.
Estimación bayesiana desarrollado por Microsoft Research Cambridge
Modela el skill del jugador como una función de densidad probabilistica [µ, σ]