Como Fazer um Jogo Simples de iPhone com Cocos2D

Ray Wenderlich

Este post também está disponível em: Chinês, Inglês, Russo, Espanhol

Ninjas Going Pew-Pew!

Ninjas Fazendo Pew-Pew!

Atualizado 25/11/12: Há uma versão mais recente desse tutorial, compatível com Cocos2D 2.X aqui.

Cocos2D é uma poderosa biblioteca para iPhone que pode te poupar bastante tempo de desenvolvimento do seu jogo de iPhone. Ele tem suporte a sprite, efeitos gráficos legais, biblioteca de física, sistema de som e muito mais.

Estou apenas começando a aprender Cocos2D, e enquanto existem vários tutorias sobre como começar com Cocos2D por ai, eu não consegui encontrar nada perto do que eu estava procurando – fazer um jogo simples mas funcional com animações, colisões e sons sem usar recursos muito avançados. Eu acabei construindo um jogo simples de minha autoria, e pensei em escrever uma série de tutoriais com base nessa minha experiência, o que pode ser útil para outros novatos.

Esta série de tutorias vai andar ao seu lado através do processo de criar um simples jogo para iPhone com Cocos2D, do início ao fim. Você pode seguir ao longo da série ou simplesmente pular direto para o exemplo ao fim do artigo. E sim. Teremos ninjas.
(Pular para a Parte 2 ou Parte 3 da série.)

Baixando e Instalando o Cocos2D

Você pode baixar o Cocos2D da página do Cocos 2D no Google Code.

Depois de você baixar o código, você vai querer instalar os úteis templates de projetos. Abra uma janela do Terminal para o diretório que você baixou o Cocos2D e coloque o seguinte comando: ./install-templates.sh -f -u

Note que você pode passar opcionalmente um parâmetro para instalar um script se você tem o XCode instalado em um diretório não padrão (como você pode ter feito se você tem mais de uma versão do SDK na sua máquina).

Hello, Cocos2D!

Vamos começar pegando um simples projeto “Hello World” e rodar usando o template de Cocos2D que acabamos de instalar. Abra o Xcode e crie um novo projeto de Cocos2D selecionando o template do cocos2d e nomeie o projeto de “Cocos2DSimpleGame”.

Cocos2D Templates

Vá em frente e rode o template como está. Se tudo funcionou, você vai ver o seguinte:

HelloWorld Screenshot

Cocos2D é organizado no conceito de “scenes”(cenas), o que são algo como “fases” ou “telas” de um jogo. Por exemplo, você pode ter uma scene para o menu de abertura do jogo, outra para o jogo em si e uma scene de término de jogo ao final. Dentro das scenes, você pode ter um número de layers(camadas), parecido com o Photoshop, e layers podem conter nodes(nós) como sprites, labels, menus ou mais. E os nodes podem conter outros nodes também (p.s. um sprite pode ter um sprite filho dentro dele).

Se você der uma olhada no projeto de exemplo, você verá apenas uma layer – HelloWorldLayer – e começaremos a implementar nossa tela principal aqui. Vá em frente e abra – você vai ver que agora no método init está adicionando uma label que diz “Hello World” para a layer. Vamos tirar isso, e colocar um sprite no lugar.

Adicionando um Sprite

Antes de colocarmos um sprite, nós precisaremos de algumas imagens para trabalhar. Você pode querer usar as suas, ou usar uma que minha amável esposa criou para o projeto: uma imagem de Personagem, uma imagem de um Projétil e uma imagem Alvo.

Uma vez que você tenha conseguido as imagens, arreste elas para a pasta “resources” no XCode e tenha certeza que “Copy items into destination groups folder(if needed)” esteja marcado.

Agora que possuímos nossas imagens, temos de apontar para onde nós queremos colocar o personagem. Note que no Cocos2D o canto inferior esquerdo da tela tem as coordenadas de (0,0) e os valores de x e y aumentam quando você move para cima e para direita. Como este projeto está em modo paisagem, isto significa que o canto superior direito é (480, 320).

Note também que por padrão quando nós definimos a posição de um objeto, a posição é relativa ao centro do sprite que nos adicionamos. Então se nós queremos o sprite do nosso personagem esteja alinhado com o canto esquerdo da tela horizontalmente e centralizado verticalmente:

  • Para a coordenada x da posição, definiremos como [player sprite's width]/2.
  • Para a coordenada y da posição, definiremos como [window height]/2.

Aqui está uma imagem que pode ajudar a mostrar isso um pouco melhor:

Screen and Sprite Coordinates

Então vamos disparar! Abra a pasta Classes e clique em HelloWorldLayer.m e substitua o método init pelo seguinte:

-(id) init
{
  if( (self=[super init] )) {
    CGSize winSize = [[CCDirector sharedDirector] winSize];
    CCSprite *player = [CCSprite spriteWithFile:@"Player.png" 
      rect:CGRectMake(0, 0, 27, 40)];
    player.position = ccp(player.contentSize.width/2, winSize.height/2);
    [self addChild:player];		
  }
  return self;
}

Você pode compilar e rodá-lo e seu sprite vai aparecer bem, mas note que o fundo da tela esta em preto padrão. Para essa artwork, branco terá uma aparência muito melhor. Um jeito fácil de definir o fundo de uma layer em Cocos2D para uma cor customizada é usar o CCLayerColor class. Então vamos disparar. Clique no HelloWorldLayer.h e troque a declaração de interface do HelloWorld  para a forma a seguir:

@interface HelloWorldLayer : CCLayerColor

Então clique no HelloWorldLayer.m e faça uma pequena modificação no método init, então podemos definir a cor de fundo para branco.

if( (self=[super initWithColor:ccc4(255,255,255,255)] )) {

Vá em frente compile e rode, e você poderá ver seu sprite no topo de um fundo branco. Woot, nosso ninja, parece pronto para a ação.

Sprite Added Screenshot

Movendo alvos

Em seguida, queremos adicionar alguns alvos dentro da scene para nosso ninja enfrentar. Para deixar isso mais interessante, nós queremos que os alvo se movam – de outro jeito eles não seriam muito desafiadores! Então vamos criar alvos escondidos fora da tela a direita e definir uma ação para informar quando eles se moverão para a esquerda.

Adicione o seguinte métodos antes do método inti:

-(void)addTarget {
  CCSprite *target = [CCSprite spriteWithFile:@"Target.png" 
    rect:CGRectMake(0, 0, 27, 40)]; 
 
  // Determine where to spawn the target along the Y axis
  CGSize winSize = [[CCDirector sharedDirector] winSize];
  int minY = target.contentSize.height/2;
  int maxY = winSize.height - target.contentSize.height/2;
  int rangeY = maxY - minY;
  int actualY = (arc4random() % rangeY) + minY;
 
  // Create the target slightly off-screen along the right edge,
  // and along a random position along the Y axis as calculated above
  target.position = ccp(winSize.width + (target.contentSize.width/2), actualY);
  [self addChild:target];
 
  // Determine speed of the target
  int minDuration = 2.0;
  int maxDuration = 4.0;
  int rangeDuration = maxDuration - minDuration;
  int actualDuration = (arc4random() % rangeDuration) + minDuration;
 
  // Create the actions
  id actionMove = [CCMoveTo actionWithDuration:actualDuration 
    position:ccp(-target.contentSize.width/2, actualY)];
  id actionMoveDone = [CCCallFuncN actionWithTarget:self 
    selector:@selector(spriteMoveFinished:)];
  [target runAction:[CCSequence actions:actionMove, actionMoveDone, nil]];
 
}

Eu tenho escrito as coisas de uma forma detalhada aqui para tornar as coisas tão fáceis de entender quanto possível. A primeira parte deve fazer sentido baseado no que temos discutido até agora: nós fazemos algumas contas simples para determinar onde queremos criar o objeto, definir a posição do objeto, e adicioná-lo à cena da mesma forma que fizemos para o sprite do jogador.

O novo elemento aqui é acrescentar ações. Cocos2D fornece um monte de ações embutidas extremamente úteis que você pode usar para criar animações com seus sprites, como ações de movimento, acões de pulo, ações de fade, ações de animação e mais. Aqui nos usamos três ações no alvo.

  • CCMoveTo: Usamos a ação CCMoveTo para direcionar objetos para se mover de fora da tela para a esquerda. Note que podemos especificar a duração de quanto tempo o movimento irá durar e aqui temos uma variedade de velocidades randomica de 2 a 4 segundos.
  • CCCallFuncN: A função CCCallFuncN permite especificarmos um callback para ocupar nosso objeto quando a ação é executada. Nós especificando um callback com o nome de “spriteMoveFinished” que nós ainda não escrevemos.
  • CCSequence: A ação CCSequence nos permite encadear uma sequencia de ações que são executadas em ordem, uma de cada vez. Deste jeito, podemos ter a ação CCMoveTo executando primeiro, e quando completa, executa a ação CCCallFuncN.

Em seguida, adicione a função de callback que nos referimos na ação CCCallFuncN. Você pode adiciona-lo logo antes de addTarget:

-(void)spriteMoveFinished:(id)sender {
  CCSprite *sprite = (CCSprite *)sender;
  [self removeChild:sprite cleanup:YES];
}

A finalidade dessa função é remover o sprite da scene assim que ele sai da tela. Isto é importante para que não ocorra perda de memória com o tempo por haver toneladas de sprites sem uso sentados fora da tela. Note que existe outras (e melhores) maneiras de resolver este problema, como ter arrays reutilizáveis de sprites, mas para este tutorial de iniciantes nós pegamos o caminho simples.

Uma última coisa antes de prosseguirmos. Precisamos realmente chamar 0 método para criar alvos! E para fazer as coisas interessantes, teremos alvos surgindo continuamente ao longo do tempo. Podemos fazer isso em Cocos2D programando uma função de callback para ser convocada periodicamente. Uma vez por segundo deve-se fazer isso. Então adicionamos o seguinte chamado para seu método init:

[self schedule:@selector(gameLogic:) interval:1.0];

E então implementamos a função de callback simplismente do seguinte jeito:

-(void)gameLogic:(ccTime)dt {
  [self addTarget];
}

É isto! Então agora se você compilar e rodar o projeto você pode ver alvos felizes se movendo pela da tela:

Targets Screenshot

Disparando Projéteis

Neste ponto o ninja começa a implorar por alguma ação – então vamos colocar disparos! Existem muitas maneiras que poderiamos implementar disparos, mas para este jogo vamos fazer quando o usuário toca na tela, uma bala é disparada do personagem na direção do toque.

Eu quis usar uma ação CCMoveTo para implementar isso e manter as coisas num nivel iniciante, mas para usá-lo nós teremos que fazer algumas contas. Isto por que o CCMoveTo exige que demos um destino para o projétil, mas não podemos apenas usar o ponto de toque por que o ponto de toque representa apenas a direção relativa ao personagem. Realmente queremos manter a bala se movendo do ponto de toque até ela ir para fora da tela.

Aqui está uma imagem que ilustra o assunto:

Projectile Triangle

Então como você pode ver, nós temos um pequeno triângulo criado pelo x e y deslocado da origem ao ponto de toque. Apenas precisamos fazer o grande triângulo com a mesma proporção –  e sabemos que queremos uma das extremidades seja fora da tela.

Ok, então vamos ao código. Primeiro nós temos que permitir toques em nossa layer. Adicione a seguinte linha no método init:

self.isTouchEnabled = YES;

Desde que habilitamos toques na nossa layer, iremos receber o callback nos eventos de toque. Então vamos implementar o método ccToucherEnded, que é chamado sempre que o usuário completa o toque, como a seguir:

- (void)ccTouchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
 
  // Choose one of the touches to work with
  UITouch *touch = [touches anyObject];
  CGPoint location = [touch locationInView:[touch view]];
  location = [[CCDirector sharedDirector] convertToGL:location];
 
  // Set up initial location of projectile
  CGSize winSize = [[CCDirector sharedDirector] winSize];
  CCSprite *projectile = [CCSprite spriteWithFile:@"Projectile.png" 
    rect:CGRectMake(0, 0, 20, 20)];
  projectile.position = ccp(20, winSize.height/2);
 
  // Determine offset of location to projectile
  int offX = location.x - projectile.position.x;
  int offY = location.y - projectile.position.y;
 
  // Bail out if we are shooting down or backwards
  if (offX

Na primeira parte, escolhemos um dos toques para trabalhar com ele, pegando a localização na view atual, então chamamos convertToGL para converter as coordenadas para nosso layout atual. Isto é importante fazer pois estamos em modo paisagem.

Em seguida carregamos o sprite projetado e setamos a posição inicial como usual. Vamos determinar para onde desejamos mover o projétil, usando o vetor entre o personagem e o toque como guia, de acordo com o algoritmo descrito antes.

Note que este algoritimo não é o ideal. Forçamos a bala para manter-se em movimento até atingir a posição X fora da tela – mesmo se estiver saído da tela na posição Y  antes! Existe várias maneiras fazer isto incluindo verificar a distância mais curta para sair da tela, tendo nosso callback verificando a lógica do jogo para projéteis fora da tela e removendo em vez de usar o método de callback, etc, mas para este tutorial de iniciante deixaremos como está.

A última coisa que temos de fazer é determinar a duração do movimento. Queremos que a bala seja disparada a uma taxa constante, apesar da direção do tiro, então novamente temos de fazer um pouco de conta. Podemos descobrir o quão longe estamos nos movendo usando o Teorema de Pitágoras. Lembrando da geometria, está é a regra que diz a distância de uma hipotenusa de um triângulo é igual a raiz quadrada da soma dos quadrados dos dois lados.

Uma vez que temos a distância, apenas dividimos ela pela velocidade a fim de obter a duração. Isto é porque velocidade = distância sobre tempo, ou em outras palavras tempo = distância sobre velocidade.

O restante está definindo as ações assim como fizemos para os alvos. Compile e rode, e agora nosso ninja está habilitado para disparar contra as hordas invasoras!
Projectiles Screenshot

Detectando Colisões

Então agora temos shurikens voando para todo lado – mas o que nosso ninja realmente quer fazer é acertar alguém. Então vamos colocar algum código para detectar quando nossos projéteis encontrem nossos alvos.

Existem várias maneiras para resolver isso com Cocos2D, incluindo usar uma das bibliotecas de física incluidas: Box2D ou Chipmunk. Contudo para manter as coisas fáceis, vamos implementar um simples detector de colisão nós mesmos.

Para fazer isso, precisamos primeiro ter um melhor controle de alvos e projéteis atualmente em cena. Adicione a seguinte declaração de classe para seu HelloWorldLayer:

NSMutableArray *_targets;
NSMutableArray *_projectiles;

E inicie o array no seu método init:

_targets = [[NSMutableArray alloc] init];
_projectiles = [[NSMutableArray alloc] init];

E enquanto pensamos nisso, limpamos a memória na seu método dealloc:

[_targets release];
_targets = nil;
[_projectiles release];
_projectiles = nil;

Agora, mude seu método addTarget acrescentando o novo alvo para o array de alvos e defina uma tag para uso futuro:

target.tag = 1;
[_targets addObject:target];

E modifique seu método ccTouchesEnded para adicionar o novo projétil para o array de projéteis e defina uma tag para uso futuro:

projectile.tag = 2;
[_projectiles addObject:projectile];

Finalmente, mude seu método spriteMoveFinished para remover o sprite para o array apropriado baseado na tag:

if (sprite.tag == 1) { // target
  [_targets removeObject:sprite];
} else if (sprite.tag == 2) { // projectile
  [_projectiles removeObject:sprite];
}

Compile e rode o projeto para ter certeza que tudo está funcionando bem. Não deve ter nenhuma diferença perceptível até este ponto, mas agora que temos o registro, precisamos implementar alguma detecção de colisão.

Agora adicione o seguinte método para o HelloWorldLayer:

- (void)update:(ccTime)dt {
  NSMutableArray *projectilesToDelete = [[NSMutableArray alloc] init];
  for (CCSprite *projectile in _projectiles) {
    CGRect projectileRect = CGRectMake(
      projectile.position.x - (projectile.contentSize.width/2), 
      projectile.position.y - (projectile.contentSize.height/2), 
      projectile.contentSize.width, 
      projectile.contentSize.height);
    NSMutableArray *targetsToDelete = [[NSMutableArray alloc] init];
    for (CCSprite *target in _targets) {
      CGRect targetRect = CGRectMake(
        target.position.x - (target.contentSize.width/2), 
        target.position.y - (target.contentSize.height/2), 
        target.contentSize.width, 
        target.contentSize.height);
 
      if (CGRectIntersectsRect(projectileRect, targetRect)) {
        [targetsToDelete addObject:target];				
      }						
    }
 
    for (CCSprite *target in targetsToDelete) {
      [_targets removeObject:target];
      [self removeChild:target cleanup:YES];									
    }
 
    if (targetsToDelete.count > 0) {
      [projectilesToDelete addObject:projectile];
    }
    [targetsToDelete release];
  }
 
  for (CCSprite *projectile in projectilesToDelete) {
    [_projectiles removeObject:projectile];
    [self removeChild:projectile cleanup:YES];
  }
  [projectilesToDelete release];
}

O trecho acima deve ser bastante claro. Acabamos de percorrer através de nossos projéteis e alvos, a criação de retângulos correspondentes às suas caixas delimitadoras, e usar CGRectIntersectsRect para verificar interseções. Se alguma for encontrada, nós removemos da cena e dos arrays. Note que temos que adicionar os objetos para um array “toDelete” porque você não pode remover um objeto de u array enquanto você está interagindo com ele. Novamente, existem formas mais melhores para implementar esse tipo de coisa, mas estou indo pela abordagem simples.

Você só precisa de mais uma coisa antes que você esteja pronto para agitar – programar esse método para executar o mais rápido possível, colocando a seguinte linha ao seu método init:

[self schedule:@selector(update:)];

Dê um compilar e rodar e agora quando seus projéteis encontrarem algos eles irão sumir!

Toques Finais

Nós estamos bem perto de termos um jogo funcional, mas extremamente simples, agora. Precisamos apenas adicionar alguns efeitos sonoros e música(que tipo de jogo não tem som!) e algumas lógicas simples de jogo.

Se você tem seguido minha série de posts sobre programação de audio para iPhone, você ficará extremanente satisfeito de saber como simples os desenvolveres do Cocos2D fizeram para tocar um simples efeito sonoro no seu jogo.

Primeiro, arraste alguma música de fundo e um efeito sonoro de disparos dentro da sua pasta resources. Sinta-se livre pra usar o o fundo musical irado que eu fiz ou meu  incrível efeito sonoro de pew-pew, ou faça o seu próprio.

Então, adicione o seguinte import para o topo do seu HelloWorldLayer.m:

#import "SimpleAudioEngine.h"

No seu método int, inicie a música de fundo como a seguir:

[[SimpleAudioEngine sharedEngine] playBackgroundMusic:@"background-music-aac.caf"];

E no seu método ccTouchesEnded, toque o efeito sonoro como a seguir:

[[SimpleAudioEngine sharedEngine] playEffect:@"pew-pew-lei.caf"];

Agora, vamos criar uma nova scene que vai servir como nosso indicador de “Você Venceu!” ou “Você Perdeu”. Clique na pasta Classes e vá para FileNew File e escolha Objective-C class e tenha certeza que subclass of NSObject está selecionado. Em seguida digite GameOverScene, clique em Next e escolha onde salvar o novo arquivo criado.

Então substitua o GameOverScene.h com o seguinte código:

#import "cocos2d.h"
@interface GameOverLayer : CCLayerColor {
  CCLabelTTF *_label;
}
@property (nonatomic, retain) CCLabelTTF *label;
@end
@interface GameOverScene : CCScene {
  GameOverLayer *_layer;
}
@property (nonatomic, retain) GameOverLayer *layer;
@end

Então substitua o GameOverScene.m com o seguinte código:

#import "GameOverScene.h"
#import "HelloWorldLayer.h"
@implementation GameOverScene
@synthesize layer = _layer;
- (id)init {
  if ((self = [super init])) {
    self.layer = [GameOverLayer node];
    [self addChild:_layer];
  }
  return self;
}
- (void)dealloc {
  [_layer release];
  _layer = nil;
  [super dealloc];
}
@end
@implementation GameOverLayer
@synthesize label = _label;
-(id) init
{
  if( (self=[super initWithColor:ccc4(255,255,255,255)] )) {
 
    CGSize winSize = [[CCDirector sharedDirector] winSize];
    self.label = [CCLabelTTF labelWithString:@"" fontName:@"Arial" fontSize:32];
    _label.color = ccc3(0,0,0);
    _label.position = ccp(winSize.width/2, winSize.height/2);
    [self addChild:_label];
 
    [self runAction:[CCSequence actions:
      [CCDelayTime actionWithDuration:3],
      [CCCallFunc actionWithTarget:self selector:@selector(gameOverDone)],
      nil]];
 
  }	
  return self;
}
- (void)gameOverDone {
  [[CCDirector sharedDirector] replaceScene:[HelloWorldLayer scene]];
 
}
- (void)dealloc {
  [_label release];
  _label = nil;
  [super dealloc];
}
@end

Perceba que existem dois objetos diferentes aqui: uma scence e uma layer. A scene pode conter qualquer número de layer, entretanto neste exemplo será apenas um. A layer apenas coloca uma label no meio da tela, e programa a transição para acontecer depois de 3 segundos para voltar para a Hello World scene.

Finalmente, vamos adicionar alguma lógica de jogo extremamente básica. Primeiro, vamos manter o controle dos projéteis que o personagem destruiu. Colocando uma variável para sua classe HelloWorldLayer no HelloWorldLayer.h como a seguir:

int _projectilesDestroyed;

Dentro do HelloWorldLayer.m, adicione um import para a classe GameOverScene:

#import “GameOverScene.h”

Aumente a contagem e verifique a condição de vitória no seu método de atualização dentro do loop targetsToDelete logo após removeChild:target:

_projectilesDestroyed++;
if (_projectilesDestroyed > 30) {
  GameOverScene *gameOverScene = [GameOverScene node];
  _projectilesDestroyed = 0;
  [gameOverScene.layer.label setString:@"You Win!"];
  [[CCDirector sharedDirector] replaceScene:gameOverScene];
}

E, finalmente, vamos fazê-lo de um jeito que se apenas um alvo passar, você perde. Mude o método spriteMoveFinished adicionando o seguinte código dentro do if sprite.tag == 1 logo após removeChild: sprite:

GameOverScene *gameOverScene = [GameOverScene node];
[gameOverScene.layer.label setString:@"You Lose :["];
[[CCDirector sharedDirector] replaceScene:gameOverScene];

Vá em frente e dê um compile e rode e você agora deve ter as condições de vitória e derrota e ver a cena de game over quando for o momento!

Me dê o código!

E no capricho! Aqui está o código completo para o simples jogo de Cocos2D para iPhone que desenvolvemos até agora.

Para onde ir agora?

Este projeto pode ser uma boa base para brincar um pouco mais com Cocos2D adicionando algumas novas funcionalidades no projeto. Talvez tentar adicionar uma barra para mostrar quantos alvos você tem de destruir antes de ganhar (dê uma olhada na amostra de projeto drawPrimitivesTest para exemplos de como fazer isso). Talvez adicionar animações de morte legais para quando os monstros são destruídos(veja os projetos ActionsTest, EffectsTest e EffectsAdvancedTest para isso). Talvez adicionar mais sons, artwork ou lógica de jogabilidade apenas de brincadeira. O céu é o limite!

Se você quer continuar com esta série de tutorias, dê uma olhada na parte dois,  How To Add A Rotating Turret ou na parte três Harder Monsters and More Levels!

Também se você deseja continuar aprendendo mais sobre Cocos2D, dê uma olhada nos meus tutorias sobre how to create buttons in Cocos2Dintro to Box2D, ou how to create a simple Breakout game.

Sinta-se livre para chamar se você conhece alguma maneira melhor de fazer várias coisas com este projeto ou se existe algum problema – como eu disse essa é a primeira vez que eu brinquei com Cocos2D, então eu tenho muito para aprender!

Ray Wenderlich

Ray is an indie software developer currently focusing on iPhone and iPad development, and the administrator of this site. He’s the founder of a small iPhone development studio called Razeware, and is passionate both about making apps and teaching others the techniques to make them.

When Ray’s not programming, he’s probably playing video games, role playing games, or board games.

User Comments

0 Comment

Other Items of Interest

Ray Newsletter Mensal

Sign up to receive a monthly newsletter with my favorite dev links, and receive a free epic-length tutorial as a bonus!

Advertise with Us!

Hang Out With Us!

Every month, we have a free live Tech Talk - come hang out with us!


Coming up in May: Procedural Level Generation in Games with Kim Pedersen.

Sign Up - May

Coming up in June: WWDC Keynote - Podcasters React! with the podcasting team.

Sign Up - June

Vote For Our Next Book!

Help us choose the topic for our next book we write! (Choose up to three topics.)

    Carregando ... Carregando ...

Nossos livros

Our Team

Time de Tutoriais

  • Sam Davies

... 55 total!

Editorial Team

  • Ryan Nystrom

... 22 total!

Code Team

  • Orta Therox

... 1 total!

Equipe de Tradução

  • Wilson Lin

... 38 total!

Subject Matter Experts

  • Richard Casey

... 4 total!