Das einfachste Arduino-Spiel mit einem 1602-Display - Teil 2

TEIL 2 von Anfang bis Ende


Wir machen weiterhin ein Spiel auf Arduino und nehmen dieses Spiel später in das Programm auf, das ich für das Auto mache. Basierend auf unserem Wissen werden wir ein zweites Spiel zum Spaß erstellen und die richtige Musik dafür machen.

Um zu verstehen, was wir jetzt schreiben müssen, müssen wir einen Plan für das erstellen, was wir haben und in welcher Form. Wir haben die Hauptfigur, er hat zwei Animationsbilder und wir haben es bereits in der letzten Lektion geschafft. Ich habe nicht angefangen, das Sprung-Sprite für ihn zu ändern, nicht weil ich zu faul war, ich sehe den Punkt darin einfach noch nicht. Als nächstes sollten wir eine Lebensbezeichnung haben, nur Herzen hintereinander zu machen ist nicht interessant, und die Zahlen sind auch banal. Machen wir drei Lebenspunkte und markieren sie zum Beispiel mit dem Batteriestand. Und zur Freude des Auges schlägt ein Herz in der Nähe dieser Batterie. Die Anzahl der Punkte pro Runde, die auf der linken Seite des Bildschirms eingestellt sind, ist rein digital und der Feind wird einen bösen Kaktus haben, zunächst einmal.

Wir haben uns für die Objekte entschieden und lassen uns sie zeichnen, sofort alle Sprites und sie aufschreiben.





Wir haben Sprites und können uns bereits vorstellen, wie sie auf dem Bildschirm aussehen werden. Nach der ersten Lektion werden wir sie in das Binärsystem schreiben. Denken Sie daran, wo die Null leer ist und wo sich die Einheit befindet. Unser Pixel brennt. Weiter:

"--------------------------------------------------------------------------" //   : byte Player_1[SYMBOL_HEIGHT] = {B01110,B01110,B00100,B01110,B10101,B00100,B01110,B01010,}; //   : byte Player_2[SYMBOL_HEIGHT] = {B00000,B01110,B01110,B00100,B11111,B00100,B01110,B01010,}; // : byte Enemy_1[SYMBOL_HEIGHT] = {B00010,B00110,B10111,B10110,B11111,B00110,B00110,B11111,}; // : byte Heart_L[SYMBOL_HEIGHT] = {B00000,B01010,B11111,B11111,B11111,B01110,B00100,B00000,}; // : byte Heart_R[SYMBOL_HEIGHT] = {B00000,B00000,B01010,B11111,B01110,B00100,B00000,B00000,}; //  : byte Battery1[SYMBOL_HEIGHT] = {B01110,B11111,B11111,B11111,B11111,B11111,B11111,B11111,}; //  : byte Battery2[SYMBOL_HEIGHT] = {B01110,B10001,B10011,B10111,B11111,B11111,B11111,B11111,}; //  : byte Battery3[SYMBOL_HEIGHT] = {B01110,B10001,B10001,B10001,B10011,B10111,B11111,B11111,}; //  : byte Battery4[SYMBOL_HEIGHT] = {B01110,B10001,B10001,B10001,B10001,B10001,B10001,B11111,}; "--------------------------------------------------------------------------" 

Jetzt haben wir Sprites und es ist Zeit, alles wiederzubeleben. Lassen Sie uns zunächst überlegen, welche zusätzlichen Funktionen wir benötigen. Da wir wissen, dass Arduino nicht perfekt funktioniert und an einigen Stellen sogar sehr launisch ist, versuchen wir, das Leben dieses Mikrocontrollers so weit wie möglich zu vereinfachen. Überladen Sie nicht und fordern Sie gleichzeitig die volle Rückgabe. Deshalb führen wir solche zusätzlichen Funktionen ein, die ihr eigenes Leben führen und alles tun, was wir brauchen:

- Herzanimation
- Heldenanimation
- Überprüfung des Heldenschadens
- Bewegung des bösen Kaktus
- Punkte sammeln (vorerst jede Sekunde +1, dann ändern)
- Aktualisieren des Bildschirms (aber dies ist nicht korrekt, höchstwahrscheinlich werden wir diese Funktion entfernen und eine weitere hinzufügen, ich mochte das Blinken des Bildschirms nicht, ich möchte Stabilität). Anschließend ersetzen wir diese Funktion durch das Löschen der vorherigen Position des Helden. Dadurch wird das tatsächliche Flackern des Bildschirms entfernt, und das Nullstellen des Bösewichts befindet sich im bösen Skript. Ich denke, es werden maximal ein oder zwei Zeilen angezeigt.
- Fernbedienung
- Loop- und Setup-Einstellungen

Wir möchten, dass wir eine Animation des Herzschlags haben. Wenn wir es in eine separate Funktion bringen und uns dazu zwingen, unser eigenes Leben zu führen, wird es für uns einfacher sein, die Arbeit zu verfolgen, und in Zukunft wird es einfacher sein, sie zu bearbeiten, da wir alles, gut oder fast alles an einem Ort haben. Dieser Code könnte in loop () angezeigt und auskommentiert werden, aber ich persönlich habe ihn in separate Funktionen unterteilt. Sie suchen nicht nach dem Code in der gesamten Liste und wissen, dass eine separate Funktion einzelne Elemente unseres Spiels steuert.

Jetzt werde ich Codeteile schreiben, wir werden sie am Ende verbinden und ein vollwertiges Skript erhalten, ich werde Ihnen jetzt die Essenz und Idee erklären, und dann werden wir das Puzzle zusammensetzen und das Ergebnis genießen.
Bevor ich den Code selbst starte, erkläre ich einige Punkte.

lcd.createChar // Dies ist ein Befehl, um auf den Bildschirm zuzugreifen und eine der Speicherzellen zum Aufzeichnen neuer Zeichen zu verwenden. Die Nummer der Zelle und, durch ein Komma getrennt, der Name der Variablen mit Informationen werden in Klammern angegeben.

Wir werden das Rendern über eine digitale Variable mit vier Ziffern steuern, damit die Animation korrekt ist. Wenn wir das Herz hier und da schlagen lassen möchten, passt die gewöhnliche Bool-Variable zu uns. Aber meine Idee ist anders, ein großer Schlag und zwei kurze, also wird es interessanter aussehen.

 "--------------------------------------------------------------------------" void HeartHit () //    { if (HeartControl == 0 || HeartControl == 2){lcd.createChar(1, Heart_L);} //      ,        if (HeartControl == 1 || HeartControl == 3){lcd.createChar(1, Heart_R);} //      ,        if (currentMillis - HeartHitBigCheck >= HeartHitBig) { //     if (currentMillis - HeartHitLightCheck >= HeartHitLight) { //    HeartHitLightCheck = currentMillis; //      if (HeartControl<3){HeartControl++;} //      ,           else {HeartControl = 0; HeartHitBigCheck = currentMillis;} //     ,          } } } "--------------------------------------------------------------------------" 

Ich möchte Ihre Aufmerksamkeit noch einmal auf diesen Code richten:
lcd.createChar (x, y); Zuordnung der Speicherzelle "x" aus (0 ... 7) Daten zur Anzeige auf dem Bildschirm "y"

Mach weiter =)

Jetzt haben wir einen Code, der den Effekt eines interessanten Herzschlags erzeugt. Er macht nichts Nützliches, nur Show-offs =)

Auf dieser Grundlage werden wir eine Animation unseres Helden erstellen. Es gibt große Vorteile. Je weiter wir in diesen Artikel eintauchen, desto mehr lernen Sie meinen Denkstil und desto weniger müssen Sie mir erklären und sich mehr auf den Code konzentrieren und die Idee aufbauen. Ich werde versuchen, mehr in den Skriptkommentaren zu erklären, um weniger außerhalb des Skripts zu schreiben.
Beginnen wir also mit der Hauptfigur und erstellen eine weitere Funktion dafür:

 "--------------------------------------------------------------------------" //             ().     =)  ,         . void PlAn () //     { If (JumpB == true && GGpozY == 0){ //    (      )   =     . if (currentMillis - JumpUPCheck >= JumpUP) { //     0.8f JumpB = false; GGclear (); GGpozY = 1; //   ,         .  = ;      ();     =  ; } } if (AnimPlayer == 1){lcd.createChar(0, Player_1);} //    ,        if (AnimPlayer == 2){lcd.createChar(0, Player_2);} //    ,        if (AnimPlayer < 2) //    ,     ,            { lcd.setCursor(GGpozX, GGpozY); //    lcd.write(0); //  } if (currentMillis - AnimatedTimeCheck >= AnimatedTime) { //   AnimatedTimeCheck = currentMillis; //   if (AnimPlayer == 2){AnimPlayer = 1;} //        else{AnimPlayer = 2;} //   ,  . } } void GGclear () //    { lcd.setCursor(GGpozX, GGpozY); //   lcd.print(" "); //  } "--------------------------------------------------------------------------" 

Nun, der Timer, ähm, oder besser gesagt unsere Punkte, die vergeben werden, schreiben wir einfach einen Timer und nehmen an, dass dies Punkte sind.

 "--------------------------------------------------------------------------" void timer () //   { if (currentMillis - DHTTimeRCheck >= DHTTimeR) //       { DHTTimeRCheck = currentMillis; //   Timer_z ++; //      lcd.setCursor(0, 0); //       lcd.print(Timer_z); //     } } --------------------------------------------------------------------------" 

Das ist alles. Je weiter desto einfacher.

Jetzt bleibt es, unseren Kaktus zu erarbeiten.

Seine Aufgabe ist einfach, zu erscheinen, den ganzen Weg von rechts nach links zu gehen und zu versuchen, den Helden zu berühren, um Schaden zu verursachen. Mit Schaden ist alles einfacher, eine Berührung - ein Treffer. Bisher werden wir mit zunehmender Komplexität nichts tun. Lassen Sie den Kaktus sich mit einer Geschwindigkeit von 0,5 f bewegen (Komplexität, dies sind bereits Ihre Hausaufgaben =)), oder auf Russisch ist eine halbe Sekunde ein Schritt.

Schauen wir uns an, wie dieser Code aussehen wird:

 "--------------------------------------------------------------------------" void enemy_go () //   { if (Emeny_check_1 == 0) //       ,            ,        ,            { Emeny_control = random (100); //  ,     ,    ,  ,    ,   . if (Emeny_control == 1) { //   = 1  100   . Emeny_check_1 = 1; //  ,  ,    bool      ,    ,       hitON = false; //          } } if (Emeny_check_1 == 1) //   ,     { if (currentMillis - TimeBlinkCheck >= TimeBlink) //  0.5f { TimeBlinkCheck = currentMillis; //   lcd.createChar(2, Enemy_1); //   2   lcd.setCursor(E1pozX, E1pozY); // 1   lcd.print(" "); //  E1pozX--; //     lcd.setCursor(E1pozX, E1pozY); // 2  lcd.write(2); //  if (E1pozX <= 0) //      { lcd.setCursor(0,1); //     lcd.print(" "); //  Emeny_control = 0; //   Emeny_check_1 = 0; //      E1pozX = 16; // - \/ \/ \/ E1pozY = 1; // -         } } } } "--------------------------------------------------------------------------" 

Es bleibt einiges und nach dem Testen jedes Stücks kann ich bereits das komplette Skript zusammenstellen, konfigurieren und für den Test auf Ihren Geräten bereitstellen.

Jetzt haben wir eines der wichtigsten Skripte in der Reihe. Dies ist ein Schadensprüfungsskript und die Einbeziehung eines Spielers in Abwesenheit von Leben. Das Drehbuch enthält nichts Übernatürliches. Daher können wir beginnen, es zu zerlegen (übrigens, wenn Sie vorsichtig wären, würden Sie feststellen, dass wir es beim Erstellen des Herzens nicht auf dem Bildschirm angezeigt haben, sodass Sie jetzt sehen werden, wo ich diesen Teil des Codes abgelegt habe):

 "--------------------------------------------------------------------------" void check_hit_gg_1 () //   { if (E1pozX == GGpozX && E1pozY == GGpozY && hitON == false){ //     Y ,   ,    LifeCheck -= 1; //    hitON = true; //           if (LifeCheck <= 0){ //      AnimPlayer = 50; //  loop () Emeny_check_1 = 50; //     lcd.clear(); //  lcd.setCursor(3, 0); //  lcd.print("GAME OVER"); //    } } else { // !         … lcd.setCursor(13, 0); //    … lcd.write(1); //   lcd.setCursor(14, 0); lcd.print("="); //    lcd.setCursor(15, 0); lcd.write(3); //   } } "--------------------------------------------------------------------------" 

Dieser Code ist sehr einfach. Seine Hauptfunktion besteht darin, zu überprüfen und zu warten, bis alle Bedingungen erfüllt sind, um das Spiel zu stoppen und uns mitzuteilen, dass wir verloren haben.

Jetzt ist die letzte Funktion das Management. In der Tat, wenn wir den obigen Code analysieren, wird es für uns einfacher als einfach erscheinen. Von der ersten Lektion an haben wir die Fernbedienungscodes herausgezogen, ich habe sie alle geschrieben, sie sehen so aus:

* CH- 0xFFA25D
* CH 0xFF629D
* CH+ 0xFFE21D
* << 0xFF22DD
* >> 0xFF02FD
* >|| 0xFFC23D
* - 0xFFE01F
* + 0xFFA857
* EQ 0xFF906F
* 0 0xFF6897
* 100+ 0xFF9867
* 200+ 0xFFB04F
* 1 0xFF30CF
* 2 0xFF18E7
* 3 0xFF7A85
* 4 0xFF10EF
* 5 0xFF38C7
* 6 0xFF5AA5
* 7 0xFF42BD
* 8 0xFF4AB5
* 9 0xFF52AD


Button _ Code (Achtung!) (Fernbedienungscodes können variieren)

Wer nicht gelesen hat, rate ich Ihnen, den 1. Teil zu lesen.

Etwas Ähnliches, dann haben Sie es und können einfach alles konfigurieren, was Sie brauchen.

Jetzt werden wir den einfachsten Algorithmus erstellen, basierend auf dem, was wir bereits wissen, und unser Spiel wird zum Leben erweckt.

 "--------------------------------------------------------------------------" void IRCheck () //    { if ( irrecv.decode( &results )) //   ,  { if (results.value == 0xFF18E7 && GGpozY == 1){ //    « 2 »    «» GGclear (); //     GGpozY = 0; //    2  ( ) JumpB = true; //        JumpUPCheck = currentMillis; //       } // 2 if (results.value == 0xFF10EF && GGpozX >= 0){ //             GGclear (); //      GGpozX -= 1; //     } // 4 if (results.value == 0xFF5AA5 && GGpozX <= 15){ //          GGclear (); //    GGpozX += 1; //   } // 6 if (results.value == 0xFF6897){ // 0 //  ,    … lcd.clear(); //   AnimPlayer = 1; //    LifeCheck = 3; //   Timer_z = 0; //   GGpozX = 8; // \/ \/ \/ GGpozY = 1; //       Emeny_check_1 = 0; //     E1pozX = 16; // \/ \/ \/ E1pozY = 1; //    . } irrecv.resume(); //    } "--------------------------------------------------------------------------" 

Schlussfolgerung aus dem oben geschriebenen Code:
Knopf 2 ist ein Sprung
Taste 4 ist ein Schritt nach links
Taste 6 ist ein Schritt nach rechts
Taste 0 setzt das Spiel zurück und startet es neu

Jetzt müssen wir Setup & Loop konfigurieren, alles geht bereits zu Ende. Wir haben alle zusätzlichen Funktionen erstellt und alles, was uns übrig blieb, war, alle Bibliotheken zu kleben und hinzuzufügen. Ich denke, wir werden uns die Variablen und die wichtigsten Einstellungen für Setup \ Loop bereits im allgemeinen Code ansehen. Beginnen wir also, und dann müssen Sie Strg + C & Strg + V drücken, und das ist alles =) und eine weitere unabhängige Entwicklung in diese Richtung, wenn Sie alle natürlich sind Mir hat es gefallen.

 "--------------------------------------------------------------------------" #include <IRremote.h> //   #include <Wire.h> // i2P  #include <LiquidCrystal_I2C.h> //  1602 LiquidCrystal_I2C lcd(0x3F,16,2); //   int AnimPlayer = 1; //   int GGpozX = 8; //     int GGpozY = 1; //     int Emeny_check_1 = 0; //   int Emeny_control = 0; //           int E1pozX = 16; //    int E1pozY = 1; //    int HeartControl = 0; //    int LifeCheck = 3; //   long Timer_z = 0; //   long AnimatedTime = 300; //     long AnimatedTimeCheck = 0; //   long HeartHitBig = 800; //     long HeartHitLight = 250; //     long HeartHitBigCheck = 0; //     long HeartHitLightCheck = 0; //     long BatteyBlink = 200; //      1  long BatteyBlinkCheck = 0; //   long JumpUP = 800; //     long JumpUPCheck = 0; //   long DHTTimeR = 1000; //  long DHTTimeRCheck = 0; //   long TimeBlink = 500; //    long TimeBlinkCheck = 0; //   long currentMillis = 0; //    bool JumpB = false; //    bool BatteryB = false; //     bool hitON = false; //    decode_results results; //      IRrecv irrecv(A0); //      enum { SYMBOL_HEIGHT = 8 }; byte Player_1[SYMBOL_HEIGHT] = {B01110,B01110,B00100,B01110,B10101,B00100,B01110,B01010,}; byte Player_2[SYMBOL_HEIGHT] = {B00000,B01110,B01110,B00100,B11111,B00100,B01110,B01010,}; byte Enemy_1[SYMBOL_HEIGHT] = {B00010,B00110,B10111,B10110,B11111,B00110,B00110,B11111,}; byte Heart_L[SYMBOL_HEIGHT] = {B00000,B01010,B11111,B11111,B11111,B01110,B00100,B00000,}; byte Heart_R[SYMBOL_HEIGHT] = {B00000,B00000,B01010,B11111,B01110,B00100,B00000,B00000,}; byte Battery1[SYMBOL_HEIGHT] = {B01110,B11111,B11111,B11111,B11111,B11111,B11111,B11111,}; byte Battery2[SYMBOL_HEIGHT] = {B01110,B10001,B10011,B10111,B11111,B11111,B11111,B11111,}; byte Battery3[SYMBOL_HEIGHT] = {B01110,B10001,B10001,B10001,B10011,B10111,B11111,B11111,}; byte Battery4[SYMBOL_HEIGHT] = {B01110,B10001,B10001,B10001,B10001,B10001,B10001,B11111,}; void setup() { Serial.begin(9600); //   irrecv.enableIRIn(); //   lcd.init(); //   Wire.begin(); //   lcd.backlight();//    } void loop() { currentMillis = millis(); //   IRCheck (); //    if (AnimPlayer < 3){ //   ,   ,    if (LifeCheck == 3) {lcd.createChar(3, Battery1);} //   if (LifeCheck == 2) {lcd.createChar(3, Battery2);} //   if (LifeCheck == 1) {//  ,  1   if (BatteryB == false){lcd.createChar(3, Battery3);} //    if (BatteryB == true){lcd.createChar(3, Battery4);} //   if (currentMillis - BatteyBlinkCheck >= BatteyBlink) {BatteyBlinkCheck = currentMillis; //  if (BatteryB == false) {BatteryB = true;} else {BatteryB = false;}} //  } timer(); //  check_hit_gg_1 (); //   PlAn(); //   HeartHit (); //    enemy_go(); //   } } void IRCheck () //    { if ( irrecv.decode( &results )) //   ,  { if (results.value == 0xFF18E7 && GGpozY == 1){ //    « 2 »    «» GGclear (); //     GGpozY = 0; //    2  ( ) JumpB = true; //        JumpUPCheck = currentMillis; //       } // 2 if (results.value == 0xFF10EF && GGpozX >= 0){ //             GGclear (); //      GGpozX -= 1; //     } // 4 if (results.value == 0xFF5AA5 && GGpozX <= 15){ //          GGclear (); //    GGpozX += 1; //   } // 6 if (results.value == 0xFF6897){ // 0 //  ,    … lcd.clear(); //   AnimPlayer = 1; //    LifeCheck = 3; //   Timer_z = 0; //   GGpozX = 8; // \/ \/ \/ GGpozY = 1; //       Emeny_check_1 = 0; //     E1pozX = 16; // \/ \/ \/ E1pozY = 1; //    . } irrecv.resume(); //    } } void timer () //   { if (currentMillis - DHTTimeRCheck >= DHTTimeR) //       { DHTTimeRCheck = currentMillis; //   Timer_z ++; //      lcd.setCursor(0, 0); //       lcd.print(Timer_z); //     } } //             ().     =)  ,         . void PlAn () //     { if (JumpB == true && GGpozY == 0){ //    (      )   =     . if (currentMillis - JumpUPCheck >= JumpUP) { //     0.8f JumpB = false; GGclear (); GGpozY = 1; //   ,         .  = ;      ();     =  ; } } if (AnimPlayer == 1){lcd.createChar(0, Player_1);} //    ,        if (AnimPlayer == 2){lcd.createChar(0, Player_2);} //    ,        if (AnimPlayer < 2) //    ,     ,            { lcd.setCursor(GGpozX, GGpozY); //    lcd.write(0); //  } if (currentMillis - AnimatedTimeCheck >= AnimatedTime) { //   AnimatedTimeCheck = currentMillis; //   if (AnimPlayer == 2){AnimPlayer = 1;} //        else{AnimPlayer = 2;} //   ,  . } } void GGclear () //    { lcd.setCursor(GGpozX, GGpozY); //   lcd.print(" "); //  } void enemy_go () //   { if (Emeny_check_1 == 0) //       ,            ,        ,            { Emeny_control = random (100); //  ,     ,    ,  ,    ,   . if (Emeny_control == 1) { //   = 1  100   . Emeny_check_1 = 1; //  ,  ,    bool      ,    ,       hitON = false; //          } } if (Emeny_check_1 == 1) //   ,     { if (currentMillis - TimeBlinkCheck >= TimeBlink) //  0.5f { TimeBlinkCheck = currentMillis; //   lcd.createChar(2, Enemy_1); //   2   lcd.setCursor(E1pozX, E1pozY); // 1   lcd.print(" "); //  E1pozX--; //     lcd.setCursor(E1pozX, E1pozY); // 2  lcd.write(2); //  if (E1pozX <= 0) //      { lcd.setCursor(0,1); //     lcd.print(" "); //  Emeny_control = 0; //   Emeny_check_1 = 0; //      E1pozX = 16; // - \/ \/ \/ E1pozY = 1; // -         } } } } void check_hit_gg_1 () //   { if (E1pozX == GGpozX && E1pozY == GGpozY && hitON == false){ //     Y ,   ,    LifeCheck -= 1; //    hitON = true; //           if (LifeCheck <= 0){ //      AnimPlayer = 50; //  loop () Emeny_check_1 = 50; //     lcd.clear(); //  lcd.setCursor(3, 0); //  lcd.print("GAME OVER"); //    } } else { // !         … lcd.setCursor(13, 0); //    … lcd.write(1); //   lcd.setCursor(14, 0); lcd.print("="); //    lcd.setCursor(15, 0); lcd.write(3); //   } } void HeartHit () //    { if (HeartControl == 0 || HeartControl == 2){lcd.createChar(1, Heart_L);} //      ,        if (HeartControl == 1 || HeartControl == 3){lcd.createChar(1, Heart_R);} //      ,        if (currentMillis - HeartHitBigCheck >= HeartHitBig) { //     if (currentMillis - HeartHitLightCheck >= HeartHitLight) { //    HeartHitLightCheck = currentMillis; //      if (HeartControl<3){HeartControl++;} //      ,           else {HeartControl = 0; HeartHitBigCheck = currentMillis;} //     ,          } } } "--------------------------------------------------------------------------" 

Durch Verbinden. Wir haben den Code erstellt, aber hier erfahren Sie, wie Sie die Drähte sammeln und was und wo Sie kleben müssen. Ich habe Ihnen dies nicht erklärt. Ich bin mir zwar so sicher, dass mehr als 80% derjenigen, die diesen Artikel lesen möchten, dies bereits wissen, aber für mich wird der Artikel nicht vollständig sein, wenn ich nicht die maximalen Informationen zur Verfügung stelle.



A5 Anzeige 1602 - SCL
A4 Anzeige 1602 - SDA
A0 IR-Sensor

arduino, 1602 ( , , ), , - ( 4 , ), , 2000 Rock n Roll Racing. =) arduino ( ), ~60 % 15% — 20%, … … , , , 10 , . , . - - , , . ? .

Alle, abonnieren, folgen den neuen Artikeln.

Vielen Dank für Ihre Aufmerksamkeit, Ciao Kakao!

PS: Ich kann die Leistung des Spiels immer noch nicht auf Video aufnehmen, die Kamera am Telefon ist gestorben und das ist sehr traurig. Aber ich werde mir etwas einfallen lassen und dem Artikel ein späteres Video hinzufügen.

Der erste Teil des Artikels ->

Source: https://habr.com/ru/post/de425367/


All Articles