TW - 'Emotions' RTS Tutorial 1

Posted by TwistyWristy on Jan. 17, 2007, 7:11 a.m.

This material is Copyright 2007 Sheldon Allen

This material is intended for your personal use only.

You do not have permission to copy it, redistribute it or profit from it in anyway without written permission from the author.

Any included files and their content, may be used for any non-commercial purpose.

Contact the author for information regarding commercial use.

Emotions RTS Tutorial - 1

Previous Part (Intro)

Included Files:

Editable .gm6 file

Welcome to the start of the 'Emotions' RTS Tutorial.

For the first few lessons, we will be building up a basic infrastructure for the engine/game, things that are needed for almost any RTS.

It's a pain having to wait for the 'real' stuff, but once we get the basic stuff together, we can really get to it!

Also, because we're building up the infrastructure, some of the code really isn't something to discuss.

It will be more of a code dump with lots of commentation and will talk about a few of the more interesting things.

The focus for today will be selections.

There's many different ways to select and deselect units:

- Click Select

- Drag Select

- Shift to Add Units

- Ctrl to Deselect Units

- Hotkey Select (0-9)

- Double-Click Type Selection (Select Units of the same type/class)

The main part of selecting and deselecting units requires getting the mouse code together.

Basically, there are two choices for deciding on how to handle the input.

The first way is to use the built-in mouse events and the second is to code our own mouse handling routines.

While the first method is undoubtedly faster than coding everything ourselves, we will still have to add code ourselves for dragging and double-clicking, all spread out through several events.

If we do things ourselves, everything is in one script, one place.

It's not any harder than using the built-in events, once we know the right functions we need.

Since all the code is in one place, it's much easier to make changes to everything.

We also gain greater control over the input…

Later on, it'll be easier to implement mouse sensitivity, swap mouse buttons and other such mouse settings.

So, we are going to go with option two;

if you still want to use events, you'll have to tweak things a little to work…

Before we get started with the coding, let's setup a few sprites to represent the units as well as setup some objects.

First, we'll need three objects for the units.

The first is called obj_unit, and will be the parent of all other unit objects.

The other two are called obj_unit_happy and obj_unit_mad.

Set them both to their respective sprites and their parent to obj_unit.

Now, the only reason we need three objects is because we have no way to differentiate between the units without using the room creation code to set a variable and it all gets to be a big mess.

We'll have a World Creator later, so using three objects will do for the time being..

The code for obj_unit is very simple.

In the create event we initialize a few variables:

obj_unit Create Event

Quote:

selected = 0 // Whether the unit is selected or not

direction = 270 //The direction the unit is facing

image_index = direction/360*image_number //Display the correct sub-image

The end step is used to choose the correct sub-image:

obj_unit End Step Event

Quote:

image_index = direction/360*image_number //Display the correct sub-image

Finally, in the Draw Event, the actual unit is drawn, as well as a ring if selected:

obj_unit Draw Event

Quote:

if selected{

draw_set_color(c_red)

draw_ellipse(x,y,x+50,y+25,false) // Draw an Ellipse under the unit (looks better than a circle)

//draw_circle(x,y,50,false) // Draw a circle under the unit

}

draw_sprite(sprite_index,-1,x,y)

Bam! No problemo, right? (I hope. [GRIN])

With the units squared away, it's time to get to the selection part.

Now, the first step is to keep track of the state of the mouse buttons.

A mouse button has several states:

Double Clicked (Clicked twice)

-Clicked (Pressed and Released)

-Pressed (Held Down)

-Released (Let Go)

The state of these buttons can be determined through a combination of variables and the various mouse button functions:

mouse_check_button(button) // is true as long as button is pressed

mouse_check_pressed_button(button) // is set to true once on the first press of the button.

It is then set to false, and will not be re-triggered until the mouse is pressed back down.

So basically, all we need to do is test if the left mouse button is pressed using mouse_check_button_pressed(mb_left) in conjuction with several variables such as global.pressed, global.released, etc., and we'll have the mouse code squared away.

Like I said earlier, there's not much to discuss; I think it's best to just have a look at the code yourself.

Before we get buried under a pile of code, let's take a quick look at how we'll keep track of the units.

The method that I find extremely useful is to use list structures to store all of the units.

There are eleven lists, all stored in a global array from 0 to 10 (global.unit_list[0…10])

The first (0th) holds the currently selected units, while the other ten are used for hot-groups (0-9 on the keyboard).

I find it much easier to handle the units using lists rather than an array, or a selected variable, not to mention much faster

It will also come in handy later on if we want to make a little mini-bar for the units (like in Empire Earth), or any other time where need a quick reference to the unit.

We can just look through the list instead of all units…

Alright, let's get the rest of this code setup. To the dump, dump, dump…

First up, make a new object called obj_controller, responsible for spawning all other controller objects, and initializing the game:

Quote:

// Initialize all constants for the game

scr_constants() //Setup Constants

instance_create(-1000,-1000,obj_input_controller) //Create other objects in code so the room doesn't look a mess

//Unit List

for (i=0;i<10+1;i+=1){

global.unit_list = ds_list_create()

}

Now, I don't know about you, but I hate adding Constants to the game.

It's messy, inconvenient, no way to re-order it, etc.

For the most part this is fixed in GM 7.0 by use of adding a constant script as an extension, or by just using a global (since variables no longer have 'global.' in front of them).

Until that happy time, I'll continue using globals as constants.

Script scr_constants

Quote:

//————————————Constants——————————-

// Mouse

global.pressed = 1

global.released = 2

// Hotkeys

global.shift = 0

global.control = 0

global.alt = 0

Technically, global.shift, global.control and global.alt aren't constants.

Shh… I won't tell if you don't.

Anyway, those three variables will store the state of their respective keys.

global.pressed and global.released actually are constants, used for the state of a mouse button.

Now, to code the very last, and very biggest object, obj_input_controller.

First up in the create event, we initialize some variables, with a call to the script, scr_init_input:

Script scr_init_input

Quote:

// Initialize mouse

// Buttons

global.mouse_left = 0 //Left Button State

global.mouse_right = 0 //Right Button State

//Dragging

global.mouse_drag = 0

global.mouse_drag_x = mouse_x //The starting x coordinate for a drag

global.mouse_drag_y = mouse_y //The starting y coordinate for a drag

// Clicking-Hovering

global.click = 0 // Whether the mouse has been clicked

global.click_id = -1 //The id of the instance last-clicked by the mouse

global.mouse_over = - 1 //The id of the instance the mouse is hovering over

//Double-Clicking

global.double_click = 0 //Whether the mosue has been double-clicked

global.double_click_time = 200 //Time allowed for double-click

global.mouse_click_timer = current_time + global.double_click_time // Tracks time left to double-click

It will make more since if we move the global.shift, global.control and global.alt 'constant' out of scr_constants and move them into scr_init_input.

So let's do that, sticking it at the top…

Now, in the begin step event, still of obj_input_controller, we make calls to three more scripts:

Quote:

scr_get_input()

scr_check_selection()

scr_check_hotkeys()

The first script is responsible for getting the input.

We check all of the mouse buttons and their various combinations and store the results in one or two variables:

Script scr_get_input

Quote:

// Clear out old mouse states

global.double_click = false //Clear double-click state

if global.mouse_drag = 2{ //Released Drag

global.mouse_drag = 0 //Clear drag state

}

if current_time > global.mouse_click_timer{ // Clear Double-Click State (too slow)

global.click = 0

}

if global.mouse_left = global.released{ // Clear Release State

global.mouse_left = 0

global.mouse_click_timer = current_time + global.double_click_time

}

if global.mouse_right = global.released{ // Clear Release State

global.mouse_right = 0

}

global.mouse_over = collision_point(mouse_x,mouse_y,all,true,false) // Object Mouse Hovers Over

//Clicking - Left Button

if mouse_check_button_pressed(mb_left) && global.mouse_left !=global.pressed{ // First Press

global.mouse_left = global.pressed // Set mouse state

global.mouse_drag_x = mouse_x // Set Drag X

global.mouse_drag_y = mouse_y // Set Drag Y

if global.click = 0 and global.double_click = false{ // Single-Click

global.click = 1 //Set click state to single-click

global.click_id = global.mouse_over // Instance receiving click

global.mouse_click_timer = current_time + global.double_click_time //Start timer for double-click

}else if global.click = 1{ //Test For Double-Click

global.click = 0

if current_time < global.mouse_click_timer && global.mouse_over = global.click_id{ // Double-Click

global.double_click = true

}

}

}

//Clicking - Right Button

if mouse_check_button_pressed(mb_right) && global.mouse_right !=global.pressed{

global.mouse_right = global.pressed

}

//Dragging

if global.mouse_left = global.pressed && global.mouse_drag = 0 &&

point_distance(global.mouse_drag_x,global.mouse_drag_y,mouse_x,mouse_y) > 5{ //Test for drag

global.mouse_drag = 1

}

//Releasing

if !mouse_check_button(mb_left) && global.mouse_left = global.pressed{ // Releasing Left Mouse Button

global.mouse_left = global.released

if global.mouse_drag = 1{

global.mouse_drag = 2

}

}

if !mouse_check_button(mb_right) && global.mouse_right = global.pressed{ //Releasing Right Mouse Button

global.mouse_right = global.released

}

if global.double_click = true{ // Clear the mouse after a double-click

mouse_clear(mb_left)

}

The first thing that we want to do is clear out several previous mouse states.

Releasing the mouse, the first press of the mouse, Double-Clicking, etc.

All of these only last for one step before being cleared.

The bulk of the code is where we check if the left-mouse button is being pressed for the same time.

If it is pressed, we also set the starting drag coordinates, as well as start the timer for double-clicking.

Finally, we check if the mouse is being dragged, or release.

For the dragging, we allow a mouse_zone of 5 pixels to give the user a little room.

Whew! In the next script, scr_check_selection, we check if the units are being selected.

Basically, we check the global.mouse_over variable to see if the mouse is even over anything.

If the mouse is over something, we check if it's a unit (later on, we'll have to add a check to see if it's a friendly unit).

After that, it's a matter of if statements, checking whether it's a click, double-click, or drag select, and whether shift(Add-unit) or control (Remove Unit) is pressed:

Quote:

if global.mouse_over{ // If the mouse is over anything

if object_get_parent(global.mouse_over.object_index) = obj_unit and global.mouse_left = global.pressed and !global.mouse_drag{ // If the left mouse button is pressed and over a unit

unit_id = global.mouse_over // Set the unit_id to global.mouse_over

global.unit_type = unit_id.object_index // Get the type/class of the unit

if !global.double_click{ // Single-Click

if global.shift{ //Shift-key held

if !unit_id.selected{ //If the unit hasn't already been selected

unit_id.selected = 1

ds_list_add(global.unit_list[0],unit_id) //Add it to the list

}

}else if global.control{

if unit_id.selected{

unit_id.selected = 0

ds_list_delete(global.unit_list[0],ds_list_find_index(global.unit_list[0],unit_id))

}

}else{

ds_list_clear(global.unit_list[0]) // Clear out the unit list

with(obj_unit){

selected = 0

}

unit_id.selected = 1

ds_list_add(global.unit_list[0],unit_id)

}

}else{ // Double-Click

if global.shift{ // Add units to Selection List

with(obj_unit){

if !selected and object_index = global.unit_type{

ds_list_add(global.unit_list[0],id)

selected = 1

}

}

}else if global.control{ //Remove Units from Selection List

with(obj_unit){

if object_index = global.unit_type and selected = 1{

selected = 0

ds_list_delete(global.unit_list[0],ds_list_find_index(global.unit_list[0],id))

}

}

}else if !global.control and !global.shift{ // Clear List and add new Units

//mouse_clear(mb_left)

ds_list_clear(global.unit_list[0]) // Clear out the unit list

with(obj_unit){

selected = 0

}

with(obj_unit){ //Reset all units selected variable to zero. Should actually run through list

if object_index = global.unit_type{

ds_list_add(global.unit_list[0],id)

selected = 1

}

}

}

}

}

}else if global.mouse_left = global.released and global.mouse_drag = 0{

ds_list_clear(global.unit_list[0]) // Clear out the unit list

with(obj_unit){

selected = 0

}

global.click = 0

}

if global.mouse_drag = 2 and global.mouse_drag_x !=mouse_x and global.mouse_drag_y !=mouse_y{ //Released

x1 = min(global.mouse_drag_x,mouse_x)

y1 = min(global.mouse_drag_y,mouse_y)

x2 = max(global.mouse_drag_x,mouse_x)

y2 = max(global.mouse_drag_y,mouse_y)

// Check for units

// Any objects belonging to player get selected

if global.shift{

instance_deactivate_region(x1,y1,x2-x1,y2-y1,false,true)

with(obj_unit){

selected = 1

ds_list_add(global.unit_list[0],id)

}

}else if global.control{ //Remove object

instance_deactivate_region(x1,y1,x2-x1,y2-y1,false,true)

with(obj_unit){

selected = 0

ds_list_delete(global.unit_list[0],ds_list_find_index(global.unit_list[0],id))

}

}else{

with(obj_unit){

selected = 0

}

instance_deactivate_region(x1,y1,x2-x1,y2-y1,false,true)

ds_list_clear(global.unit_list[0])

with(obj_unit){

selected = !selected

ds_list_add(global.unit_list[0],id)

}

}

instance_activate_all()

}

Finally, the last script, scr_check_hotkeys, checks if any hotkeys are pressed.

Right now we only have the number 0-9 for hotkeys:

Script scr_check_hotkeys:

Quote:

var j,iid;

//Hot-Keys

//048-057

if keyboard_key >=048 && keyboard_key <=057{

global.group = 9 - (57 - keyboard_key) + 1 // Number of Hotkey pressed. Add one to amount because '0' is default list

if keyboard_check(vk_control){ //Create Group

ds_list_clear(global.unit_list[global.group])

for (j=0;j<ds_list_size(global.unit_list[0]);j+=1){ //Loop through current list

ds_list_add(global.unit_list[global.group],ds_list_find_value(global.unit_list[0],j)) // Add selected unit to specified list

}

}else{ //Select group

if !keyboard_check(vk_shift){

with (obj_unit){ //Reset Selection to 0

selected = 0

}

}

ds_list_clear(global.unit_list[0])

for (j=0;j<ds_list_size(global.unit_list[global.group]);j+=1){ //Loop through List

iid = ds_list_find_value(global.unit_list[global.group],j) //Pull Id from list

ds_list_add(global.unit_list[0],iid) //Add unit to default list (0)

iid.selected = 1

}

}

}

For the hotkey script, we check if one of the number keys is being pressed, and if so, copy the list to the default one, selecting the units along the way by setting selected to true.

Something to watch out for…

If you use show_message() in the script (which I was, to make sure there were no off by one errors with the groups selecting) it clears out the keyboard and mouse states.

This makes sense, but when I tried to store the keyboard_key in a variable before calling show_message() it still wouldn't work properly!

I have no idea why and am still trying to figure out the cause and I'd be gratified if you dropped me a line if you know.

So to summarize, I sacrificed a half and hour of my life, and am valiantly warning you now so you won't do the same. [GRIN]

Just two more pieces of code!

The first one just gives us something to test out with the units

If we press the left or right arrow keys, selected objects will spin around:

obj_input_controller begin step event

Quote:

if keyboard_check(vk_left){

//Spin left

with (obj_unit){

if selected{

direction -=10

}

}

}

if keyboard_check(vk_right){

//Spin left

with (obj_unit){

if selected{

direction +=10

}

}

}

And last, the code to draw the selection box.

This goes in the draw event by the way:

obj_input_controller Draw Event

Quote:

if global.mouse_drag{

draw_set_color(c_blue)

draw_rectangle(global.mouse_drag_x,global.mouse_drag_y,mouse_x,mouse_y,1)

}

Place obj_controller in a room along with a few units and give it a spin.

Voila! Hopefully, everything runs smoothly and you can select and spin units around.

If not, I tremendously hope that I didn't ruin any code.

I checked over everything but there's always that possibility that I missed something, and if that turns out to be the case, feel free to let out your aggravation at me.[GRIN]

Remember that you can always take a look at the source files as well.

I hope you've found this tutorial useful, or you've learned something new.

Next time, we'll be working on a HUD and scrollable view…

If you have any suggestions, comments, or ideas, for future tutorials, send me an e-mail at

rtstutorial at gamedevguru.com (Replace 'at' with @ sign)

You can also send me a PM, or post a comment and I'll get back to you as soon as possible!

Until then,

TwistyWristy

Comments

TwistyWristy 17 years, 11 months ago

Sorry everyone had trouble with the formatting.

Had to use
Quote:
instead of
.
The links to the included files are now available.

TwistyWristy

Josea 17 years, 11 months ago

Doesn't the

 tag keep the spaces?

TwistyWristy 17 years, 11 months ago

No,

 won't break the lines properly.
Quotes is messing up the indentation though, let me try to fix it...

TwistyWristy

Theodore III 17 years, 11 months ago

Wall of text

Josea 17 years, 11 months ago

Quote:
Wall of text[/skull]

It is supposed to be a tutorial -_-

Polystyrene Man 17 years, 11 months ago

Quote:
Wall of text
Some of us liked reading it…

I've got to say, this is one of the best Game Maker tutorials I've seen. After all the difficulties I've had trying to make an RTS, I don't know if I'll make another attempt… but I'll definitely be keeping up to date with this. Keep up the good work!

TwistyWristy 17 years, 11 months ago

Polystyrene Man

Thanks! [GRIN]

Does anyone know of anything off-hand that can parse GML code to BBcode? OR even RTF to BBcode?

I'd rather use something that's already out there than make my own…

TwistyWristy