No nível pode haver milhares de inimigos.A missão do defensor: Valley of the Forgotten DX sempre teve problemas de longa data com velocidade, e finalmente consegui resolvê-los. O principal incentivo para um aumento maciço de velocidade foi o nosso
porto no PlayStation Vita . O jogo já foi lançado no PC e funcionou bem, se não perfeitamente, no
Xbox One com o
PS4 . Mas sem uma grande melhoria no jogo, nunca poderíamos lançá-lo no Vita.
Quando um jogo fica mais lento, os comentaristas da Internet geralmente culpam uma linguagem ou mecanismo de programação. É verdade que linguagens como C # e Java são mais caras que C e C ++, e ferramentas como Unity têm problemas insolúveis, como coleta de lixo. De fato, as pessoas apresentam essas explicações porque a linguagem e o mecanismo são as propriedades mais óbvias do software. Mas os verdadeiros assassinos do desempenho podem ser pequenos detalhes estúpidos que não têm nada a ver com arquitetura.
0. Ferramentas de criação de perfil
Existe apenas uma maneira real de tornar o jogo mais rápido - realizar perfis. Descubra em que o computador gasta muito tempo e faça com que gaste menos tempo ou, melhor ainda, não perca tempo.
A ferramenta de criação de perfil mais simples é o monitor do sistema Windows padrão (monitor de desempenho):
De fato, essa é uma ferramenta bastante flexível e é muito fácil trabalhar com ela. Basta pressionar Ctrl + Alt + Delete, abra o "Gerenciador de tarefas" e clique na guia "Desempenho". No entanto, não execute muitos outros programas. Se você observar atentamente, poderá detectar facilmente picos no uso da CPU e até mesmo vazamentos de memória. Essa é uma maneira pouco informativa, mas pode ser o primeiro passo para encontrar locais lentos.
O Defender's Quest está escrito na linguagem
Haxe de alto nível, compilada em outros idiomas (meu principal objetivo era C ++). Isso significa que qualquer ferramenta capaz de criar um perfil do C ++ também pode criar um perfil do meu código C ++ gerado pelo Haxe. Então, quando eu queria entender as causas dos problemas, iniciei o Performance Explorer no Visual Studio:
Além disso, diferentes consoles têm suas próprias ferramentas de criação de perfil, o que é muito conveniente, mas por causa do NDA, não posso dizer nada sobre eles. Mas se você tiver acesso a eles, não deixe de usá-los!
Em vez de escrever um tutorial terrível sobre como usar ferramentas de criação de perfil como o Performance Explorer, deixo um link para a
documentação oficial e passo para o tópico principal - coisas incríveis que levaram a um enorme aumento de produtividade e como consegui encontrá-las !
1. Detecção de Problemas
O desempenho do jogo não é apenas a velocidade em si, mas também a sua percepção. O Defender's Quest é um jogo de gênero de defesa de torre que é renderizado a 60 FPS, mas com velocidade de jogo variável na faixa de 1 / 4x a 16x. Independentemente da velocidade do jogo, a simulação usa um
carimbo de data / hora fixo com 60 atualizações por segundo do tempo de simulação 1x. Ou seja, se você executar o jogo a uma velocidade de 16x, a lógica de atualização funcionará na verdade com uma frequência de
960 FPS . Honestamente, esses são pedidos muito altos para o jogo! Mas fui eu quem criou esse modo e, se for lento, os jogadores definitivamente o perceberão.
E no jogo existe
esse nível:
Esta é a batalha final de bônus "Endless 2", também é "meu pesadelo pessoal". A captura de tela foi feita no modo Novo Jogo +, no qual os inimigos não são apenas muito mais fortes, mas também têm recursos como restaurar a saúde. A estratégia favorita do jogador aqui é bombear os dragões para o nível máximo de Rugido (ataque AOE que atordoa os inimigos), e por trás deles, vários cavaleiros com Knockback bombeado ao máximo para empurrar todos os que passam os dragões de volta à sua área de ação. O efeito cumulativo é que um enorme grupo de monstros fica interminavelmente em um só lugar, muito mais do que os jogadores teriam que sobreviver se eles realmente os matassem. Como os jogadores precisam
esperar pelas ondas e não
matá- las para receber recompensas e conquistas, essa estratégia é absolutamente eficaz e brilhante - esse é exatamente o comportamento dos jogadores que eu estimulei.
Infelizmente, isso também acaba sendo um caso
patológico para o desempenho,
especialmente quando os jogadores querem jogar em velocidades de 16x ou 8x. É claro que apenas os jogadores mais hardcore tentarão obter a conquista “Hundredth Wave” no New Game + no nível Endless 2, mas eles são apenas aqueles que falam o jogo mais alto, então eu queria que eles fossem felizes.
É apenas um jogo 2D com um monte de sprites, o que poderia estar errado com ele?
E de fato. Vamos acertar.
2. Resolução de colisão
Dê uma olhada nesta captura de tela:
Vê este bagel ao redor do guarda florestal? Essa é a sua área de impacto - observe que também há uma zona morta na qual ela
não pode atingir alvos. Cada classe tem sua própria área de ataque e cada defensor tem uma área de tamanho diferente, dependendo do nível de reforço e dos parâmetros pessoais. E todo defensor em teoria pode apontar para qualquer inimigo no campo de seu alcance. O mesmo vale para certos tipos de inimigos. Pode haver até 36 defensores no mapa (não incluindo o personagem principal Azru), mas não há limite superior no número de inimigos. Cada defensor e inimigo tem uma lista de alvos possíveis, criados com base em chamadas para verificar a área a cada etapa da atualização (menos o corte lógico daqueles que não podem atacar no momento, e assim por diante).
Hoje, os processadores de vídeo são muito rápidos - se você não os exercitar demais, eles podem processar quase qualquer número de polígonos. Mas mesmo as CPUs mais rápidas com facilidade têm “gargalos” em procedimentos simples, especialmente aqueles que crescem exponencialmente. É por isso que um jogo em 2D pode se tornar mais lento que um jogo em 3D muito mais bonito - não porque o programador não possa lidar (talvez isso também seja, pelo menos no meu caso), mas em princípio porque a lógica às vezes pode ser mais cara, do que desenhar! A questão não é quantos objetos estão na tela, mas o que eles
fazem .
Vamos explorar e acelerar o reconhecimento de colisões. Para comparação, direi que, antes da otimização, o reconhecimento de colisão levou até 50% do tempo da CPU no principal ciclo de batalha. Após a otimização, menos de 5%.
É tudo sobre árvores quadrante
A principal solução para o problema do reconhecimento lento de colisões é
dividir o espaço - e desde o início usamos uma implementação
de alta qualidade
da árvore do quadrante . Essencialmente, ele efetivamente separa o espaço para que muitas verificações opcionais de colisão possam ser ignoradas.
Em cada quadro, atualizamos a árvore inteira dos quadrantes (QuadTree) para rastrear a posição de cada objeto e, quando o inimigo ou o defensor deseja mirar em alguém, ele pede ao QuadTree uma lista de objetos próximos. Mas o criador de perfil nos disse que essas duas operações são muito mais lentas do que deveriam ser.
O que há de errado aqui?
Como se viu - muito.
Digitação de string
Como mantive inimigos e defensores em uma árvore do quadrante, tive que indicar o que estava procurando, e isso foi feito assim:
var things:Array<XY> = _qtree.queryRange(zone.bounds, "e"); //"e" - "enemy"
No jargão dos programadores, isso é chamado de código de
digitação de string e, entre outros motivos, é ruim porque as comparações de string são sempre mais lentas que as comparações inteiras.
Eu rapidamente peguei constantes inteiras e substituí o código por este:
var things:Array<XY> = _qtree.queryRange(zone.bounds, QuadTree.ENEMY);
(Sim, provavelmente valeu a pena usar o
Enum Abstract para segurança máxima do tipo, mas eu estava com pressa e precisava fazer o trabalho primeiro.)
Somente essa mudança deu uma
enorme contribuição, porque essa função é chamada de forma
constante e recursiva, toda vez que alguém precisa de uma nova lista de objetivos.
Vetor vs matriz
Dê uma olhada nisso:
var things:Array<XY>
As matrizes Haxe são muito semelhantes às matrizes ActionScript e JS, pois são coleções de objetos redimensionáveis, mas no Haxe elas são fortemente tipadas.
No entanto, há outra estrutura de dados que é mais eficiente com linguagens de destino estáticas, como o cpp, a saber
haxe.ds.Vector . Os vetores Haxe são essencialmente os mesmos que os arrays, exceto que, quando criados, obtêm um tamanho fixo.
Como minhas árvores do quadrante já tinham um volume fixo, substituí as matrizes por vetores para obter um aumento notável da velocidade.
Solicite apenas o que você precisa
Anteriormente, minha função
queryRange
retornava uma lista de objetos, instâncias
XY
. Eles continham as coordenadas x / y do objeto de jogo referenciado e seu identificador inteiro exclusivo (índice de pesquisa na matriz principal). O objeto do jogo que executava a solicitação recebeu esses XYs, extraiu um identificador inteiro para obter seu destino e depois esqueceu o resto.
Então, por que devo passar todas essas referências aos objetos XY para cada nó QuadTree
recursivamente e até
960 vezes por quadro? É o suficiente para eu retornar uma lista de identificadores inteiros.
DICA PROFISSIONAL: números inteiros são muito mais rápidos para transmitir do que quase todos os outros tipos de dados!Comparado a outras correções, isso era bastante simples, mas o crescimento do desempenho ainda era perceptível, porque esse loop interno era usado de maneira muito ativa.
Otimização da recursão da cauda
Há uma coisa elegante
chamada otimização de chamada de
cauda . É difícil de explicar, então é melhor mostrar um exemplo.
Foi:
nw.queryRange(Range, -1, result);
ne.queryRange(Range, -1, result);
sw.queryRange(Range, -1, result);
se.queryRange(Range, -1, result);
return result;
Tornou-se:
return se.queryRange(Range, filter, sw.queryRange(Range, filter, ne.queryRange(Range, filter, nw.queryRange(Range, filter, result))));
O código retorna os mesmos resultados lógicos, mas, de acordo com o criador de perfil, a segunda opção é mais rápida, pelo menos ao traduzir para cpp. Os dois exemplos executam exatamente a mesma lógica - eles fazem alterações na estrutura de dados "resultado" e a passam para a próxima função antes de retornar. Quando fazemos isso recursivamente, podemos evitar que o compilador gere referências temporárias, porque ele pode simplesmente retornar o resultado da função anterior imediatamente, em vez de cumpri-lo em uma etapa extra. Ou algo assim. Eu não entendo completamente como isso funciona, então leia o post no link acima.
(A julgar pelo que sei, a versão atual do compilador Haxe não possui uma função de otimização de recursão de cauda, ou seja, provavelmente é o trabalho do compilador C ++ - portanto, não se surpreenda se esse truque não funcionar ao traduzir o código Haxe que não está no cpp.)
Pool de Objetos
Se eu precisar de resultados precisos, devo destruir e reconstruir o QuadTree novamente a cada chamada de atualização. Criar novas instâncias do QuadTree é uma tarefa bastante comum, mas com um grande número de novos objetos AABB e XY, os QuadTrees que dependem deles levaram a uma sobrecarga grave de memória. Como esses objetos são muito simples, seria lógico alocar muitos desses objetos com antecedência e apenas reutilizá-los constantemente. Isso é chamado de
pool de objetos .
Eu costumava fazer algo assim:
nw = new QuadTree( new AABB( cx - hs2x, cy - hs2y, hs2x, hs2y) );
ne = new QuadTree( new AABB( cx + hs2x, cy - hs2y, hs2x, hs2y) );
sw = new QuadTree( new AABB( cx - hs2x, cy + hs2y, hs2x, hs2y) );
se = new QuadTree( new AABB( cx + hs2x, cy + hs2y, hs2x, hs2y) );
Mas então substituí o código por este:
nw = new QuadTree( AABB.get( cx - hs2x, cy - hs2y, hs2x, hs2y) );
ne = new QuadTree( AABB.get( cx + hs2x, cy - hs2y, hs2x, hs2y) );
sw = new QuadTree( AABB.get( cx - hs2x, cy + hs2y, hs2x, hs2y) );
se = new QuadTree( AABB.get( cx + hs2x, cy + hs2y, hs2x, hs2y) );
Utilizamos a estrutura
HaxeFlixel de código-fonte aberto, então
implementamos isso usando a classe
FlxPool HaxeFlixel. No caso de otimizações altamente especializadas, geralmente substituo alguns elementos básicos do Flixel (por exemplo, reconhecimento de colisão) por minha própria implementação (como fiz com o QuadTrees), mas o FlxPool é melhor do que tudo o que escrevi e faz exatamente o que precisa.
Especialização se necessário
Um objeto
XY
é uma classe simples que possui as propriedades
x
,
y
int_id
. Como foi usado em um loop interno particularmente usado ativamente, pude salvar muitos comandos e operações de alocação de memória movendo todos esses dados para uma estrutura de dados especial que fornece a mesma funcionalidade do
Vector<XY>
. Chamei essa nova classe de
XYVector
e o resultado pode ser visto
aqui . Este é um aplicativo altamente especializado e não é flexível ao mesmo tempo, mas nos forneceu algumas melhorias de velocidade.
Funções incorporadas
Agora, depois de concluirmos a ampla fase do reconhecimento de colisão, precisamos fazer muitas verificações para descobrir quais objetos realmente colidem. Sempre que possível, tento comparar pontos e figuras, não figuras e figuras, mas às vezes tenho que fazer o último. De qualquer forma, tudo isso requer suas próprias verificações especiais:
private static function _collide_circleCircle(a:Zone, b:Zone):Bool { var dx:Float = a.centerX - b.centerX; var dy:Float = a.centerY - b.centerY; var d2:Float = (dx * dx) + (dy * dy); var r2:Float = (a.radius2) + (b.radius2); return d2 < r2; }
Tudo isso pode ser aprimorado com uma única
inline
-
inline
:
private static inline function _collide_circleCircle(a:Zone, b:Zone):Bool { var dx:Float = a.centerX - b.centerX; var dy:Float = a.centerY - b.centerY; var d2:Float = (dx * dx) + (dy * dy); var r2:Float = (a.radius2) + (b.radius2); return d2 < r2; }
Quando adicionamos inline a uma função, dizemos ao compilador que copie e cole esse código e cole as variáveis quando ele for usado, e não faça uma chamada externa para uma função separada, o que gera custos desnecessários. A incorporação nem sempre é aplicável (por exemplo, aumenta a quantidade de código), mas é ideal para situações em que pequenas funções são chamadas repetidamente.
Trazemos à mente conflitos
A verdadeira lição aqui é que, no mundo real, as otimizações nem sempre são do mesmo tipo. Essas correções são uma mistura de técnicas avançadas, hacks baratos, aplicação de recomendações lógicas e eliminação de erros estúpidos. Tudo isso em geral nos dá um aumento de desempenho.
Mas ainda assim -
meça sete vezes, corte uma!Duas horas de otimização pedante da função, chamada uma vez a cada seis quadros e com 0,001 ms, não valem o esforço, apesar da feiura e estupidez do código.
3. Classifique tudo
Na verdade, foi uma das minhas últimas melhorias, mas acabou sendo tão vantajosa que merece seu próprio título. Além disso, era o mais simples e provou-se repetidamente. O criador de perfil me mostrou um procedimento que eu não pude melhorar - o loop principal draw (), que levou muito tempo. O motivo foi a função que classificou todos os elementos da tela antes da renderização - ou seja,
classificar todos os sprites levou muito mais tempo do que desenhá-los!
Se você olhar as capturas de tela do jogo, verá que todos os inimigos e defensores são classificados primeiro por
y
e depois por
x
, para que os elementos se sobreponham de trás para frente, da esquerda para a direita, quando passamos do canto superior esquerdo para o canto inferior direito da tela.
Uma maneira de superar a classificação é simplesmente passar a classificação de renderização pelo quadro. Este é um truque útil para algumas funções caras, mas imediatamente levou a erros visuais muito visíveis, por isso não nos convinha.
Finalmente, a decisão veio de um dos mantenedores da HaxeFlixel,
Jens Fisher . Ele perguntou: "Você se certificou de usar um algoritmo de classificação rápido para matrizes quase ordenadas?"
Não! Acabou que não. Eu usei a classificação de matriz da biblioteca padrão Haxe (acho que foi a
classificação por mesclagem - uma boa opção para casos gerais. Mas eu tive um caso muito
especial . Ao classificar em cada quadro, a posição de classificação altera apenas um número muito pequeno de sprites, mesmo se houver muitos. Substituí a chamada de classificação antiga
pela classificação por inserções e
boom! - a velocidade aumentou instantaneamente.
4. Outros problemas técnicos
O reconhecimento e a classificação de colisões foram grandes vitórias na lógica de
update()
e
draw()
, mas muitas outras armadilhas foram ocultadas nos loops internos usados ativamente.
Std.is () e elenco
Em diferentes loops internos "quentes", eu tinha um código semelhante:
if(Std.is(something,Type)) { var typed:Type = cast(something,Type); }
Na linguagem Haxe,
Std.is()
nos diz se um objeto pertence a um tipo específico (Tipo) ou a uma classe (Classe), e o
cast
tenta convertê-lo em um tipo específico durante a execução do programa.
Existem versões seguras e desprotegidas de
cast
com
cast
seguro, resultando em desempenho reduzido, mas elencos desprotegidos não.
Seguro:
cast(something, Type);
Desprotegido:
var typed:Type = cast something;
Quando uma tentativa de conversão insegura falha, obtemos nulo, enquanto uma transmissão segura gera uma exceção. Mas se não vamos capturar uma exceção, então qual é o sentido de fazer um elenco seguro? Sem captura, a operação ainda falha, mas funciona mais lentamente.
Além disso, não faz sentido preceder uma
Std.is()
segura com a verificação
Std.is()
. O único motivo para usar um elenco seguro é uma exceção garantida, mas se verificarmos o tipo antes do elenco, já garantimos que o elenco não falhará!
Posso acelerar um pouco as
Std.is()
com um elenco
Std.is()
depois de verificar
Std.is()
. Mas por que precisamos reescrever a mesma coisa, se eu não preciso verificar o tipo de classe?
Suponha que eu tenha um
CreatureSprite
, que pode ser uma instância de uma subclasse de
DefenderSprite
ou
EnemySprite
. Em vez de chamar
Std.is(this,DefenderSprite)
podemos criar um campo inteiro no
CreatureSprite
com valores como
CreatureType.DEFENDER
ou
CreatureType.ENEMY
, que são verificados ainda mais rapidamente.
Repito, vale a pena corrigi-lo apenas nos locais em que uma desaceleração significativa é claramente registrada.
A propósito, você pode ler mais sobre o elenco
seguro e
desprotegido no
manual Haxe .
Serialização / desserialização do universo
Foi chato encontrar esses lugares no código:
function copy():SomeClass { return SomeClass.fromXML(this.toXML()); }
Sim Para copiar um objeto, nós o
serializamos em XML e, em seguida,
analisamos todo esse XML , após o qual descartamos instantaneamente o XML e retornamos um novo objeto. Essa é provavelmente a maneira mais lenta de copiar um objeto, além disso, sobrecarrega a memória. Inicialmente, escrevi chamadas XML para salvar e carregar do disco e acho que estava com preguiça de escrever os procedimentos de cópia corretos.
Provavelmente, tudo estaria em ordem se essa função raramente fosse usada, mas essas chamadas surgissem em locais inadequados no meio da jogabilidade. Então me sentei e comecei a escrever e testar a função de cópia correta.
Diga não ao Nulo
A verificação de igualdade para nulo é usada com bastante frequência, mas, ao converter o Haxe em cpp, um objeto que permite um valor indefinido gera custos desnecessários que não surgem se o compilador puder assumir que o objeto nunca será nulo. Isso é especialmente verdadeiro para tipos de base como
Int
- Haxe implementa a validade de um valor indefinido para eles no sistema de destino estático por sua "embalagem", o que ocorre não apenas para variáveis declaradas explicitamente como nulas (
var myVar:Null<Int>
), mas também para coisas como opções de ajuda (
?myParam:Int
). Além disso, as verificações nulas causam desperdício desnecessário.
Consegui corrigir alguns desses problemas apenas olhando o código e pensando em alternativas - posso fazer uma verificação mais simples, que sempre será verdadeira quando o objeto for nulo? Posso capturar nulo muito antes na cadeia de chamadas de função e passar um simples sinalizador inteiro ou booleano para chamadas filho? Posso estruturar tudo para que
nunca seja garantido que o valor se torne nulo? E assim por diante Não podemos eliminar completamente verificações e valores nulos, mas tirá-los das funções me ajudou muito.
5. Tempo de download
No PSVita, tivemos problemas sérios com o tempo de carregamento de algumas cenas. Ao criar um perfil, os motivos se resumem principalmente à rasterização de texto, renderização desnecessária de software, renderização cara de botões e outras coisas.
Text
O HaxeFlixel é baseado no
OpenFL , que possui um TextField incrível e confiável. Mas usei objetos FlxText de maneira imperfeita - os objetos FlxText têm um campo de texto OpenFL interno que é rasterizado. No entanto, descobriu-se que eu não precisava da maioria dessas funções complexas de texto, mas, devido à maneira estúpida de configurar meu sistema de interface do usuário, os campos de texto precisavam ser renderizados antes que todos os outros objetos fossem localizados. Isso levou a saltos pequenos, mas perceptíveis, por exemplo, ao carregar uma janela pop-up.
Aqui fiz três correções - primeiro, substituí o máximo de texto possível por fontes raster. O Flixel tem suporte interno para vários formatos de fonte raster, incluindo o
BMFont do AngelCode , o que facilita o trabalho com Unicode, estilo e kerning, mas a API de texto raster é um pouco diferente da API de texto sem formatação, então eu tive que escrever uma pequena classe de wrapper para simplifique a transição. (Eu dei um nome adequado para
FlxUITextHack
).
Isso melhorou levemente o trabalho - as fontes de bitmap renderizam muito rapidamente - mas aumentou ligeiramente a complexidade: tive que preparar especialmente conjuntos de caracteres separados e adicionar lógica de comutação, dependendo da localidade, em vez de apenas configurar uma caixa de texto que fizesse todo o trabalho.
A segunda correção foi criar um novo objeto de interface do usuário que fosse um
espaço reservado simples para o texto, mas tivesse as mesmas propriedades públicas do texto. Eu o chamei de “área de texto” e criei uma nova classe para ela na minha biblioteca da interface do usuário, para que meu sistema de interface do usuário possa usar essas áreas de texto da mesma maneira que os campos de texto reais, mas ele não renderiza nada até calcular o tamanho e a posição de todo o resto. Então, quando minha cena foi preparada, iniciei o procedimento de substituição dessas áreas de texto por campos de texto reais (ou campos de texto de fontes de bitmap).
A terceira correção dizia respeito à percepção. Se houver pausas entre a entrada e a reação, mesmo em meio segundo, o jogador percebe isso como uma frenagem. Portanto, tentei encontrar todas as cenas nas quais há um atraso na entrada até a próxima transição e adicionei uma camada translúcida com a palavra "Carregando ..." ou apenas uma camada sem texto. Uma correção tão simples melhorou bastante a
percepção de capacidade
de resposta do jogo, pois algo acontece imediatamente após o jogador tocar no controle, mesmo que demore algum tempo para exibir o menu.
Renderização de software
A maioria dos menus usa uma combinação de escala de software e composição de 9 fatias. Isso aconteceu porque na versão para PC havia uma interface do usuário independente da resolução que poderia funcionar com uma proporção de 4: 3 e 16: 9, dimensionada de acordo. Mas no PSVita já
sabemos a resolução, ou seja, não precisamos de todos esses recursos extras de alta resolução e algoritmos de dimensionamento em tempo real. Podemos simplesmente pré-renderizar os recursos para a resolução exata e colocá-los na tela.
Primeiro, entrei na marcação da interface do usuário para as condições do Vita que mudaram o jogo para o uso de um conjunto paralelo de recursos. Então eu precisava criar esses recursos preparados para uma permissão. O
depurador HaxeFlixel acabou sendo muito útil
aqui - adicionei meu script a ele para que ele simplesmente libere o cache de varredura no disco. Em seguida, criei uma configuração de compilação especial para Windows que simula a permissão para o Vita, abri todos os menus do jogo, alternei para o depurador e iniciei o comando de exportação para as versões em escala dos recursos como PNGs prontos. Depois, apenas os renomeei e os usei como recursos para o Vita.
Renderização de botão
Meu sistema de interface do usuário tinha um problema real com os botões - quando eles foram criados, os botões renderizaram o conjunto de recursos padrão e, instantes depois, redimensionaram (e renderizaram novamente) o código de inicialização da interface do usuário, e às vezes até a
terceira vez, antes do carregamento de toda a interface do usuário. . Resolvi esse problema adicionando opções que atrasavam a renderização dos botões para o último estágio.
Digitalização de texto opcional
A revista estava carregando especialmente devagar. No começo, pensei que o problema estava nos campos de texto, mas não. O texto da revista pode conter links para outras páginas, indicadas por caracteres especiais incorporados no próprio texto bruto. Esses caracteres foram posteriormente cortados e usados para calcular a localização do link.
Acabou. que eu digitalizei
todos os campos de texto para encontrar e substituir esses caracteres por links formatados corretamente, sem verificar primeiro se há algum caractere especial nesse campo de texto! Pior, de acordo com o design, os links eram usados
apenas na página de conteúdo, mas eu os marquei em todas as caixas de texto em todas as páginas.
Consegui contornar todas essas verificações usando a construção if do formulário "Esta caixa de texto usa links?". A resposta para essa pergunta geralmente era não. Finalmente, a página que demorou mais para carregar acabou sendo a página de índice. Como nunca muda no menu do diário, por que não o armazenamos em cache?
6.
— . , Vita. , .
« »? : , , , . , , :
, , .
, — PC
, . ( ) — , . , !
Haxe , open source, , , Unity. hxcpp API!
, :
cpp.vm.Gc.run(false); // (true/false - / )
, , , , , .
7.
, PC, PSVita, Nintendo Switch, .
« », ,
.
16x , . — , AOE- — . .
, - 16x
8x, , 8x 4x. Endless Battle 2. , .
. Vita , , .
Endless Battle 2 —
, . , ?
, , , .
— , .
, « » , . «» 8 «» ( ) , .
«» , «» 1 «», . «» , . , ,
.
, «», — , , , , .
(,
.)
, . , , — . , , — - ,
!
,
, , , . , . , , , ,
. , - , , «» . , , . , , .
, — , . , , «».
— , PSVita, — ! !
! ()
, , . , , , !
,
PSVita, , , . - HD- , . ( ).
— PC , . , Vita , .
, , , ,
. UI,
.
, - «» , . , .
NaN
. , . Haxe C++ :
, ,
-90
270
.
-724
,
4
.
-
-2147483648
.
. -2147483648 360, 5 965 233 , 0 . , (
—
update !) — , ( - ) .
, ,
NaN
— , «Not a number» ( ), , . , .
Math.isNan()
, que redefinem o ângulo quando esse evento (bastante raro, mas inevitável) ocorre. Ao mesmo tempo, continuei pesquisando a causa raiz do erro, encontrei-o e o atraso desapareceu imediatamente. Acontece que, se você não executar 6 milhões de iterações sem sentido, poderá obter um grande aumento de velocidade!(Uma correção para esse erro foi inserida no próprio HaxeFlixel).Não se engane
OpenFL, HaxeFlixel . , , , . , .
- : ,
, , , « » . «» , , «», .
8. , Endless Battle 2
, , . , , , . , , , . Endless Battle 2 ,
.
PSVita Endless 2, XB1 PS4, Endless 2. , , . , PSVita , , PS4 XB1. «endurance» - . PC Endless Batlte 2 .
, Defender's Quest II — ! , «» Tower Defense, , -, , ? , — , ..
9.
— , , , . , , , , , .
, , , «»
. . — .
, Defender's Quest II. , PSVita, . PSVita, , Defender's Quest.