Hello! I'm making a Task list with a listbox. When a task is completed, a button with an image of a line appears on top of it, marking it as completed. For each of the tasks, I have an invisible button image ready. This works fine:
function RemoveTaskListItem(String nameofentry) {
int index;
while (index < lstTaskList.ItemCount) {
// Button with a line appears
if(lstTaskList.Items[index].CompareTo(nameofentry) ==0) {
if(index ==0) btnTaskList_0.Visible = true;
if(index ==1) btnTaskList_1.Visible = true;
if(index ==2) btnTaskList_2.Visible = true;
}
else index ++;
}
}
However, I'm trying to make it a bit more fancy and add categories to it: There is the topic and maybe three tasks under it. Now it can happen that a new task is added inside a topic, using InsertItemAt command. That will move all the listbox items under it nicely, but it will break the button placement, if there already is a completed task below.
My question is how to link my button images to the Listbox items? When the order of the list changes, the buttons placement needs to be updated, but I can't really figure out a good way to do this.
Any ideas? Thanks!
The traditional approach to this kind of task is in separating the Data and a View, where Data is stored in variables and View is a representation that shows the data on screen.
In practical sense this means that you should not make buttons rely on actual items inside ListBox, and instead make both controls rely on what is in variables, and which range of data are you displaying on screen right now.
Whenever you display a GUI on screen, you read tasks from the variables, and appoint ListBox contents and Buttons accordingly.
Whenever anything changes, while GUI is on screen (tasks added, removed, task state changes, etc), you run the update on it, and again read data from variables and reappoint ListBox and Buttons.
Because ListBox may have a lot of items in it but display only certain range at once, you'd also need to know that range and adjust buttons whenever ListBox is scrolled by the player. I don't know if you deal with this now, and if yes then how. In the simple case you just need to know the range: first index to display, and displayed count.
This approach, while seemingly more complex, will give you much more freedom in what and how do you display on screen.
How may be a list of tasks stored in variables? This may be an array of "Task" structs that describe them.
To give an example (I am making things up here, not knowing what do you have in your tasks).
struct Task
{
String Name;
bool Active;
bool Complete;
int NumSubTasks; // number of subtasks in this task
};
Task GameTasks[MAX_TASKS];
In this example, the subtasks are stored in the same "Task" array, following their parent tasks, and NumSubTasks is set in parent Task to tell how many subtasks does it have.
This is kind of a ugly structure, but works for a simpler case. If you need a bigger, expandable tree-like structure of tasks and subtasks, another structure would be required, but that's a broader topic.
On a game start initialize this array. Subtasks should follow the parent tasks, for example:
function game_start()
{
GameTasks[0].Name = "Go shopping";
GameTasks[0].NumSubTasks = 3; // next 3 tasks are subtasks of this one
GameTasks[1].Name = "Buy food";
GameTasks[2].Name = "Buy a dress";
GameTasks[3].Name = "Buy a new carpet";
}
Whenever you add or remove your tasks, you do so inside this array of structs, e.g. by setting their Active bool to true or false.
Then the ListBox may be filled by running over GameTasks array, and fill in only items that you like to display: All, only Active, only Completed, etc, based on variables in struct.
The Buttons then are updated based on:
- the top index of displayed task
- the task's state
For a very crude example:
function UpdateTasksGUI()
{
lstTaskList.Clear();
for (int i = 0; i < MAX_TASKS; i++)
{
if (GameTasks[i].Active)
{
lstTaskList.AddItem(GameTasks[i].Name);
}
}
UpdateButtons();
}
function OnTaskListScrolled()
{
UpdateButtons();
}
function UpdateButtons()
{
int top_index = ListBox.TopItem; // an index of active tasks visible on GUI
int row_count = ListBox.RowCount; // how many active tasks are visible on GUI
int active_task_index = 0; // an index of active tasks in a list
int button_index = 0; // the next index of a button to use
for (int i = 0; i < MAX_TASKS; i++)
{
if (GameTasks[i].Active)
{
if (active_task_index >= top_index)
{
// Find the next available button on GUI
int but_id = btnTaskList_0.ID + button_index;
Button *button = taskListGUI.Controls[but_id].AsButton;
button.Visible = GameTasks[i].Completed;
button_index++; // button used, moved to next
}
active_task_index++
}
if (active_task_index >= top_index + row_count)
{
break; // no more items are visible on gui
}
}
}
Wonderful! Thanks so much for this CW! I was kind of transitioning to some system like this, but didn't know how (in my previous game the simple and crude task list worked well enough). I'll start testing this approach now.
QuoteBecause ListBox may have a lot of items in it but display only certain range at once, you'd also need to know that range and adjust buttons whenever ListBox is scrolled by the player. I don't know if you deal with this now, and if yes then how. In the simple case you just need to know the range: first index to display, and displayed count.
I was thinking to simulate a paper task list, and if the page is full, it will continue on the next page. I'm sure it would cause all sorts of problems, but a scrolling list doesn't really fit into the game's atmosphere.
That's the idea of separation between data and view
You would just grab a fixed amount of items that can be fit in a page (or some other approach if the amount of text is like a lot) and then just make the navigation to be page based. Like say you can get how many lines each item has and you have a "page line budget", you get enough to fill a page and then it goes to the next one, and so on.
If the view and data are decoupled you can reason and break the problem more easily.
Did some quick testing and it seems to work nicely! Buttons appear in the right places etc. I had to do some adjustments to make it work. This for example was giving me error in the UpdateButtons function:
int top_index = ListBox.TopItem; // an index of active tasks visible on GUI
int row_count = ListBox.RowCount; // how many active tasks are visible on GUI
I changed it into this, and got rid of the error:
int top_index = lstTaskList.TopItem; // an index of active tasks visible on GUI
int row_count = lstTaskList.RowCount; // how many active tasks are visible on GUI
Also not 100% sure what the "NumSubTasks" does actually? What would be the difference if I wouldn't use it?
I'll continue with this next week, and probably have more questions. But huge thanks already!
Quote from: rongel on Fri 30/08/2024 18:15:35Also not 100% sure what the "NumSubTasks" does actually? What would be the difference if I wouldn't use it?
This was more a prototype. But that may come useful if you need to distinguish subtasks. For example, you may want to indent a subtask with couple of spaces in a listbox. Then you'll need to find out which items are subtasks.
One issue I noticed now: In the game, the player can get these tasks quite freely in a non-linear order. The problem is that now the task placement is fixed to the order of the array at game_start. So when a player gets a new task, it could jump at the beginning of the list, or in the middle of it, rather than appearing at the bottom (like when using ListBox.AddItem).
The optimal situation would be that the new tasks appear at the bottom of the list, unless they are subtasks. Subtasks would appear in the correct category, and rearrange the list.
You could add a separate array containing indexes of tasks in the order of them being received.
And then when you are updating GUI you iterate this array instead.
For example:
int TaskReceived[MAX_TASKS];
int TaskReceivedCount;
function AddTask(int taskIndex, bool addSubtasks)
{
TaskReceived[TaskReceivedCount++] = taskIndex;
if (addSubtasks)
{
// add subtasks too (they have sequential indexes)
for (int i = 0; i < GameTasks[taskIndex].NumSubTasks; i++)
{
TaskReceived[TaskReceivedCount++] = taskIndex + i;
}
}
}
If a subtask has to be inserted later, then insert one in the middle, and shift all indexes
function AddSubTask(int parentIndex, int subIndex)
{
int insertAt = -1;
for (int i = 0; i < TaskReceivedCount; i++)
{
if (TaskReceived[i] == parentIndex)
{
insertAt = i + 1;
// Find if other subtasks are already added and skip these
int maxSubs = GameTasks[parentIndex].NumSubTasks;
for (int j = i + 1; (j < TaskReceivedCount) && (TaskReceived[j] < parentIndex + maxSubs); j++)
{
insertAt++;
}
}
}
// Copy everything to the right to free the index, in the reverse order
for (int i = TaskReceivedCount; i > insertAt; i--)
{
TaskReceived[i] = TaskReceived[i - 1];
}
// And insert a subtask on its place
TaskReceived[insertAt] = subIndex;
TaskReceivedCount++;
}
and then gui update:
function UpdateTasksGUI()
{
lstTaskList.Clear();
for (int i = 0; i < TaskReceivedCount; i++)
{
int taskID = TaskReceived[i];
lstTaskList.AddItem(GameTasks[taskID].Name);
}
And UpdateButtons changed accordingly.
function UpdateButtons()
{
<...>
for (int i = 0; i < TaskReceivedCount; i++)
{
int taskID = TaskReceived[i];
if (active_task_index >= top_index)
{
// Find the next available button on GUI
int but_id = btnTaskList_0.ID + button_index;
Button *button = taskListGUI.Controls[but_id].AsButton;
button.Visible = GameTasks[taskID].Completed;
button_index++; // button used, moved to next
}
active_task_index++
}
<...>
}
On a separate note, I think that above structure may also be used if there are more nested subtask levels.
The Task struct could use a "ParentID" field for easier finding a parent in case we need to go from child to parent when reading tasks.
And some of the operations from above will have to become recursive (calling same function from inside a function for the nested child level of tasks).
Thanks again for the great help! That kind of system seems perfect for my purposes. I did some testing with it and got partial success.
First of all, I got an error saying "Error parsing path; unexpected token after array index" from these lines in the AddTask function:
TaskReceived[TaskReceivedCount++] = taskIndex;
TaskReceived[TaskReceivedCount++] = taskIndex + i;
I managed to get rid of the error by formatting it into TaskReceived[TaskReceivedCount +1], but I couldn't get any of the tasks appear with it. I simplified the code for testing purposes and got the "basic" addTask function to work by doing this:
function AddTask(int taskIndex)
{
TaskReceived[TaskReceivedCount] = taskIndex;
TaskReceivedCount ++;
}
Now I can add tasks and they appear at the end of the task list. I tried to do the same (and many other things) with the addSubtasks bool, but couldn't get it to work properly, it was giving me duplicates etc.
The function AddSubTask(int parentIndex, int subIndex) seems to work nicely, with that I can insert subtasks under the main task in correct position.
So there's some kind of formatting problem with the AddTask function. Any ideas how to properly fix it, and get the addSubtasks part working as well?
It appears that the old compiler cannot handle ++ operator inside brackets.
TaskReceived[TaskReceivedCount] = taskIndex;
TaskReceivedCount++;
^ this is a correct equivalent.
A full function will look like
function AddTask(int taskIndex, bool addSubtasks)
{
TaskReceived[TaskReceivedCount] = taskIndex;
TaskReceivedCount++;
if (addSubtasks)
{
// add subtasks too (they have sequential indexes)
for (int i = 0; i < GameTasks[taskIndex].NumSubTasks; i++)
{
TaskReceived[TaskReceivedCount] = taskIndex + 1 + i;
TaskReceivedCount++;
}
}
}
EDIT: Ah, it should be "taskIndex + 1 + i;" for subtasks, fixed that mistake.
Ok, things seem to work much better now! A couple of questions:
1. How exactly should I use the subTask feature at game start? Let's say I have three subtasks, two of them active at game start and one will get activated later on. For example:
GameTasks[0].Name = "Go shopping";
GameTasks[0].NumSubTasks = 3; // next 3 tasks are subtasks of this one
GameTasks[1].Name = "Buy food";
GameTasks[2].Name = "Buy a dress";
GameTasks[3].Name = "Buy a new carpet";
AddTask(0, true);
Now all the subtasks will be visible already from the beginning, which is bad. The carpet option should be added later. Is this the correct way to do it:
GameTasks[0].NumSubTasks = 2;
Now the third option doesn't get shown yet. And then later on I'll do this:
AddSubTask(0, 3);
2. I'd prefer that the later on added subtask would go to the bottom of the substack list. Right now it seems to go in the middle, or one row below the first item? Is there a way to do it by editing the insertAt int?
if (TaskReceived[i] == parentIndex)
{
insertAt = i + 1;
// Find if other subtasks are already added and skip these
int maxSubs = GameTasks[parentIndex].NumSubTasks;
for (int j = i + 1; (j < TaskReceivedCount) && (TaskReceived[j] < parentIndex + maxSubs); j++)
{
insertAt++;
}
}
If you want certain subtasks to not be visible, do not call AddTask with "addSubtasks" true.
Instead call it with "false" and then do AddSubTask for each subtask that you want to make active.
QuoteI'd prefer that the later on added subtask would go to the bottom of the substack list. Right now it seems to go in the middle, or one row below the first item? Is there a way to do it by editing the insertAt int?
When I wrote the code the intent was that the new active subtasks is appended after all other active subtasks, or right after parent if no other subtasks are active.
If it does not work like that, then there's a mistake in code.
EDIT: probably the condition is wrong, should be:
for (int j = i + 1; (j < TaskReceivedCount) && (TaskReceived[j] > parentIndex && TaskReceived[j] < parentIndex + maxSubs); j++)
Quote from: Crimson Wizard on Sun 01/09/2024 14:58:44EDIT: probably the condition is wrong, should be:
for (int j = i + 1; (j < TaskReceivedCount) && (TaskReceived[j] > parentIndex && TaskReceived[j] < parentIndex + maxSubs); j++)
I tested the new code, but got similar results. The added subtask goes on in the middle of the subtask list, not bottom. It could be that I've made some mistake, so just to clarify, here's what I've done:
At game_start:
GameTasks[0].Name = "Go shopping";
GameTasks[0].NumSubTasks = 3; // next 3 tasks are subtasks of this one
GameTasks[1].Name = " - Buy food";
GameTasks[2].Name = " - Buy a dress";
GameTasks[3].Name = " - Buy a new carpet";
AddTask(0, false); // shopping
AddTask(2, false); // dress
AddTask(3, false); // carpet
UpdateTasksGUI_v2();
Then I add the third subtask later in the game:
AddSubTask(0, 1); // food
UpdateTasksGUI_v2();
Which does this:
// ADD SUBTASK
function AddSubTask(int parentIndex, int subIndex)
{
int insertAt = -1;
for (int i = 0; i < TaskReceivedCount; i++)
{
if (TaskReceived[i] == parentIndex)
{
insertAt = i + 1;
// Find if other subtasks are already added and skip these
int maxSubs = GameTasks[parentIndex].NumSubTasks;
for (int j = i + 1; (j < TaskReceivedCount) && (TaskReceived[j] > parentIndex && TaskReceived[j] < parentIndex + maxSubs); j++)
{
insertAt++;
}
}
}
// Copy everything to the right to free the index, in the reverse order
for (int i = TaskReceivedCount; i > insertAt; i--)
{
TaskReceived[i] = TaskReceived[i - 1];
}
// And insert a subtask on its place
TaskReceived[insertAt] = subIndex;
TaskReceivedCount++;
}
The end result is still that the added subtask goes between subtask 2 (dress) and subtask 3 (carpet) instead of going to the bottom of the list. If I add subtask 1 (food) and subtask 2 (dress) at game_start, and the add subtask 3 (carpet) later, only then it will go to the bottom. This is a quite minor issue, but it would be great to get it working 100%!
I had to make a game and test this script to find mistakes.
The updated script:
function AddTask(int taskIndex, bool addSubtasks)
{
TaskReceived[TaskReceivedCount] = taskIndex;
TaskReceivedCount++;
if (addSubtasks)
{
// add subtasks too (they have sequential indexes)
for (int i = 0; i < GameTasks[taskIndex].NumSubTasks; i++)
{
TaskReceived[TaskReceivedCount] = taskIndex + 1 + i;
TaskReceivedCount++;
}
}
}
function AddSubTask(int parentIndex, int subIndex)
{
int insertAt = -1;
for (int i = 0; i < TaskReceivedCount; i++)
{
if (TaskReceived[i] == parentIndex)
{
// Find if other subtasks are already added and skip these
int maxSubs = GameTasks[parentIndex].NumSubTasks;
for (insertAt = i + 1;
(insertAt < TaskReceivedCount) && (TaskReceived[insertAt] > parentIndex && TaskReceived[insertAt] <= (parentIndex + maxSubs));
insertAt++)
{
}
}
}
// Copy everything to the right to free the index, in the reverse order
for (int i = TaskReceivedCount; i > insertAt; i--)
{
TaskReceived[i] = TaskReceived[i - 1];
}
// And insert a subtask on its place
TaskReceived[insertAt] = parentIndex + 1 + subIndex;
TaskReceivedCount++;
}
Also, this will only work properly if you use AddSubTask when adding sub-tasks, and not AddTask.
Subtask's index is also 0-based:
AddTask(0, false); // shopping
AddSubTask(0, 1); // dress
AddSubTask(0, 2); // carpet
UpdateTasksGUI();
AddSubTask(0, 0); // food
UpdateTasksGUI();
I've done some testing with this, it seems to work perfectly now. Thank you very much CW!
I'm now working on setting up the Task List page limit, so that each page can hold only limited amount of tasks.
I did some awful coding, but it *seems* to work. Currently I'm testing with 5 tasks per page limit:
function UpdateTasksGUI()
{
lstTaskList.Clear();
int i = 0;
// Tasklist page 1
if(Tasklist_page == 1) {
while (i < TaskReceivedCount && i < 5)
{
int taskID = TaskReceived[i];
lstTaskList.AddItem(GameTasks[taskID].Name);
i ++;
}
}
// Tasklist page 2
else if(Tasklist_page == 2) {
i = 5;
while (i < TaskReceivedCount && i < 10)
{
int taskID = TaskReceived[i];
lstTaskList.AddItem(GameTasks[taskID].Name);
i ++;
}
}
UpdateButtons();
}
I added a similar check to the UpdateButtons function and the button behavior *seems* to work as well. I think I can make the code a bit nicer, but can you already see if there's a problem brewing? Or is this approach ok?
The rule of thumb is: when something depends on a condition, put only that under condition, and leave the rest out.
What does depend on a "page" condition? Only the range of items does. Therefore the only thing that should be done under "if(Tasklist_page == X)" is assigning "first item" and "end item" or "item count" variables.
int start_at;
int end_at;
if(Tasklist_page == 1) {
start_at = 0;
end_at = 5;
}
else if(Tasklist_page == 2) {
start_at = 5;
end_at = 10;
}
<...>
for (int i = start_at; i < TaskReceivedCount && i < end_at; i++)
{
int taskID = TaskReceived[i];
lstTaskList.AddItem(GameTasks[taskID].Name);
}
But in your case even that is not necessary. As you may see, the start and end indexes have a linear dependency on Tasklist_page value. So the code becomes:
int start_at = Tasklist_page * 5;
int end_at = start_at + 5;
for (int i = start_at; i < TaskReceivedCount && i < end_at; i++)
{
int taskID = TaskReceived[i];
lstTaskList.AddItem(GameTasks[taskID].Name);
}
Quote from: Crimson Wizard on Tue 03/09/2024 12:00:26But in your case even that is not necessary. As you may see, the start and end indexes have a linear dependency on Tasklist_page value. So the code becomes:
int start_at = Tasklist_page * 5;
int end_at = start_at + 5;
for (int i = start_at; i < TaskReceivedCount && i < end_at; i++)
{
int taskID = TaskReceived[i];
lstTaskList.AddItem(GameTasks[taskID].Name);
}
Beautiful! I'll start working on the proper version now, hopefully won't run into nasty problems anymore.
I'll add you in the game credits for sure!