Ръководство стъпка по стъпка за изграждане на C++ лабиринтна игра

Последна актуализация: 05/05/2026
Автор: C SourceTrail
  • Лабиринтните игри в C++ започват с мрежа в паметта, отделно състояние на играча и прост игрови цикъл, управляван от клавиатурен вход.
  • Колекционерски предмети, капани, точки, животи и време превръщат обикновената навигация в пълноценен геймплей цикъл с ясни цели и рискове.
  • Конзолните проекти преподават основни концепции – представяне на света, сблъсъци и актуализации на състоянието – които се мащабират директно към 3D DirectX лабиринтни игри.

Урок по игра лабиринт на C++

Ако някога сте искали да създадете своя собствена игра-лабиринт на C++, но сте се чувствали объркани от графични двигатели, физични библиотеки или аудио мидълуер, това ръководство е за вас. Ще се съсредоточим върху това, което наистина е важно в началото: вътрешната логика на играта, как светът живее в паметта и как играчът взаимодейства с него чрез клавиатурата. След като това ядро ​​е солидно, добавянето на визуален блясък или фантастични ефекти се превръща просто в още един слой, а не в невъзможна за изкачване планина.

Целта тук е да се създаде напълно играем лабиринт на C++, започвайки от проста конзолна версия и свързвайки я концептуално с това, което бихте направили в по-напреднал DirectX 3D проект, като например UWP „Marble Maze“. Ще видите как да представите лабиринта с решетки, да управлявате позицията на играча отделно от картата, да валидирате движението, да добавяте колекционерски предмети и капани, да управлявате животи, да проследявате времето и да структурирате основен игрови цикъл. По пътя ще посочим и как същите тези идеи се мащабират до графичен 3D лабиринт с по-богат вход и звук.

Проектиране на лабиринта като мрежа в паметта

Основата на нашата конзолна игра лабиринт е мрежа от редове и колони , която се съхранява изцяло в паметта, преди каквото и да било да се отпечата на екрана. Концептуално, всяка клетка в тази мрежа съхранява специфичен тип съдържание: стени, отворени пътища, началната позиция на играча или специални елементи като капани и колекционерски предмети. Този подход е от съществено значение, защото екранът трябва да е само отражение на текущото състояние на играта, а не мястото, където действително се намира логиката.

Много удобен начин за моделиране на тази мрежа в C++ е използването на vector<string>, където всеки низ представлява един ред от лабиринта и всеки символ в този низ е клетка. За да се запази мрежата чиста и последователна, всички редове трябва да имат еднакъв брой колони. Тази последователност опростява както рендирането, така и проверките за колизии, защото можете спокойно да предположите, че map[row][column] е валидно, стига да се придържате към известните граници.

Всеки символ на картата кодира различен тип плочкаНапример, можете да използвате # за маркиране на стени, които играчът не може да пресече, празни пространства (или специфичен символ като интервал или точка) за под, по който може да се минава, * за колекционерски предмети и може би X за опасни капани. Самият играч не е необходимо да бъде постоянно записван в мрежата; вместо това, вие го рендира временно, когато рисувате сцената въз основа на текущите му координати.

Първоначалната версия на програмата просто итерира през мрежата и отпечатва всеки ред в конзолата ред по ред. Въпреки че това изглежда елементарно, то вече дефинира граници, коридори и начално оформление. На този етап сте разделили две ключови идеи: структурата на лабиринта (статична) и състоянието на системата (динамично), дори ако динамичната част все още не е напълно имплементирана.

Умишлено малкият обхват е стратегически избор . В този проект не използвате двигатели, външни графични библиотеки или усъвършенствани прозоречни системи. Няма текстури, няма анимирани спрайтове, няма сложен аудио конвейер. Изходът се осъществява чрез стандартната конзола, което ви държи фокусирани върху правилата на играта, актуализациите на състоянието и организацията на кода, а не върху дебъгването на API за рендиране.

Представяне на състоянието на играча отделно от картата

След като картата на лабиринта е на мястото си, следващата ключова стъпка е да отделите играча от мрежата.Вместо да се постави постоянно P символ в масива на картата или vector<string>, вие поддържате местоположението на играча с две отделни променливи, обикновено целочислен ред и колона (например, playerRow намлява playerCol). Това разделение ви дава ясно разграничение между чертежа на лабиринта и текущото състояние на преиграването.

Когато рисувате сцената, преглеждате картата и инжектирате символа на играча само по време на рендериране.Често срещан модел е: за всяка клетка, ако нейните координати съвпадат с координатите на играча, нарисувайте 'P'; в противен случай, нарисувайте каквото е на статичната карта. Това улеснява движението на играча без постоянно пренаписване на данните от картата и намалява вероятността от смесване на статични стени с динамични обекти.

Самото движение може да се управлява от класически WASD клавиши.: W за нагоре, A за ляво, S за надолу и D за дясно. Всяко натискане на клавиш предлага нова потенциална позиция (например newRow = playerRow - 1 при движение нагоре). Преди действително да приложи тази актуализация, играта проверява дали целевата клетка се намира във валидните граници и не е герой от стената. Ако някоя от проверките е неуспешна, движението се отхвърля и играчът остава там, където се намира.

Тази стъпка на валидиране е сърцето на логиката на сблъсъка в лабиринт, базиран на мрежа . Вместо да позволите на героя да се плъзга през бариери или да изчезва извън екрана, вие прилагате правила, които правят света последователен: стените са плътни, лабиринтът има ограничения и играчът не може да се телепортира извън проектираната област. Дори в прост конзолен проект, това са същите концептуални проверки, които по-късно бихте внедрили с помощта на 3D колайдери и физични двигатели.

Тъй като играчът има свои собствени координати, вие сте свободни да разширите модела на света с допълнителни системи като точки, животи или инвентар, без да докосвате статичното представяне на картата. Лабиринтът си остава точно това: оформление от плочки. Всичко, което се променя по време на играта – позиция, събрани предмети, загубени животи – се намира в отделни променливи и структури, които се актуализират независимо всеки кадър.

Колекционерски предмети, точки и условия за печалба

Разходката из празен лабиринт бързо омръзва, така че следващата логична стъпка е да го напълните с колекционерски предмети.Една проста конвенция е да се използва * символ, който маркира клетки, съдържащи предмет, който играчът може да вземе. Те могат да представляват монети, скъпоценни камъни или общи „точки“, но от гледна точка на двигателя те са просто плочки със специфичен символ.

По време на всяко движение, след като се уверите, че играчът може да влезе в нова клетка, проверявате дали тази клетка съдържа колекционерски предмет.Ако е така, увеличавате брояча на точки (например, score++ или добавете някаква фиксирана стойност, например 10 точки) и след това актуализирайте картата на тази позиция до празна клетка, в която може да се ходи. Това имитира вземането на предмета и оставянето на пода свободен за бъдещи посещения.

За да поддържате играта целенасочена, можете да следите колко колекционерски предмета са останали в лабиринта.Лесен начин е да се инициализира брояч при зареждане на картата чрез сканиране за всички * герои. Всеки път, когато играчът събере един, намалявате брояча. Когато този брояч достигне нула, имате естествено условие за победа: играчът е изчистил лабиринта от всички предмети.

Като алтернатива или допълнително, можете да дефинирате конкретна изходна клетка като зона за печалбаНапример, долният десен ъгъл на лабиринта може да представлява съкровищницата. В предоставените идеи за код често ще виждате координати като (60, 40) or (60, 4) отметнати като специални позиции. След като играчът достигне една от тези плочки с всички задължителни изпълнени задачи (като събиране на всичко), вие показвате поздравително съобщение и спирате игровия цикъл.

Чрез комбиниране на колекционерски предмети с пространствена навигация, вие превръщате простото движение в истински геймплей . Играчът вече има причина да изследва задънени улици, да поема рискове около капани или да се връща през вече посетени коридори. На по-високо ниво това също отразява структурата на по-големите игрови дизайни, където изследването, плячкосването и изпълнението на цели са основни цикли, повтарящи се на много нива или среди.

Капани, животи и постоянни заплахи

За да добавите напрежение към лабиринта си, можете да добавите капани, които наказват небрежните движения.Често срещан модел е капаните да се представят с X герой на картата. За разлика от колекционерските предмети, те не са предназначени да изчезнат при задействане; те действат като трайни опасности, които играчът трябва да запомни и да избягва в бъдещи ходове.

Когато играчът стъпи върху клетка-капан, намалявате брояч на животите и нулирате позицията му до началните координати на лабиринта или до определена контролна точка. Това въвежда нов ресурс за управление – животи – и риск от неуспех, дори ако лабиринтът е технически разрешим. Ако броят на животите падне до нула, можете да прекратите играта и да отпечатате съобщение „Играта е свършила“.

Поддържането на капаните постоянни е повече от просто стилистично решение; то опростява менталния модел на играча . Колекционерските предмети променят картата, защото представляват ресурси, които могат да бъдат изчерпани. Капаните, от друга страна, променят състоянието на играча (животи, позиция), като същевременно оставят оформлението на света непроменено. Това ясно разделение помага да поддържате кода си подреден: поведението, свързано с картата, е различно от поведението, свързано с героя.

Същият този принцип се мащабира много естествено до по-напреднали двигатели и 3D среди . В игра тип „Marble Maze“, базирана на DirectX, дупките в дъската действат като капани: попадането в такава изпраща топчето обратно към най-скорошната контролна точка, без да променя геометрията на дъската. Самите контролни точки функционират като логически маркери в световното пространство, които влияят на поведението при прераждане, а не като разрушими или консумираеми плочки.

Ако искате да усъвършенствате конзолната имплементация, можете да комбинирате капани с рандомизирани оформления.Например, генериране на определени части от лабиринта с помощта на rand() и задаване на стойности на плочки като 0, 1 или 2, където някои представляват стени, други безопасни пътища, а няколко клетки стават кандидати за капани. По този начин всяко ново изпълнение може да се усеща различно, като същевременно се следва един и същ набор от основни правила.

Обработка на входни данни: От блокиране на четене до реален игрови цикъл

Много конзолни програми за начинаещи разчитат на блокиране на входа, чакайки потребителят да натисне Enter след всяка команда. Това е добре за менюта, но за истинска игра е необходим непрекъснат и бърз контрол. Вместо да спира изпълнението при всяко движение, програмата трябва да продължава да се повтаря, да проверява дали е натиснат клавиш и да актуализира данните само когато е необходимо.

В Windows често срещан модел е използването _kbhit() намлява _getch() от <conio.h>Функцията _kbhit() ви казва дали има ключ, чакащ във входния буфер, без да паузира целия процес. Ако върне true, вие вземате този ключ с _getch() и да го интерпретират като команда (WASD, клавиши със стрелки, Escape за излизане и т.н.). Ако няма клавиш, цикълът продължава, което ви позволява да прерисувате рамката, да актуализирате таймерите или да анимирате елементи.

Този неблокиращ вход превръща основната ви рутина в истински игрови цикъл . Вместо твърд цикъл „вход → актуализиране → рисуване → изчакване“, разделен от потребителски потвърждения, имате непрекъснат цикъл, който многократно: проверява за вход, актуализира позициите и състоянието и пречертава лабиринта. Такива цикли са в основата на почти всяка интерактивна игра, от текстови приключения до AAA 3D ​​шутъри.

Все още можете да предоставяте допълнителни команди освен движениетоНапример, примерните фрагменти от код споменават ключове като R за да излезеш от играта, N да поискате новогенериран лабиринт и дори ключ като T за да се покаже крайното време. Обработката им е просто въпрос на сравняване на натиснатия клавиш със специфични символи във входния блок.

В по-напреднали среди, като например UWP DirectX игра, входните източници се умножават . Същата концепция за лабиринт може да се управлява с клавиатура, геймпад, мишка, акселерометър или сензорен екран. API за всеки тип устройство е различен, но идеята е непроменена: игровият цикъл непрекъснато проверява най-новото входно състояние, преобразува го в игрови действия и го подава към симулационната логика, преди да рендира актуализирания кадър.

Подобряване на рендирането в конзолата и HUD

Дори в текстова конзола можете да направите много, за да изглежда лабиринтът ви като изпипана игра.Вместо постоянно да отпечатвате нови редове и да оставяте лабиринта да се превърта надолу, можете да обновявате една и съща област на екрана отново и отново. В Windows, извикването system("cls") При всеки кадър конзолата се изчиства, след което се рисува актуализираният лабиринт, играчът, резултатът и всички съобщения. Просто е и не е най-ефективното, но за малки проекти работи добре.

За да позиционирате текста точно там, където искате, можете да използвате помощник като gotoxy(int x, int y)Под Windows тази функция извиква SetConsoleCursorPosition() от windows.h, използвайки a COORD структура, за да зададете желаните координати. Чрез изрично преместване на курсора можете да поставите лабиринта в една част на екрана и HUD с резултат, животи и оставащи предмети в горната или долната част.

Скриването на мигащия текстов курсор също подобрява визуалното изживяванеМожете да направите това чрез SetConsoleCursorInfo(), конфигуриране на CONSOLE_CURSOR_INFO структура, така че bVisible е настроен на FALSEТова е малък детайл, но намалява разсейването и прави мрежата на лабиринта да изглежда като истинско игрално поле, а не като суров изглед на терминал.

ASCII символите ви позволяват да рисувате граници и стени с повече индивидуалностНапример, стойности като 205 (двойна хоризонтална черта), 186 (двойна вертикална черта), 201 (горен ляв ъгъл), 187 (горен десен ъгъл), 200 (долен ляв ъгъл) и 188 (долен десен ъгъл) могат да бъдат преобразувани в char и отпечатани, за да образуват декоративна рамка около лабиринта. Запълнените блокове като 219 са чудесни за дебели стени. Това придава на картата по-„игрален“ вид, а не произволна колекция от символи.

Можете дори да покажете малък „екран за начално меню“ или въвеждащ банер , който отпечатва името на проекта, авторските права и приятелско поздравление, след което изчаква произволен клавиш, преди да започне основния цикъл. Този вид въведение задава тона, обяснява основните контроли (например „Натиснете произволен клавиш, за да стартирате, използвайте клавишите със стрелки или WASD за движение“) и прави проекта да изглежда завършен, въпреки че използва само текстов изход.

Време, случайни лабиринти и съображения за производителност

Измерването на времето, необходимо на играча да реши лабиринта, е друго лесно, но мощно допълнение.Един лесен подход в C++ е да се използва clock_t тип и clock() функция от <time.h>В началото на бягане вие ​​извиквате clock() за да съхраните началното време. Когато играчът достигне целта или задейства условието за край, изчислявате изминалите тактове, като изваждате първоначалната стойност от нова clock() обади се.

Суровата стойност, върната от clock() представлява тактове на процесора, а не секундиЗа да го преобразувате в удобни за човека единици, разделяте на константата CLOCKS_PER_SECТова ви дава броя секунди, които играчът е прекарал в навигиране в лабиринта. Показването на това число и предоставянето на опция на потребителя като „Натиснете T, за да видите времето си“ може да добави стойност за повторното играене, докато се опитва да подобри личния си рекорд.

Ако се чувствате авантюристично настроени, можете също да генерирате нови лабиринти динамично.В един от фрагментите на кода има пример за присвояване на случайни стойности на клетки в картата с израз като map[i][j] = rand() % 3Можете да интерпретирате тези случайни числа като различни типове плочки, например 0 за стена, 1 за отворен път и 2 за специална плочка, по която може да се минава. С малко повече логика можете да ги трансформирате в последователни лабиринтни структури, които се променят всеки път, когато играчът поиска нов лабиринт с ключ като N.

Малко забавяне в цикъла може да попречи на натоварването на процесора на 100%.Функцията на Windows Sleep(30) паузира програмата за около 30 милисекунди между итерациите на основния цикъл. Това води до по-плавна производителност и избягва разхищение на процесорна мощност, което е особено важно, когато играта работи на гола конзола без вертикална синхронизация или ограничаване на кадрите.

Докато интегрирате синхронизацията и рандомизацията, дръжте логиката на играта и представянето ѝ отделно . Генерирането на случайни стойности трябва да засяга само данните от картата, изчисленията на времето трябва да актуализират променливите, а кодът за рендиране трябва просто да чете тези стойности, за да ги показва. Това разделяне ще улесни значително разширяването на играта по-късно, отстраняването на грешки или пренасянето на основната логика към друга платформа или система за рендиране.

От конзолен лабиринт до 3D DirectX мраморен лабиринт

След като се почувствате комфортно с конзолен лабиринт, е естествено да се запитате как това се превежда в 3D игра . В Windows 10 добър пример е проект на Universal Windows Platform, който използва C++ и DirectX за изграждане на 3D мраморен лабиринт. В този вид игра не местите герой клетка по клетка; вместо това накланяте физически изглеждаща дъска, така че стоманено или стъклено топче да се търкаля през лабиринта под симулирана гравитация.

Основната цел на 3D мраморния лабиринт все още е да насочва обект от началото до края, без да пада в дупки . Лабиринтът се държи като настолна играчка, построена от дърво, но имплементирана в код. Накланяте дъската – чрез вход от акселерометъра, контроли от геймпада, движения на мишката или жестове с докосване – и топчето реагира на физични изчисления, сблъсквайки се със стени и падайки в дупки, ако не внимавате. Контролните точки позволяват на топчето да се прероди на най-скоро достигнатото безопасно място след инцидент.

За да се изгради това на C++ с DirectX, се разчита на няколко ключови API и библиотеки . Direct3D и Direct2D рендират 3D лабиринта и всички 2D наслагвания, докато Windows Runtime API обработват жизнения цикъл на UWP приложението. Геометричните и физичните изчисления често използват DirectXMath за векторни и матрични операции, откриване на сблъсъци и интеграция на движение. За аудио, XAudio2 служи като основен енджин за обработка на музикални записи и звукови ефекти като търкаляне, удари или падане в дупки.

Въпреки че графичната страна е по-сложна, логическата структура отразява лабиринта на конзолата.Все още имате представяне на света (дъската и дупките), обект, който се движи (мрамора), препятствия (стени) и условия за неуспех (падане в яма). Това, което преди бяха герои в vector<string> сега се превръщат в 3D модели и сблъсъчни обеми, но вашият ментален модел – правила и цели – остава по същество същият.

Създаването на UWP игра-лабиринт с DirectX предполага, че вече познавате C++ и основни концепции на DirectX . Трябва да сте запознати с COM, как да управлявате ресурси като текстури и шейдъри и как UWP се различава от класическите настолни приложения по отношение на структурата на приложението. Официалната документация за Marble Maze обикновено разглежда аспекти като оформление на проекта, настройка на графичния конвейер, обработка на вход за докосване и сензори и интеграция на аудио, всичко това по модулен начин, така че да можете да използвате повторно компоненти в собствените си проекти.

Структуриране на кодовата база: Заглавки, Изходни кодове и Класове

Независимо дали работите в конзолна среда или в UWP DirectX проект, организирането на вашия код е от решаващо значение.Вместо да хвърляме всичко в main.cpp, по-добре е да разделите функционалността на специални заглавни и изходни файлове. Например, можете да създадете Maze.h намлява Maze.cpp за логиката на картата, Player.h намлява Player.cpp за състоянието на играча и отделен помощен файл за работа с конзолата (включително gotoxy и конфигурация на курсора).

Заглавките декларират интерфейса на вашите класове и функции, докато изходните файлове определят тяхното поведение.Включвате заглавни файлове в main.cpp използвайки #include така че компилаторът да знае за наличните типове и функции. Тази модулност подобрява четимостта и поддръжката, особено след като добавите повече елементи като врагове, множество нива или разширени HUD компоненти.

В C++ класовете, конструкторите и деструкторите управляват времето на живот на ресурсите . Конструкторът се изпълнява, когато се създава обект и може да задава начално състояние, да разпределя памет или да конфигурира манипулатори. Деструкторът се извиква, когато обектът се унищожи и е правилното място за освобождаване на ресурси, затваряне на файлове или освобождаване на динамично разпределена памет. Дори в малък проект тип лабиринт, правилното използване на деструктори помага за предотвратяване на течове и разсеяни манипулатори.

За частите, специфични за Windows, ще взаимодействате и с манипулатори и структури на системно ниво.Управлението на конзолата разчита на HANDLE стойности, получени чрез GetStdHandle(), което предавате на функции като SetConsoleCursorPosition() намлява SetConsoleCursorInfo()Отнасяйте се с тези манипулатори със същото уважение, както с всеки друг ресурс: или ги капсулирайте в малък клас, или ги управлявайте внимателно, за да не смесвате различни стандартни изходни манипулатори или да не губите представа за състоянието.

Тази дисциплинирана структура носи допълнителни дивиденти, когато преминете към усъвършенствани API-та като DirectX . Там ще се занимавате с устройства, контексти, вериги за размяна, буфери и текстури, всяка със свои собствени правила за създаване и унищожаване. Ако вече сте се научили да държите конзолните помощници, логиката на лабиринта и състоянието на играча отделени, ще ви се стори естествено да направите същото и с подсистемите за рендиране, вход и аудио в по-голям проект.

Събирайки всичко заедно, една игра-лабиринт на C++ – от прости конзолни лабиринти до пълен 3D мраморен лабиринт – се основава на няколко солидни идеи : представяне на света като структурирани данни в паметта, отделяне на състоянието на играча от картата, валидиране на движение и сблъсъци, добавяне на смислени цели с колекционерски предмети и условия за победа, внасяне на реална опасност чрез капани и животи и оркестриране на всичко с адаптивен игрови цикъл и ясна организация на кода. Овладяването на тези основи в минималистичен конзолен контекст прави прехода към по-богата графика и вход от множество устройства далеч по-малко плашещ и ви оставя с многократно използваем набор от инструменти за мислене за всяка бъдеща C++ игра, която решите да създадете.

Подобни публикации: