Maison  >  Article  >  Périphériques technologiques  >  Afin de sauvegarder les souvenirs d'enfance, le développeur a décidé d'utiliser une programmation ancienne : un remake haute définition d'un jeu en Flash

Afin de sauvegarder les souvenirs d'enfance, le développeur a décidé d'utiliser une programmation ancienne : un remake haute définition d'un jeu en Flash

WBOY
WBOYavant
2023-04-11 22:16:071492parcourir

Il y a plus de deux ans, Adobe a publié une annonce intéressante selon laquelle il mettrait fin au support de Flash le 31 décembre 2020, annonçant ainsi la fin d'une époque.

Deux ans plus tard, Adobe a déjà supprimé toutes les archives des premières versions de Flash Player du site officiel et bloqué l'exécution du contenu Flash. Microsoft a également mis fin à la prise en charge d'Adobe Flash Player et a interdit son exécution sur tout navigateur Microsoft. Le composant Adobe Flash Player a été définitivement supprimé via Windows Update en juillet 2021.

Après le retrait de Flash des étagères, dans un coin du monde, ce "vieux camarade" exerçait encore son énergie restante.

Hapland est un jeu de réflexion Flash lancé en 2005, et c'est aussi un souvenir d'enfance pour de nombreuses personnes. Dans le jeu, les joueurs doivent trouver des moyens d'ouvrir des niveaux en sollicitant l'aide des gens de ce monde sans les laisser manger par des monstres ou exploser par des mines terrestres.

Les graphismes de ce jeu sont dessinés en Flash, le code est écrit en Flash et toutes les animations sont réalisées dans la timeline Flash. Cela peut être compris ainsi : ce jeu a "Flash dans ses os".

En tant que membre de l'industrie du développement de jeux, Robin Allen a remarqué que les gens semblaient particulièrement apprécier le jeu Hapland. Il a donc souhaité apporter quelques correctifs à la version Steam du jeu Flash, notamment en dessinant de meilleurs graphismes, en modifiant la fréquence d'images Augmentez-la à 60FPS, ajoutez quelques "secrets" supplémentaires et plus encore.

Afin de sauvegarder les souvenirs denfance, le développeur a décidé dutiliser une programmation ancienne : un remake haute définition dun jeu en Flash

Que faire à ce moment-là ? L'auteur décrit le processus expérimental en détail.

Quelques expériences ratées

Tentative ratée 1 :

La première chose que j'ai essayée a été de demander à Flash d'exporter le jeu en tant qu'exécutable, mais cela a échoué car il avait les mêmes performances que 2005. Tout aussi mauvais . Je veux créer quelque chose qui fonctionne à des fréquences d'images contemporaines. Je veux me débarrasser de Flash Player.

Tentative échouée 2 :

Deuxièmement, j'ai passé trop de temps à bidouiller Adobe AIR (runtime de bureau Flash) et Starling (une bibliothèque pour dessiner des scènes Flash sur le GPU).

En fin de compte, j'ai abandonné, en partie parce qu'AIR avait tellement de problèmes et était terrible, mais aussi parce que je ne voulais pas me retrouver avec un résultat Adobe étrange à la fin de tout cela ; j'ai mon propre truc qui pourrait faire ce que je voulais. Par exemple, que se passe-t-il si je souhaite passer à Linux ?

La voie à suivre était évidente : je devais créer mon propre lecteur Flash.

Plan

Voici comment fonctionne Hapland. Il y a un arbre de sprites ici, et dans Flash, les sprites d'animation peuvent attacher du code à certaines images qui s'exécutent lorsque la flèche de lecture y arrive. Hapland utilise souvent cette approche. Les parcours des personnages du jeu sont tous des animations chronologiques très longues, et les personnages ont souvent des actions image par image, comme ouvrir des portes après leur fermeture, ou se déclencher s'ils atteignent un champ de mines avant qu'il n'explose.

Le petit « a » dans la chronologie est l'action du cadre.

Heureusement, les fichiers .fla ne sont que du XML. Il me suffisait de l'analyser, d'exporter les données pertinentes dans un format personnalisé simple et d'écrire un lecteur pour le lire, dessiner la scène, gérer l'entrée et exécuter l'animation.

Hapland est toujours un projet Flash, écrit et maintenu dans l'éditeur Flash ; seul Flash Player sera remplacé.

Vecteurs rastérisés

Flash prend en charge les graphiques raster, mais est en fait conçu pour les graphiques vectoriels. C'est pourquoi les animations Flash se chargent rapidement, même via une connexion commutée.

Tous les graphiques Hapland sont des graphiques vectoriels. Le GPU n'aime pas beaucoup dessiner des graphiques vectoriels, mais aime les grands lots de triangles texturés. Je dois donc pixelliser ces vecteurs.

J'ai décidé de les pixelliser hors ligne et de regrouper les fichiers raster dans le jeu. Ce serait amusant de les pixelliser et de devenir ce petit exécutable pendant que le jeu est en cours d'exécution, mais je ne veux pas avoir ces pièces mobiles supplémentaires. J'aime avoir autant de code que possible en cours d'exécution sur ma machine de développement afin de pouvoir le surveiller.

Flash stocke les graphiques vectoriels au format XML. Vous pourriez dire que XML est un mauvais choix pour les données graphiques, mais vous n'êtes pas chef de produit chez Macromedia. Jetez un œil à ceci :

Afin de sauvegarder les souvenirs denfance, le développeur a décidé dutiliser une programmation ancienne : un remake haute définition dun jeu en Flash

Données vectorielles vues dans un fichier .fla.

Je ne me plains pas, ça me facilite le travail.

Même si je n'ai pas accès aux spécifications, la rastérisation n'a pas posé de problème. Le modèle de courbe de Bézier pour les graphiques vectoriels est omniprésent depuis PostScript. Toutes ces API fonctionnent de la même manière. Après quelques essais et erreurs, j'ai écrit un programme pour analyser ces définitions de forme et les restituer au format PNG à l'aide de la bibliothèque CoreGraphics du Mac.

CoreGraphics est un choix discutable. Je l'ai choisi car je travaille sur Mac et j'ai beaucoup de dépendances. Mais cela a fonctionné, j'ai donc toujours dû pixelliser les graphiques sur Mac, même sur la version Windows. Si je devais recommencer, je choisirais probablement une bibliothèque multiplateforme.

Après le rendu de ces PNG, l'exportateur va les assembler dans un atlas ? Non, il trie simplement tout par hauteur, puis passe ligne par ligne comme le texte du document. C'est loin d'être optimal, mais c'est suffisant.

Pour plus de simplicité, l'atlas mesure 2048 × 2048 pixels, ce qui est la taille minimale de texture qu'une implémentation OpenGL 3.2 doit prendre en charge.

Afin de sauvegarder les souvenirs denfance, le développeur a décidé dutiliser une programmation ancienne : un remake haute définition dun jeu en Flash

Ensemble d'illustrations de Hapland 3.

La rastérisation des formes est très lente, donc pour garder des temps de construction raisonnables, je dois ignorer le rendu des éléments qui n'ont pas changé. Le format XML compressé utilisé par Flash comporte des champs de dernière modification pour chaque fichier, mais Flash ne semble pas les utiliser correctement, vous ne pouvez donc pas vous y fier.

Au lieu de cela, je hache simplement le XML pour chaque forme et je ne le reconstruis que s'il change. Même cela échoue car Flash aime parfois réorganiser les balises XML dans des objets inchangés, mais encore une fois, cela suffit.

Écrire des fichiers binaires en assembleur

L'exportateur écrit les données d'animation dans un format binaire personnalisé. Il parcourt simplement la chronologie image par image et écrit tous les changements pour chaque image.

J'ai pensé à écrire une liste d'assemblage ici au lieu d'écrire directement dans un fichier binaire, et j'aime ça. Il n'y a pas d'instructions CPU, juste des données, ce qui facilite le débogage car je peux consulter le fichier assembleur pour voir ce qui a été généré au lieu de parcourir les octets dans un éditeur hexadécimal.

output.bin

13 92 49 EC : BD 31 E8 FF
09 DD BE DE : C9 5A 1D 36
3F C0 4E 31 : 52 FD 41 C6
8B 5D C0 20 : 19 1F 5F 1F
54 97 8C 27 : 34 1F 30 EA
A9 A9 E0 55 : 40 29 A3 19
89 BC 5F 24 : 3A 98 FD B9
DE 15 F2 D4 : 2A B7 41 2C
4E 9D 37 D9 : E2 13 4B 01
36 3F 40 08 : AC 3C FF 84
E9 AE C5 2C : 11 2F 69 CF
63 CE 85 D1 : A7 CB B1 1A
5F 5B 60 1A : 77 99 71 B0
60 6E C4 C7 : 73 1F EA 1F
31 0D 0C 39 : B0 86 70 42

output.asm

; Left Side
timeline_132:; --- Left Side, Frame 0 ---
.frame_0:; --- Left Side, Frame 0, Layer 22 ---
db Quad
dd 0.152926, 0.162029, 0.184475, 1.000000 ; color
dd 799.599976, -20.950001dd 799.599976, 556.650024dd 46.000000, 556.650024dd 46.000000, -20.950001; --- Left Side, Frame 0, Layer 21 ---
; instance of shape [Left Side] [Wall Shadows] [Frame 0]
dd Shape
dw 1560

Lequel préféreriez-vous déboguer ?

J'aurais pu demander à l'exportateur d'écrire les octets dans un fichier tout en écrivant une liste distincte de texte dans un autre fichier sans utiliser l'assembleur, mais je ne l'ai pas fait car :

1) Les assembleurs existent déjà ;

2) Je n'ai pas besoin de les déboguer.

3) Ils prennent en charge les balises.

导出器的其余部分大多不够有趣;它只是 walk the tree 并将变换矩阵、颜色效果等事物,然后继续游戏程序本身。我选择用 C++ 编写这个,因为我已经知道它,并且新事物让我害怕。

场景图

Hapland 非常适合场景图。这是 Flash 使用的模型,Hapland 就是围绕它设计的,因此尝试使用不同的模型是没有意义的。

我将场景存储在内存中,作为一棵节点树,每个节点都有一个变换,可以自行绘制并接受鼠标点击。每个具有自己行为的游戏对象都是其自己类的实例,派生自 Node.js。「面向对象」目前在游戏开发圈子里并不流行,但我使用的是 Flash,所以显然不关心这个问题。

Hapland 使用的 Flash 功能,如颜色变换和遮罩,都是存在的。不过我没有像 Flash 那样实现任意遮罩,只是实现了矩形剪辑并编辑了我所有的图形,所以所有的遮罩都是矩形。

框架脚本

几乎所有的 Hapland 逻辑都包含在附加到时间轴帧的 ActionScript 中。要如何导出所有这些东西?我可不想在我的游戏中包含 ActionScript 解释器。

Afin de sauvegarder les souvenirs denfance, le développeur a décidé dutiliser une programmation ancienne : un remake haute définition dun jeu en Flash

一个简单的帧动作。

最后,我们使用了一些技巧,我的导出器从每一帧读取 ActionScript 并应用大量正则表达式以尝试将其转换为 C++。例如,crate.lid.play () 可能会变成 crate ()→lid ()→play ();。这两种语言在句法上非常相似,这对于许多更简单的框架动作来说效果很好,但它仍然留下了相当多的错误代码,除了手动重写所有剩余的框架动作之外别无他法。

对于 C++ 中的所有框架脚本,它们在构建时被提取并成为每个符号的 Node 子类上的方法。还会生成一个调度方法以在正确的时间调用,看起来像这样:

void tick() override {
 switch (currentFrame) {
 case 1: _frame_1(); break;
 case 45: _frame_45(); break;
 case 200: _frame_200(); break;
 }
}

需要指出的最后一件事是脚本系统最终是某种静态类型的,这有点难受。游戏输出的最终游戏对象如下所示:

struct BigCrate: Node {
 BigCrateLid *lid() { return (BigCrateLid *)getChild("lid"); }
 BigCrateLabel *label() { return (BigCrateLabel *)getChild("label"); }

 void swingOpen() { ... }
 void snapShut() { ... }
 void burnAway() { ... }
};

因此,即使一切仍然是大量的自动字符串名称查找,类型安全的单板会阻止你在错误的对象上调用错误的函数,从而使你免于在动态语言中遇到的那类烦人的 bug。

纵横比

HD 重置版游戏都会遇到画面拉伸的问题,最初的 Flash 游戏很多是页游,甚至没有全屏运行的能力,所以它们只是使用设计者喜欢的宽高比,大多是 3:2 左右。

如今最常见的纵横比似乎是 16:9,16:10 在笔记本电脑上也很流行。我希望游戏在其中任何一个方面看起来都不错,没有任何黑条或拉伸。要做到这一点的唯一方法是从原件上切掉一些部分,或者在上面添加一些部分。

所以,我为游戏画面画了两个矩形,一个比例为 16:9,另一个比例为 16:10。然后游戏根据屏幕的宽高比在它们之间进行插值,并使用插值矩形作为视图边界。只要所有重要的游戏元素都在这些矩形的交叉点内,并且它们的公共边界矩形不超出场景边缘,就可以很好地工作。

Afin de sauvegarder les souvenirs denfance, le développeur a décidé dutiliser une programmation ancienne : un remake haute définition dun jeu en Flash

Hapland 2 的 16:10 和 16:9 框,与原来的 3:2 不同。

Problèmes d'espace colorimétrique

Après quelques tests, j'ai découvert que Flash effectue un mélange alpha et des transformations de couleurs dans l'espace perceptuel plutôt que dans l'espace linéaire. C'est mathématiquement douteux, mais d'un autre côté, nous devons également savoir que de nombreux programmes graphiques fonctionnent de cette manière, vous voulez que vos outils grand public fonctionnent comme les gens l'attendent, même si c'est une sorte de problème pour les mathématiciens. Mais fondamentalement, c’est faux ! Cela peut causer des problèmes comme l'anticrénelage.

Lorsque vous rastérisez un graphique vectoriel et avez besoin d'une sortie anti-aliasée, le rastériseur génère une valeur alpha, appelée "valeur de couverture". Cela signifie que si un pixel donné est à moitié couvert par une forme vectorielle, ce pixel sera généré avec alpha = 0,5.

Mais dans Flash, quand quelque chose a un alpha de 0,5, cela signifie qu'il est perceptuellement à mi-chemin entre les couleurs de premier plan et d'arrière-plan.

Ce n'est pas du tout la même chose !

Les pixels blancs à moitié recouverts dessinés au-dessus de pixels noirs opaques ne doivent pas être perçus comme 50 % de gris. Ce n’est pas ainsi que fonctionne la lumière, et ce n’est pas ainsi que fonctionne la rastérisation vectorielle. Un rastériseur ne peut pas dire "ce pixel doit détecter xx% entre les couleurs d'arrière-plan et de premier plan" sans connaître la couleur d'arrière-plan.

Afin de sauvegarder les souvenirs denfance, le développeur a décidé dutiliser une programmation ancienne : un remake haute définition dun jeu en Flash

Mélange réalisé dans un espace perceptuel (sRGB). Haut : noir sur blanc transparent ; milieu : noir transparent sur blanc ; bas : même mélange de gris réalisé dans un espace linéaire (physiquement précis). Notez qu’une couverture de 50 % est différente d’une couverture de 50 % de gris.

Ainsi, nos formes rastérisées anti-aliasées utilisent une définition de l'alpha, tandis que notre transparence alpha exportée par Flash, nos dégradés et nos transformations de couleur utilisent une autre définition. Mais nous n'avons qu'un seul canal alpha dans notre pipeline de rendu. Alors, comment le moteur de rendu doit-il interpréter la valeur alpha ? S'il les interprète comme des facteurs de fusion perceptuels, les objets translucides sembleront corrects, mais les bords anticrénelés de tout sembleront faux. S'il les interprète comme des valeurs de couverture, alors vice versa. Quelque chose ne va toujours pas !

Ici, je pense qu'il n'y a que deux solutions sérieuses : 1) définir deux canaux alpha, un pour la superposition et un pour le mélange perceptuel ; 2) pixelliser toutes les formes sans AA, dessiner le tout dans un très grand framebuffer, puis filtrer. vers le bas pour le réduire.

Je dois admettre qu'aucune de ces idées n'a été mise en œuvre. Ces choses translucides n'avaient pas l'air correctes dans Flash et dans le jeu, j'ai juste ajusté progressivement les graphismes jusqu'à ce que le jeu semble correct. Les objets transparents dans Flash ne sont jamais exactement ce que je voulais qu'ils soient, mais ils ne sont pas nombreux et ce n'est pas grave.

Pour m'assurer que tout le reste était correct, j'ai fait un graphique "test de couleur" avec un tas de couleurs à différentes intensités, un effet de rotation de teinte 10, etc., j'ai laissé le jeu l'afficher et je me suis assuré qu'il fonctionnait correctement en Flash.

Afin de sauvegarder les souvenirs denfance, le développeur a décidé dutiliser une programmation ancienne : un remake haute définition dun jeu en Flash

devient une question de comparaison des couleurs.

Framerate

Les jeux Flash originaux ont un framerate nominal de 24FPS, mais en réalité, ils fonctionnent à n'importe quel framerate souhaité par Flash Player. Avec Flash, vous pourriez demander 24FPS et obtenir 15FPS, ou demander 30FPS et du coup obtenir 24FPS, ce qui ne semble pas du tout rigoureux.

Je voulais refaire le jeu à 60FPS, ce qui signifiait jouer avec le fait qu'Hapland devait jouer à environ 24FPS lors de sa création. Les outils d'animation de Flash sont basés sur des images discrètes plutôt que sur un temps continu.

J'ai d'abord demandé à l'exportateur de doubler toutes les images, en exportant deux images pour chaque image de la timeline, cela a directement amélioré les 24FPS à 48FPS, mais toujours pas 60, l'animation requise était toujours 25% plus rapide. La solution est un travail manuel à l'ancienne : parcourir le jeu complètement et ajouter manuellement des images supplémentaires à l'animation qui semble désormais trop rapide.

À ce stade, nous avons une assez bonne conversion C++ du jeu Hapland qui fonctionnera sûrement sur les ordinateurs modernes pendant au moins un an ou deux. Mais je ne pouvais tout simplement pas m'empêcher de penser que je devais essayer d'apporter une valeur supplémentaire, donc en ajouter de nouvelles était inévitable. En plus de redessiner de nombreux anciens graphiques et animations, j'ai également apporté des modifications majeures.

Gagnez du temps

Je pense qu'Hapland 3 doit être rendu moins écrasant. Les niveaux de ce jeu sont longs et il y a beaucoup d'endroits où vous mourez et devez recommencer, peut-être que cela aurait été amusant en 2006, mais nous sommes adultes maintenant et nous n'avons pas le temps pour ça.

Enregistrer l'état est une fonction que l'émulateur devrait avoir. Si vous appuyez sur "Enregistrer l'état", il enregistrera l'intégralité de l'état du jeu en cours en vidant la mémoire de la console dans un fichier. Ensuite, si vous vous trompez, appuyez sur "Loading State" et vous serez de retour près de l'endroit où vous vouliez réessayer.

L'implémentation des états de sauvegarde dans les jeux Flash originaux n'est pas réalisable car Flash ne donne pas aux programmeurs accès à l'intégralité de son état. Mais comme j'utilise tout mon propre code cette fois, c'est possible.

J'ai quelque chose appelé une Zone qui est juste un allocateur qui alloue toute sa mémoire dans un bloc de taille fixe. Tous les nœuds de scène sont alloués dans la zone actuelle. Pour implémenter la sauvegarde et la restauration, je n'ai besoin que de deux zones, la zone active et une "zone d'état de sauvegarde" distincte. Pour enregistrer l'état, je mémorise la zone active dans la zone d'état enregistré. Pour charger l'état, je renvoie memcpy dans l'autre sens.

Niveaux répétés

Le temps de jeu de Hapland n'est pas particulièrement long, bien qu'il y en ait trois au total, mais nous voulons toujours donner aux joueurs quelques heures de jeu supplémentaires. J'ai donc décidé de donner à chaque jeu une "Deuxième Quête" - une version modifiée du niveau original avec une disposition et des énigmes légèrement différentes. Réaliser une telle deuxième quête demande moins d'efforts que créer un jeu entièrement nouveau, mais apporte quand même une valeur supplémentaire.

Créer Second Quest m'a permis de recommencer le développement de jeux de réflexion Flash pour la première fois depuis environ 15 ans, et honnêtement, cela m'a fait du bien. L'interface utilisateur Flash rétro est géniale, les boutons ont des bords, les icônes sont réalistes et l'espace est bien utilisé.

L'utilisation de l'ancienne interface utilisateur me donne l'impression d'être un archéologue découvrant une technologie romaine oubliée. L’art perdu de la conception d’interface utilisateur, soigné.

Afin de sauvegarder les souvenirs denfance, le développeur a décidé dutiliser une programmation ancienne : un remake haute définition dun jeu en Flash

De quel genre de magie s'agit-il ?

Bien que Flash présente de nombreux bugs, soit lent et manque de certaines fonctions très basiques, je ne déteste pas l'utiliser, et bien sûr, il est plus confortable d'utiliser des applications modernes.

Pour éviter que la deuxième mission ne ressemble trop à la première, ils avaient besoin de nouveaux arrière-plans et la scène entière a été inversée horizontalement.

Afin de sauvegarder les souvenirs denfance, le développeur a décidé dutiliser une programmation ancienne : un remake haute définition dun jeu en Flash

Hapland 3.

Afin de sauvegarder les souvenirs denfance, le développeur a décidé dutiliser une programmation ancienne : un remake haute définition dun jeu en Flash

Deuxième quête de Hapland 3.

Musique

Pour le BGM, j'ai utilisé le contenu de mon propre disque dur et j'ai créé de la musique supplémentaire pour créer une bande-son d'ambiance rapide pour chaque jeu. Une fois, alors que j'étais en vacances au Japon, j'ai fait une séance d'enregistrement sur le terrain au sommet d'une montagne sans raison apparente et c'était bien de pouvoir l'utiliser pour quelque chose. J'ai trouvé un musicien sur Internet pour faire la musique de l'écran titre et j'ai enregistré moi-même quelques accords de guitare pour le générique de fin, qui étaient noyés dans les effets, donc on ne peut pas dire que je suis un mauvais apprenant en guitare.

Au niveau des outils, j'utilise Logic ou Live selon la musique. Je trouve que Logic est meilleur pour l'enregistrement et Live est meilleur pour la conception sonore.

Système de réalisations

Sur Steam, les joueurs aiment toujours voir les réalisations, ce qui n'est pas facile à gérer. Le paramétrage des réalisations dépend des idées du concepteur du jeu, mais ce n'est en fait pas grave.

Télécharger un système de réussite sur Steam est pénible, vous ne pouvez pas simplement définir une liste et la transmettre à leur outil de ligne de commande, mais vous devez plutôt parcourir le cadre PHP lent et déroutant du site partenaire Steam et en ajouter un. par un.

Il semble que si vous êtes un grand et important studio de jeux, vous n'avez pas à supporter cela et ils vous fournissent un outil de téléchargement groupé, mais je n'en fais évidemment pas partie. J'ai donc regardé les appels HTTP qu'il a créés, enregistré mon cookie de connexion et écrit le mien.

Après quelques révisions, j'ai opté pour un modeste ensemble de réalisations : une pour terminer chaque jeu Hapland, une pour chaque deuxième quête et deux pour débloquer de plus grands secrets. Tout secret stupide et obscur que personne ne peut découvrir n’est pas une réussite, vous devez être satisfait de ce qui se passe.

Afin de sauvegarder les souvenirs denfance, le développeur a décidé dutiliser une programmation ancienne : un remake haute définition dun jeu en Flash

Système de réalisations sous l'interface utilisateur Steamworks.

Le problème de la notarisation

Bien que je développe principalement des jeux sur mon Mac, au cours du processus de développement, Apple a inventé la « notarisation ». Si vous exécutez une application sur une nouvelle version de MacOS, elle fera une requête réseau à Apple. demandant si le développeur de l'application paie à Apple une redevance annuelle. Si un développeur ne paie pas les frais annuels, MacOS affichera une boîte de dialogue indiquant fortement que l'application est en panne et refuse de se lancer.

Par conséquent, Windows sera la première et peut-être la seule plateforme de sortie pour ce jeu.

Bibliothèques utilisées

Pour le logiciel qui est finalement livré à l'utilisateur final, nous souhaitons généralement réduire les dépendances au minimum, mais l'utilisation de logiciels de haute qualité est également nécessaire. En plus d'OpenGL et des systèmes d'exploitation standard, voici la liste complète des bibliothèques avec lesquelles l'exécutable Hapland Trilogy finit par se lier :

    Conclusion
  • Voici le problème, si vous maîtrisez la technologie, les gens ne remarqueront même pas ce qu'il y a derrière eux lorsqu'ils jouent au jeu, alors parfois cela vous donne envie de dire : Hé, regarde ce que j'ai fait !

Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!

Déclaration:
Cet article est reproduit dans:. en cas de violation, veuillez contacter admin@php.cn Supprimer