Creating a Scripting System in C++ Part III: Dynamic Loading
Covers two approaches to loading scripts, as well as scanning and matching.
// the basic instruction class Instruction { public: Instruction(opcode code) : _code(code), _datsize(0), _data(0) {} Instruction(opcode code, const char* data, size_t dataSize) : _code(code), _datsize(dataSize), _data(new char[dataSize]) { memcpy(_data, data, dataSize); } // we must store our own internal copy Instruction(const Instruction& instr) : _code(instr._code), _datsize(instr._datsize), _data(new char[_datsize]) { memcpy(_data, instr._data, _datsize); } void operator=(const Instruction& instr) { delete[] _data; _size = instr._size; _data = new char[_size]; memcpy(_data, instr._data, _datsize); } ~Instruction() { delete[] _data; } // and we must then clean up after it opcode Code() const { return _code; } const char* Data() const { return _data; } private: opcode _code; size_t _datsize; char* _data; // additional data };// data buffer to encapsulate copy-on-write functionality class Buffer { public: Buffer() : _size(0), _data(0) {} Buffer(const char* data, size_t size) : _size(size), _data(new char[_size]) { memcpy(_data, data, _size); } Buffer(const Buffer& buf) : _size(buf._size), _data(new char[_size]) { memcpy(_data, buf._data, _size); } ~Buffer() { delete[] _data; } void operator=(const Buffer& buf) { delete[] _data; _size = buf._size; _data = new char[_size]; memcpy(_data, buf._data, _size); } const char* Data() const { return _data; } private: size_t _size; char* _data; }; // the basic instruction class Instruction { public: Instruction(opcode code) : _code(code) {} Instruction(opcode code, const char* data, size_t dataSize) : _code(code), _data(data, dataSize) {} opcode Code() const { return _code; } const char* Data() const { return _data.Data(); } private: opcode _code; Buffer _data; // additional data };const int BUFFER_SIZE = 256; int main() { char buffer[BUFFER_SIZE]; // begin reading text lines from user cout << "Input script instructions:" << endl; cin.getline(buffer, BUFFER_SIZE); // an input processing loop will follow once we can scan the input return 0; }opcode arg1 arg2 ... argNopcode1 arg1 arg2 ... argN opcode2 arg1 arg2 ... argN ...and so ontypedef std::vector<char> CharArray; // an array of char tokens class Scanner { public: bool ScanLine(const std::string& line); // tokenize a line and return success const CharArray& GetTokens() const; // get tokens from most recent scan };int main() { VirtualMachine vm; Scanner scanner; vector<Instruction> instrList; char buffer[BUFFER_SIZE]; string input; // begin reading text lines from user cout << "Input script instructions:" << endl; while (input != "end") { cin.getline(buffer, BUFFER_SIZE); input = buffer; // may as well use the string if (scanner.ScanLine(input)) { const CharArray& tokens = scanner.GetTokens(); opcode code = static_cast<opcode>(tokens[0]); instrList.push_back(Instruction(code, &tokens[1], tokens.size()-1)); } else cout << "Invalid Instruction!" << endl; } // as a safety precaution, it couldn't hurt to have redundant ends instrList.push_back(Instruction(op_end)); . . . . . . // obtain a variable count size_t varCount; cout << "Input required variables count: "; cin >> varCount; Script script(instrList, varCount); // load the script and save the id size_t scriptID = vm.Load(script); // execute script by its id cout << endl << "***EXECUTING SCRIPT***" << endl; vm.Execute(scriptID); // check out the variable states cout << endl << "***VARIABLE STATES***" << endl; vm.ShowVariableState(); return 0; }- if line contains no content, break out prematurely
- scan an opcode
- for the entire length of the line:
- skip over white space
- scan in next numerical value
private: bool SkipSpacing(const std::string& line); bool ScanCode(const std::string& line); bool ScanNum(const std::string& line);private: CharArray _tokBuffer; size_t _offset;An offset into the string "line" and an array of tokens will be initialized at the start of ScanLine() and will be used and modified throughout by the specialized utility functions just described. Now we can put some code into ScanLine(): bool Scanner::ScanLine(const std::string& line) { // reset offset and token buffer _offset = 0; _tokBuffer.clear(); // check for an empty line if (line.empty()) return false; // check for valid line content if (!SkipSpacing(line)) return false; // check for a valid opcode if (!ScanCode(line)) return false; size_t len = line.length(); while (_offset < len) // scan args until the end of line { if (!SkipSpacing(line)) // get to next arg return true; // unless we're done if (!ScanNum(line)) return false; } return true; }bool SkipSpacing(const std::string& line) { while (isspace(line.c_str()[_offset])) ++_offset; if (line.c_str()[_offset] == 0) return false; return true; } bool ScanCode(const std::string& line) { size_t begin = _offset; while (isalpha(line.c_str()[_offset])) ++_offset; return MatchCode(std::string(line, begin, _offset-begin)); } bool ScanNum(const std::string& line) { size_t begin = _offset; while (isdigit(line.c_str()[_offset])) ++_offset; if (_offset == begin) // were any digits scanned? return false; std::string number(line, begin, _offset-begin); _tokBuffer.push_back(static_cast<char>(atoi(number.c_str()))); return true; }typedef std::vector<std::string> StringArray; . . . private: StringArray _codeNames; . . .bool MatchCode(const std::string& str) { char codeVal; StringArray::iterator itr = _codeNames.begin(); for (codeVal = 0; itr != _codeNames.end(); ++itr, ++codeVal) { if (str == *itr) { _tokBuffer.push_back(codeVal); return true; } } return false; }public: Scanner(const StringArray& codeNames) : _codeNames(codeNames) {} Scanner(const std::string* codeNames, size_t nameCount) { _codeNames.resize(nameCount); const std::string* end = codeNames+nameCount; std::copy(codeNames, end, _codeNames.begin()); }int main() { string codeList[num_codes] = { "talk", "print", "set", "inc", "dec", "add", "end" }; VirtualMachine vm; Scanner scanner(codeList, num_codes); . . .// enumerations describing the bytecode instructions the VM is able to process enum opcode { op_talk=0, op_print, // our new printing code // variable manipulators op_set, // char, char : destination index, value to set op_inc, // char : index to increment op_dec, // char : index to decrement op_add, // char, char, char : dest index, srce index1, srce index2 op_end, // not an opcode num_codes // value is the number of opcodes if first opcode has value of 0 };class File { public: bool Close() { assert(Good()); return (fclose(_fp) == 0); } bool Good() const { return (_fp != 0); } bool Bad() const { return (_fp == 0); } // file detection static bool Exists(const std::string& fileName) { . . . } // file length (only use before reading/writing) long Length() { /* use fseek() to determine file length */ } protected: File() : _fp(0) {} // this class should not be directly instantiated ~File() { if (_fp) fclose(_fp); } . . . FILE* _fp; };class ReaderFile : public File { public: ReaderFile() {} ReaderFile(const std::string& fileName) { Open(fileName); } bool Open(const std::string& fileName) { . . . } // reading int Read(char* dstBuf, size_t len) { /* return an fread() call here */ } // for many basic variable types int Read(particular_type& val) { /* typecast particular_type and call Read() with proper size*/ } }; class WriterFile : public File { public: WriterFile() {} WriterFile(const std::string& fileName) { Open(fileName); } WriterFile(const std::string& fileName, bool append) { Open(fileName, append); } bool Open(const std::string& fileName) { /* Truncates an existing file, rather than appending. */ } bool Open(const std::string& fileName, bool append) { . . . } // writing int Write(const char* srcBuf, size_t len) { /* return an fwrite() call here */ } // for many basic variable types int Write(particular_type val) { /* typecast particular_type and call Write() with proper size*/ } };class TextReaderFile : public File { public: TextReaderFile() {} TextReaderFile(const std::string& fileName) { Open(fileName); } bool Open(const std::string& fileName) { . . . } // reading int Read(char* buffer, size_t len) { /* call fread() */ } int Read(std::string& str, size_t len) { /* call fread() */ } }; class TextWriterFile : public File { public: TextWriterFile() {} TextWriterFile(const std::string& fileName) { Open(fileName); } TextWriterFile(const std::string& fileName, bool append) { Open(fileName, append); } bool Open(const std::string &fileName) { /* Truncates an existing file. */ } bool Open(const std::string& fileName, bool append) { . . . } // writing int Write(const char* str) { /* call fwrite() */ } int Write(const std::string& str) { /* call fwrite() */ } };// prompt user to enter a filename cout << "Enter path/name of script file: "; cin.getline(buffer, BUFFER_SIZE); // attempt to open the file TextReaderFile file(buffer); if (file.Bad()) { cout << "Could not open file!" << endl; return 0; } // read in file data string fileData; fileData.resize(file.Length()); file.Read(fileData, fileData.length());size_t begin = 0, end = 0, lineNum = 1, varCount = NUM_VARIABLES; // feed data into scanner and build instructions while (end != string::npos) { // grab a line from the file data end = fileData.find_first_of("\n", begin); string line(fileData, begin, end-begin); begin = end+1; // move past '\n' character // scan the line if (scanner.ScanLine(line)) { const CharArray& tokens = scanner.GetTokens(); opcode code = static_cast<opcode>(tokens[0]); instrList.push_back(Instruction(code, &tokens[1], tokens.size()-1)); } else { cout << "Invalid Instruction!" << endl; cout << "Line number: " << lineNum << endl; cout << "Line: " << line << endl; } ++lineNum; } // as a safety precaution, it couldn't hurt to have redundant ends instrList.push_back(Instruction(op_end));Variables: 4 ... list of instructions ...size_t . . . varCount = NUM_VARIABLES;- Outputting the variable state to a file instead of the console.
- Creating an instruction to display a particular variable state.
- Scanning in the desired variable count from a script file to allow it to be determined at run-time.
- Implementing instructions to allow the system to drive something you've written in the past.
Related Tutorials
Balancing Game Development and Creative Direction in Indie Production
A practical look at how indie developers can balance creative direction with hands-on game development. This article co…
My Unreal Engine Development Process: From Core Idea to Playable Build
A practical overview of my Unreal Engine development process, covering how I move from a core game idea to a playable b…
Introducing LaneGraph: The Ultimate Road Network Solution for Unity
Discover the power of LaneGraph, a lightweight and flexible lane-based navigation system for Unity. LaneGraph makes it…
Retargeting Mixamo Characters with Root Motion In Unreal Engine 5.4.
I have always found Retargeting Mixamo Characters To have Root Motion is a serious lengthy Tast, Recently I stumbled up…
How To Make A SIMPLE Main Menu In Unity
In this tutorial for unity, i go over how to make a simple main menu for unity, it's an unlisted video because i do not…
Guide to Gameplay Balance
A perspective on competitive gameplay balance, from a background of "shooter" sandbox design.
Discussion
More from Greg Rosenblatt
Creating a Scripting System in C++ Part V: Functionality
The latest installment in this series illustrates a way to implement primitives to support the abstraction of a high-le…
Creating a Scripting System in C++ Part IV: The Stack and Program Flow
This article will introduce a stack and program flow concepts which will eventually allow modeling higher-level languag…
Creating a Scripting System in C++ Part II: Data Manipulation
Explains how to enable your program to handle dynamic situations by adding optional data components to your instruction…
Discussion